LJKのBlog

学无止境

vim 有三种模式:

1
2
3
- 命令模式 或 普通模式
- 插入模式 或 编辑模式
- 扩展命令模式 或 命令行模式 或 末行模式

命令模式

光标跳转

字符间跳转
1
2
3
4
j      # 下
k # 上
h # 左
l # 右
单词间跳转
1
w      # 定位下一个单词
当前页跳转
1
2
3
4
5
6
H      # 定位页首
M # 定位中间行
L # 定位页末
zt # 滚动屏幕, 使当前行置顶 top
zz # 滚动屏幕, 使当前行置中
zb # 滚动屏幕, 使当前行置底 bottom
行首行尾跳转
1
2
3
^      # 定位行首第一个非空字符
0 # 定位行尾
$ # 定位行首
行间移动
1
2
3
gg     # 定位文首
G # 定位文末
nG # 定位指定行行首, 例如定位第5行: `5G`
句间和段落间移动
1
2
3
4
)      # 下一句
( # 上一句
} # 下一段
{ # 上一段
翻屏操作
1
2
3
4
ctrl+f     # 向后翻一屏
ctrl+d # 向后翻半屏
ctrl+b # 向前翻一屏
ctrl+u # 向前翻半屏

字符编辑

1
2
3
4
x     # 剪切光标处字符
p # 黏贴
~ # 转换大小写, 然后光标向后移一位
J # 删除当前行前后的换行符

替换 replace

1
2
r    # 替换光标所在处的一个字符, 按下r,然后再按替换后的字符
R # 切换成REPLACE模式, ESC回到命令模式, 按下r, 然后再输入替换后的内容

删除 delete

1
2
3
4
5
6
7
d      # 删除, 可结合光标跳转, 实现范围删除
d$ # 删除到行尾
d0 # 删除到行首
d^ # 删除到非空行首
dd # 删除当前行
#dd # 删除多行, 从当前行开始, 删除#行
D # 删除到行尾, 等同于d$

复制 yank

1
2
y  # 复制, 行为相似于d命令
Y # 复制整行

粘贴 paste

1
2
p  # 整行, 则粘贴到下一行, 否则粘贴到光标后
P # 整行, 则粘贴到上一行, 否则粘贴到光标后

命令模式 切换到 插入模式

以下快捷键, 进入插入模式

1
2
3
4
5
6
i    # 不动
I # 定位行首
a # 向后移一位
A # 定位行尾
o # 定位下一行行首, 并开辟新行
O # 定位上一行行首, 并开辟新行

插入模式

从光标前输入数据

末行模式

1
2
3
4
5
6
:wq         # 存盘并退出
:q! # 不存盘强制退出
:r file # 将file中的内容写入到当前文件, 例如\`:r a.log\`就把a.log中的内容写入到当前文件
:w file # 将当前文件内容写入file文件, 如果file已存在, 需要\`:w! file\`强制覆盖文件
:!command # 不退出文件,执行命令
:r!command # 将执行命令的输出写入到当前文件

地址定界:

1
2
3
4
5
6
7
8
9
10
11
n   # 第几行, 例如2表示第2行
n,n # 第#行到到第#行, 例如 2,5 表示2到5行
n,+n # 第#行加#行, 例如 2,+3 表示2到5行
. # 当前行
$ # 最后一行, .,$-1 表示当前行到倒数第二行
% # 全文, 相当于1、$

/pattern/ # 从当前行开始,直到第一次匹配到的行
/pattern1/,/pattern2/ # 从pattern1第一次匹配到行, 到pattern2第一次匹配到的行
n,/pattern/ # 从指定行开始, 到第一次pattern匹配到行
/pattern/,$ # 从pattern第一次匹配到的行, 到最后一行

地址定界后跟一个编辑命令:

1
2
3
4
d     # 删除
y # 复制
w file # 将范围内的行另存到file文件中
r file # 在指定位置插入file文件中的所有内容

查找并替换:

1
2
3
4
5
6
7
8
9
s/要查找的内容/替换为的内容/修饰符  # 当前行查找替换
%s/要查找的内容/替换为的内容/修饰符 # 全文查找替换

要查找的内容可使用正则
替换为的内容不能使用正则,但可以使用\1, \2, ...等后向引用符号;还可以使用“&”引用前面查找时查找到的整个内容
修饰符:
i 忽略大小写
g 全局替换
gc 全局替换, 每次替换前询问
1
2
3
4
u    # 撤销, 相当于windows中的ctrl+z
#u # 撤销多步
U # 撤销此行的所有操作
ctrl+r # 取消撤销, 相当于windows中的ctrl+y

高级用法

示例:粘贴”wang”100 次

1
100iwang [ESC]
1
2
3
4
5
di"      # 光标在” “之间,则删除” “之间的内容
yi( # 光标在()之间,则复制()之间的内容
vi[ # 光标在[]之间,则选中[]之间的内容
dtx # 删除字符直到遇见光标之后的第一个 x 字符
ytx # 复制字符直到遇见光标之后的第一个 x 字符

定制 vim 的工作特性

配置文件:

1
2
/etc/vimrc      # 全局
~/.vimrc # 个人
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
set nu
set nocompatible
set showmode
set showcmd
set encoding=utf-8
set t_Co=256

" 缩进
set autoindent
set tabstop=4

" 外观
set wrap
set linebreak
set wrapmargin=2
set showmatch
set hlsearch
set ignorecase
set smartcase

" 编辑
set autoread
set wildmenu

" 插件
autocmd FileType php set omnifunc=phpcomplete#CompletePHP

set tags=/data/wwwroot/php-7.1.32/tags

可视化模式

进入可视化模式:

1
2
3
v           # 面向字符
V # 面向整行
ctrl + v # 面向块(列)

选中的文字可以被删除、复制、变更、过滤、搜索、替换等

示例:在文件每一行行首插入 # 然后再删除

1
2
3
4
5
6
7
8
9
gg
ctrl + v
G
I
输入#
ESC退出 插入#成功
ctrl + v
d 删除成功
ESC退出

vim 总结



学习目标:

  • 了解SOCKET的基本操作如accept/connectsend/recvcloselistenbind
  • 了解SOCKET的接收缓存区、发送缓存区、阻塞/非阻塞、超时等概念

在计算机通信领域, socket 被翻译为”套接字“,是计算机之间进行通信的一种约定或者一种方式。

声明: 在 windows 和 linux 下 socket 是有差异的, 本文以 linux 下为准,忽略 windows.

“套接字”有哪些类型呢?

这个世界上有很多种套接字(socket),比如 DARPA Internet 地址(Internet 套接字)、本地节点的路径名(Unix 套接字)、CCITT X.25 地址(X.25 套接字)等。但本教程只讲第一种套接字——Internet 套接字,它是最具代表性的,也是最经典最常用的。以后我们提及套接字,指的都是 Internet 套接字。

根据数据的传输方式,可以将 Internet 套接字分成两种类型。通过 socket() 函数创建连接时,必须告诉它使用哪种数据传输方式。

​ Internet 套接字其实还有很多其它数据传输方式,但是我可不想吓到你,本教程只讲常用的两种

流格式套接字(SOCK_STREAM)

流格式套接字(Stream Sockets)也叫“面向连接的套接字”,在代码中使用 SOCK_STREAM 表示。

它使用 TCP 协议来确保数据的正确性,使用 IP 来控制数据如何从源头到达目的地,也就是常说的“路由”。

数据报格式套接字(SOCK_DGAM)

数据报格式套接字(Datagram Sockets)也叫“无连接的套接字”,在代码中使用 SOCK_DGRAM 表示。

数据报套接字也使用 IP 协议作路由,但是它不使用 TCP 协议,而是使用 UDP 协议。

OSI 网络七层模型

OSI 模型把网络通信的工作分为 7 层,从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

OSI 只是存在于概念和理论上的一种模型,它的缺点是分层太多,增加了网络工作的复杂性,所以没有大规模应用。后来人们对 OSI 进行了简化,合并了一些层,最终只保留了 4 层,从下到上分别是接口层、网络层、传输层和应用层,这就是大名鼎鼎的 TCP/IP 模型

这个网络模型究竟是干什么呢?简而言之就是进行数据封装的。

我们平常使用的程序(或者说软件)一般都是通过应用层来访问网络的,程序产生的数据会一层一层地往下传输,直到最后的网络接口层,就通过网线发送到互联网上去了。数据每往下走一层,就会被这一层的协议增加一层包装,等到发送到互联网上时,已经比原始数据多了四层包装。整个数据封装的过程就像俄罗斯套娃。

当另一台计算机接收到数据包时,会从网络接口层再一层一层往上传输,每传输一层就拆开一层包装,直到最后的应用层,就得到了最原始的数据,这才是程序要使用的数据。

给数据加包装的过程,实际上就是在数据的头部增加一个标志(一个数据块),表示数据经过了这一层,我已经处理过了。给数据拆包装的过程正好相反,就是去掉数据头部的标志,让它逐渐现出原形。

我们所说的 socket 编程,是站在传输层的基础上,所以可以使用 TCP/UDP 协议,但是不能干「访问网页」这样的事情,因为访问网页所需要的 http 协议位于应用层。

两台计算机进行通信时,必须遵守以下原则:

  • 必须是同一层次进行通信,比如,A 计算机的应用层和 B 计算机的传输层就不能通信,因为它们不在一个层次,数据的拆包会遇到问题。
  • 每一层的功能都必须相同,也就是拥有完全相同的网络模型。如果网络模型都不同,那不就乱套了,谁都不认识谁。
  • 数据只能逐层传输,不能跃层。
  • 每一层可以使用下层提供的服务,并向上层提供服务。

TCP/IP 协议族

IP、MAC 和端口号——网络通信中确认身份信息的三要素

对于目前广泛使用 IPv4 地址,它的资源是非常有限的,一台计算机一个 IP 地址是不现实的,往往是一个局域网才拥有一个 IP 地址。换句话说,IP 地址只能定位到一个局域网,无法定位到具体的一台计算机。

其实,真正能唯一标识一台计算机的是 MAC 地址,每个网卡的 MAC 地址在全世界都是独一无二的。计算机出厂时,MAC 地址已经被写死到网卡里面了(当然通过某些“奇巧淫技”也是可以修改的)。局域网中的路由器/交换机会记录每台计算机的 MAC 地址。

MAC 地址是 Media Access Control Address 的缩写,直译为“媒体访问控制地址”,也称为局域网地址(LAN Address),以太网地址(Ethernet Address)或物理地址(Physical Address)。

数据包中除了会附带对方的 IP 地址,还会附带对方的 MAC 地址,当数据包达到局域网以后,路由器/交换机会根据数据包中的 MAC 地址找到对应的计算机,然后把数据包转交给它,这样就完成了数据的传递。

有了 IP 地址和 MAC 地址,虽然可以找到目标计算机,但仍然不能进行通信。为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number),例如,Web 服务的端口号是 80,FTP 服务的端口号是 21,SMTP 服务的端口号是 25。

端口(Port)是一个虚拟的、逻辑上的概念。可以将端口理解为一道门,数据通过这道门流入流出,每道门有不同的编号,就是端口号。如下图所示:

Linux 下的 socket 演示程序

详细信息参考链接。

1
2
//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

AF_INET:使用过 IPv4 地址

SOCK_STREAM:设置套接字

IPPROTO_TCP:使用 TCP 协议,这里也可以写 0,SOCK_STREAM 默认就是 TCP

服务端

socket() 函数确定了套接字的各种属性,bind() 函数让套接字与特定的 IP 地址和端口对应起来,这样客户端才能连接到该套接字。

listen() 让套接字处于被动监听状态。所谓被动监听,是指套接字一直处于“睡眠”中,直到客户端发起请求才会被“唤醒”。

accept() 函数用来接收客户端的请求。程序一旦执行到 accept() 就会被阻塞(暂停运行),直到客户端发起请求。

write() 函数用来向套接字文件中写入数据,也就是向客户端发送数据。

和普通文件一样,socket 在使用完毕后也要用 close() 关闭。

客户端

connect() 向服务器发起请求, 直到服务器传回数据后,connect() 才运行结束。

通过 read() 从套接字文件中读取数据。

socket() 函数

1
int socket(int af, int type, int protocol);

af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET 是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。

type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。

protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。

正如大家所想,一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。

满足 af 和 type 这两个条件的协议只有一种,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议,如下所示:

1
2
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字

bind() 和 connect() 函数

socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。类似地,客户端也要用 connect() 函数建立连接。

listen()和 accept()函数:让套接字进入监听状态并响应客户端请求

对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。

listen() 函数

通过 listen() 函数可以让套接字进入被动监听状态, 所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。

1
int listen(int sock, int backlog);

请求队列

当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。

缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是 10 或者 20。

如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。

当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误,对于 Windows,客户端会收到 WSAECONNREFUSED 错误。

注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。

accept() 函数

当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。

1
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);

accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的 IP 地址和端口号,而 sock 是服务器端的套接字,注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。

最后需要说明的是:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。

send()/recv()和 write()/read():发送数据和接收数据

Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。

前面我们说过,两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用 write() 向套接字写入数据,客户端就能收到,然后再使用 read() 从套接字中读取出来,就完成了一次通信。

Windows 区分普通文件和套接字,并定义了专门的接收和发送的函数: 从服务器端发送数据使用 send() 函数; 在客户端接收数据使用 recv() 函数;

如何让服务器端持续不断地监听客户端的请求?

答: 使用 while

socket 缓冲区以及阻塞模式详解

socket 缓冲区

每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。

write() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由 TCP 协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是 TCP 协议负责的事情。

TCP 协议独立于 write() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。

read() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。

TCP套接字I/O缓冲区示意图

这些 I/O 缓冲区特性可整理如下:

  • I/O 缓冲区在每个 TCP 套接字中单独存在;
  • I/O 缓冲区在创建套接字时自动生成;
  • 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
  • 关闭套接字将丢失输入缓冲区中的数据。

输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:

阻塞模式

对于 TCP 套接字(默认情况下),当使用 write() 发送数据时:

  1. 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write() 函数继续写入数据。
  2. 如果 TCP 协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。
  3. 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。
  4. 直到所有数据被写入缓冲区 write() 才能返回。

当使用 read() 读取数据时:

  1. 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
  2. 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断囤积,直到有 read() 函数再次读取。
  3. 直到读取到数据后 read() 函数才会返回,否则就一直被阻塞。

这就是 TCP 套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。

​ CP 套接字默认情况下是阻塞模式,也是最常用的。当然你也可以更改为非阻塞模式,后续我们会讲解。

TCP 协议的粘包问题(数据的无边界性)

图解 TCP 数据报结构以及三次握手(非常详细)

1
2
sudo apt install autoconf
sudo apt-get install opencc

github 地址
下载 zip 压缩包

1
2
3
4
sudo autoreconf --install --force
sudo ./configure --prefix=/usr/local/zhman --disable-zhtw
sudo make
sudo make install
1
2
echo "alias cman='man -M /usr/local/zhman/share/man/zh_CN' " >> ~/.bashrc
. ~/.bashrc

快照原理

将当前虚拟机的虚拟硬盘文件锁定,不再更改,之后新建一个文件,之后所有更改都放到新建的文件中,读取时,优先读取这个中的,没有的话在读取锁定中的数据。所以快照占的空间取决于你做了多少更改

移动和复制

选择移动不会改变,选择复制会改变 ip、mac 等信息。

主机能 ping 通虚拟机,虚拟机 ping 不通主机

  1. 打开 WIN7 防火墙
  2. 选择高级设置
  3. 入站规则
  4. 找到配置文件类型为“公用”的“文件和打印共享(回显请求  – ICMPv4-In)”规则,设置为允许。如果“公用”已经开启,那就再开启“专用”
  5. 虚拟机可以 ping 通主机了。

  1. Win+R – regedit
  2. 找到以下位置: HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers
  3. 新建一个字符串值,命名为”c:\windows\system32\cmd.exe”,然后右键–修改 – 数值数据写入“RUNASADMIN”,确定 !

删除文件却没有释放空间

因为有程序正在占用这个文件, 只要 kill 掉这个程序, 就能释放空间了

或者使用cat /dev/null > bigfile指令先清除文件内容,然后再 rm 删除文件

从浏览器输入 URL 到页面展示过程发生了什么?

  1. 浏览器构建完整的的 url,例如输入:lujinkai.cn,浏览器会自动补全为 http://lujinkai.cn,然后浏览器进程将完整的 url 通过 ipc 发送给网络进程
  2. 网络进程接收到 url 后,对域名进行 dns 解析,得到 ip
  3. 有了 ip,检查路由是否可达
  4. 通过 arp 得到 ip 对应的 mac 地址
  5. 发起三次握手,建立连接,进行通信

参考:https://www.liaoxuefeng.com/wiki/1022910821149312/1100400176397024

参考:http://mozilla.github.io/nunjucks/cn/templating.html

Nunjucks 是 Mozilla 开发的一个纯 JavaScript 编写的模板引擎,既可以用在 Node 环境下,又可以运行在浏览器端。但是,主要还是运行在 Node 环境下,因为浏览器端有更好的模板解决方案,例如 MVVM 框架。

先定义一个基本的网页框架base.html

1
2
3
4
5
<html><body>
{% block header %} <h3>Unnamed</h3> {% endblock %}
{% block body %} <div>No body</div> {% endblock %}
{% block footer %} <div>copyright</div> {% endblock %}
</body>

base.html定义了三个可编辑的块,分别命名为headerbodyfooter子模板可以有选择地对块进行重新定义

1
2
3
4
5
{% extends 'base.html' %}

{% block header %}<h1>{{ header }}</h1>{% endblock %}

{% block body %}<p>{{ body }}</p>{% endblock %}

然后,我们对子模板进行渲染:

1
2
3
4
5
6
console.log(
env.render("extend.html", {
header: "Hello",
body: "bla bla bla...",
})
);

输出 HTML 如下:

1
2
3
4
5
<html><body>
<h1>Hello</h1>
<p>bla bla bla...</p>
<div>copyright</div> <-- footer没有重定义,所以仍使用父模板的内容
</body>

列举包可使用版本:

1
npm view element-ui versions

升级到最新版本:

1
npm update element-ui

升级到特定版本:

1
npm update element-ui@1.4.1

大端序和小端序:

  • 小端序:高位序存于高地址,更符号机器的阅读顺序
  • 大端序:高位存于低地址,人类的阅读顺序

总览

本文为 GDB 调试指南,参考 GDB 调试手册,但加入了很多实例,目前已有的篇目:

启动调试

前言

GDB(GNU Debugger)是 UNIX 及 UNIX-like 下的强大调试工具,可以调试 ada, c, c++, asm, minimal, d, fortran, objective-c, go, java,pascal 等语言。本文以 C 程序为例,介绍 GDB 启动调试的多种方式。

哪类程序可被调试

对于 C 程序来说,需要在编译时加上-g 参数,保留调试信息,否则不能使用 GDB 进行调试。

但如果不是自己编译的程序,并不知道是否带有-g 参数,如何判断一个文件是否带有调试信息呢?

gdb 文件

例如:

1
2
$ gdb helloworld
Reading symbols from helloWorld...(no debugging symbols found)...done.

如果没有调试信息,会提示 no debugging symbols found。
如果是下面的提示:

1
Reading symbols from helloWorld...done.

则可以进行调试。

readelf 查看段信息

例如:

1
2
3
4
5
6
$ readelf -S helloWorld|grep debug
[28] .debug_aranges PROGBITS 0000000000000000 0000106d
[29] .debug_info PROGBITS 0000000000000000 0000109d
[30] .debug_abbrev PROGBITS 0000000000000000 0000115b
[31] .debug_line PROGBITS 0000000000000000 000011b9
[32] .debug_str PROGBITS 0000000000000000 000011fc

helloWorld 为文件名,如果没有任何 debug 信息,则不能被调试。

file 查看 strip 状况

下面的情况也是不可调试的:

1
2
file helloWorld
helloWorld: (省略前面内容) stripped

如果最后是 stripped,则说明该文件的符号表信息和调试信息已被去除,不能使用 gdb 调试。但是 not stripped 的情况并不能说明能够被调试。

调试方式运行程序

程序还未启动时,可有多种方式启动调试。

调试启动无参程序

例如:

1
2
$ gdb helloWorld
(gdb)

输入 run 命令,即可运行程序

调试启动带参程序

假设有以下程序,启动时需要带参数:

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
int main(int argc,char *argv[])
{
if(1 >= argc)
{
printf("usage:hello name\n");
return 0;
}
printf("Hello World %s!\n",argv[1]);
return 0 ;
}

编译:

1
gcc -g -o hello hello.c

这种情况如何启动调试呢?需要设置参数:

1
2
3
4
5
6
$ gdb hello
(gdb)run 编程珠玑
Starting program: /home/shouwang/workspaces/c/hello 编程珠玑
Hello World 编程珠玑!
[Inferior 1 (process 20084) exited normally]
(gdb)

只需要 run 的时候带上参数即可。
或者使用 set args,然后在用 run 启动:

1
2
3
4
5
6
7
gdb hello
(gdb) set args 编程珠玑
(gdb) run
Starting program: /home/hyb/workspaces/c/hello 编程珠玑
Hello World 编程珠玑!
[Inferior 1 (process 20201) exited normally]
(gdb)

调试 core 文件

当程序 core dump 时,可能会产生 core 文件,它能够很大程序帮助我们定位问题。但前提是系统没有限制 core 文件的产生。可以使用命令 limit -c 查看:

1
2
$ ulimit -c
0

如果结果是 0,那么恭喜你,即便程序 core dump 了也不会有 core 文件留下。我们需要让 core 文件能够产生:

1
2
ulimit -c unlimied  #表示不限制core文件大小
ulimit -c 10 #设置最大大小,单位为块,一块默认为512字节

上面两种方式可选其一。第一种无限制,第二种指定最大产生的大小。
调试 core 文件也很简单:

1
gdb 程序文件名 core文件名

具体可参看《linux 常用命令-开发调试篇》gdb 部分。

调试已运行程序

如果程序已经运行了怎么办呢?
首先使用 ps 命令找到进程 id:

1
ps -ef|grep 进程名

attach 方式

假设获取到进程 id 为 20829,则可用下面的方式调试进程:

1
2
$ gdb
(gdb) attach 20829

接下来就可以继续你的调试啦。

可能会有下面的错误提示:

1
2
3
4
Could not attach to process.  If your uid matches the uid of the target
process, check the setting of /proc/sys/kernel/yama/ptrace_scope, or try
again as the root user. For more details, see /etc/sysctl.d/10-ptrace.conf
ptrace: Operation not permitted.

解决方法,切换到 root 用户:
将/etc/sysctl.d/10-ptrace.conf 中的

1
kernel.yama.ptrace_scope = 1

修改为

1
kernel.yama.ptrace_scope = 0

直接调试相关 id 进程

还可以是用这样的方式 gdb program pid,例如:

1
gdb hello 20829

或者:

1
gdb hello --pid 20829

已运行程序没有调试信息

为了节省磁盘空间,已经运行的程序通常没有调试信息。但如果又不能停止当前程序重新启动调试,那怎么办呢?还有办法,那就是同样的代码,再编译出一个带调试信息的版本。然后使用和前面提到的方式操作。对于 attach 方式,在 attach 之前,使用 file 命令即可:

1
2
3
4
$ gdb
(gdb) file hello
Reading symbols from hello...done.
(gdb)attach 20829

总结

本文主要介绍了两种类型的 GDB 启动调试方式,分别是调试未运行的程序和已经运行的程序。对于什么样的程序能够进行调试也进行了简单说明。

断点设置

前言

上篇《GDB 调试指南-启动调试》我们讲到了 GDB 启动调试的多种方式,分别应用于多种场景。今天我们来介绍一下断点设置的多种方式。

为何要设置断点

在介绍之前,我们首先需要了解,为什么需要设置断点。我们在指定位置设置断点之后,程序运行到该位置将会“暂停”,这个时候我们就可以对程序进行更多的操作,比如查看变量内容,堆栈情况等等,以帮助我们调试程序。

查看已设置的断点

在学习断点设置之前,我们可以使用 info breakpoints 查看已设置断点:

1
2
3
4
5
6
7
info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000004005fc in printNum2 at test.c:17
breakpoint already hit 1 time
2 hw watchpoint keep y a
breakpoint already hit 1 time
ignore next 3 hits

它将会列出所有已设置的断点,每一个断点都有一个标号,用来代表这个断点。例如,第 2 个断点设置是一个观察点,并且会忽略三次。

断点设置

断点设置有多种方式,分别应用于不同的场景。借助示例程序进行一一介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//test.c
#include<stdio.h>
void printNum(int a)
{
printf("printNum\n");
while(a > 0)
{
printf("%d\n",a);
a--;
}
}
void printNum2(int a,int num)
{
printf("printNum\n");
while(a > num && a>0)
{
printf("%d\n",a);
a--;
}
}
int div(int a,int b)
{
printf("a=%d,b=%d\n",a,b);
int temp = a/b;
return temp;
}
int main(int argc,char *argv[])
{
printNum2(12,5);
printNum(10);
div(10,0);
return 0;
}

编译:

1
gcc -g -o test test.c

注意,编译时需要带上-g 参数,具体原因参见《GDB 调试指南-启动调试》。

根据行号设置断点

1
b 9  #break 可简写为b

或者

1
b test.c:9

程序运行到第 9 行的时候会断住。

根据函数名设置断点

同样可以将断点设置在函数处:

1
b printNum

程序在调用到 printNum 函数的时候会断住。

根据条件设置断点

假设程序某处发生崩溃,而崩溃的原因怀疑是某个地方出现了非期望的值,那么你就可以在这里断点观察,当出现该非法值时,程序断住。这个时候我们可以借助 gdb 来设置条件断点,例如:

1
break test.c:23 if b==0

当在 b 等于 0 时,程序将会在第 23 行断住。
它和 condition 有着类似的作用,假设上面的断点号为 1,那么:

1
condition 1 b==0

会使得 b 等于 0 时,产生断点 1。而实际上可以很方便地用来改变断点产生的条件,例如,之前设置 b==0 时产生该断点,那么使用 condition 可以修改断点产生的条件。

根据规则设置断点

例如需要对所有调用 printNum 函数都设置断点,可以使用下面的方式:

1
rbreak printNum*

所有以 printNum 开头的函数都设置了断点。而下面是对所有函数设置断点:

1
2
3
4
#用法:rbreak file:regex
rbreak .
rbreak test.c:. #对test.c中的所有函数设置断点
rbreak test.c:^print #对以print开头的函数设置断点

设置临时断点

假设某处的断点只想生效一次,那么可以设置临时断点,这样断点后面就不复存在了:

1
tbreak test.c:l0  #在第10行设置临时断点

跳过多次设置断点

假如有某个地方,我们知道可能出错,但是前面 30 次都没有问题,虽然在该处设置了断点,但是想跳过前面 30 次,可以使用下面的方式:

1
ignore 1 30

其中,1 是你要忽略的断点号,可以通过前面的方式查找到,30 是需要跳过的次数。这样设置之后,会跳过前面 30 次。再次通过 info breakpoints 可以看到:

1
2
3
Num     Type           Disp Enb Address            What
1 breakpoint keep y 0x00000000004005e8 in printNum2 at test.c:16
ignore next 30 hits

根据表达式值变化产生断点

有时候我们需要观察某个值或表达式,知道它什么时候发生变化了,这个时候我们可以借助 watch 命令。例如:

1
watch a

这个时候,让程序继续运行,如果 a 的值发生变化,则会打印相关内容,如:

1
2
3
Hardware watchpoint 2: a
Old value = 12
New value = 11

但是这里要特别注意的是,程序必须运行起来,否则会出现:

1
No symbol "a" in current context.

因为程序没有运行,当前上下文也就没有相关变量信息。

rwatch 和 awatch 同样可以设置观察点前者是当变量值被读时断住,后者是被读或者被改写时断住。

禁用或启动断点

有些断点暂时不想使用,但又不想删除,可以暂时禁用或启用。例如:

1
2
3
4
5
disable  #禁用所有断点
disable bnum #禁用标号为bnum的断点
enable #启用所有断点
enable bnum #启用标号为bnum的断点
enable delete bnum #启动标号为bnum的断点,并且在此之后删除该断点

断点清除

断点清除主要用到 clear 和 delete 命令。常见使用如下:

1
2
3
4
5
6
7
clear   #删除当前行所有breakpoints
clear function #删除函数名为function处的断点
clear filename:function #删除文件filename中函数function处的断点
clear lineNum #删除行号为lineNum处的断点
clear f:lename:lineNum #删除文件filename中行号为lineNum处的断点
delete #删除所有breakpoints,watchpoints和catchpoints
delete bnum #删除断点号为bnum的断点

总结

本文介绍了常见的断点设置方法,断点设置之后,可以便于我们后期观察变量,堆栈等信息,为进一步的定位与调试做准备。

变量查看

前言

在启动调试以及设置断点之后,就到了我们非常关键的一步-查看变量。GDB 调试最大的目的之一就是走查代码,查看运行结果是否符合预期。既然如此,我们就不得不了解一些查看各种类型变量的方法,以帮助我们进一步定位问题。

准备工作

在查看变量之前,需要先启动调试并设置断点,该部分内容可参考《GDB 调试指南-启动调试》和《GDB 调试指南-断点设置》。后面的内容都基于在某个位置已经断住。

本文辅助说明程序如下:
testGdb.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//testGdb.c
#include<stdio.h>
#include<stdlib.h>
#include"testGdb.h"
int main(void)
{
int a = 10; //整型
int b[] = {1,2,3,5}; //数组
char c[] = "hello,shouwang";//字符数组
/*申请内存,失败时退出*/
int *d = (int*)malloc(a*sizeof(int));
if(NULL == d)
{
printf("malloc error\n");
return -1;
}
/*赋值*/
for(int i=0; i < 10;i++)
{
d[i] = i;
}
free(d);
d = NULL;
float e = 8.5f;
return 0;
}

testGdb.h

1
int a = 11;

编译:

1
$ gcc -g -o testGdb testGdb.o

普通变量查看

打印基本类型变量,数组,字符数组

最常见的使用便是使用 print(可简写为 p)打印变量内容。
例如,打印基本类型,数组,字符数组等直接使用 p 变量名即可:

1
2
3
4
5
6
7
(gdb) p a
$1 = 10
(gdb) p b
$2 = {1, 2, 3, 5}
(gdb) p c
$3 = "hello,shouwang"
(gdb)

当然有时候,多个函数或者多个文件会有同一个变量名,这个时候可以在前面加上函数名或者文件名来区分:

1
2
3
4
5
(gdb) p 'testGdb.h'::a
$1 = 11
(gdb) p 'main'::b
$2 = {1, 2, 3, 5}
(gdb)

这里所打印的 a 值是我们定义在 testGdb.h 文件里的,而 b 值是 main 函数中的 b。

打印指针指向内容

如果还是使用上面的方式打印指针指向的内容,那么打印出来的只是指针地址而已,例如:

1
2
3
(gdb) p d
$1 = (int *) 0x602010
(gdb)

而如果想要打印指针指向的内容,需要解引用:

1
2
3
4
5
(gdb) p *d
$2 = 0
(gdb) p *d@10
$3 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
(gdb)

从上面可以看到,仅仅使用*只能打印第一个值,如果要打印多个值,后面跟上@并加上要打印的长度。
或者@后面跟上变量值:

1
2
3
(gdb) p *d@a
$2 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
(gdb)

由于 a 的值为 10,并且是作为整型指针数据长度,因此后面可以直接跟着 a,也可以打印出所有内容。

另外值得一提的是,$可表示上一个变量,而假设此时有一个链表 linkNode,它有 next 成员代表下一个节点,则可使用下面方式不断打印链表内容:

1
2
3
4
(gdb) p *linkNode
(这里显示linkNode节点内容)
(gdb) p *$.next
(这里显示linkNode节点下一个节点的内容)

如果想要查看前面数组的内容,你可以将下标一个一个累加,还可以定义一个类似 UNIX 环境变量,例如:

1
2
3
4
5
6
7
(gdb) set $index=0
(gdb) p b[$index++]
$11 = 1
(gdb) p b[$index++]
$12 = 2
(gdb) p b[$index++]
$13 = 3

这样就不需要每次修改下标去打印啦。

按照特定格式打印变量

对于简单的数据,print 默认的打印方式已经足够了,它会根据变量类型的格式打印出来,但是有时候这还不够,我们需要更多的格式控制。常见格式控制字符如下:

  • x 按十六进制格式显示变量。
  • d 按十进制格式显示变量。
  • u 按十六进制格式显示无符号整型。
  • o 按八进制格式显示变量。
  • t 按二进制格式显示变量。
  • a 按十六进制格式显示变量。
  • c 按字符格式显示变量。
  • f 按浮点数格式显示变量。

还是以辅助程序来说明,正常方式打印字符数组 c:

1
2
(gdb) p c
$18 = "hello,shouwang"

但是如果我们要查看它的十六进制格式打印呢?

1
2
3
4
(gdb) p/x c
$19 = {0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x73, 0x68, 0x6f, 0x75, 0x77, 0x61,
0x6e, 0x67, 0x0}
(gdb)

但是如果我们想用这种方式查看浮点数的二进制格式是怎样的是不行的,因为直接打印它首先会被转换成整型,因此最终会得到 8:

1
2
3
4
5
(gdb) p e
$1 = 8.5
(gdb) p/t e
$2 = 1000
(gdb)

那么就需要另外一种查看方式了。

查看内存内容

examine(简写为 x)可以用来查看内存地址中的值。语法如下:

1
x/[n][f][u] addr

其中:

  • n 表示要显示的内存单元数,默认值为 1
  • f 表示要打印的格式,前面已经提到了格式控制字符
  • u 要打印的单元长度
  • addr 内存地址

单元类型常见有如下:

  • b 字节
  • h 半字,即双字节
  • w 字,即四字节
  • g 八字节

我们通过一个实例来看,假如我们要把 float 变量 e 按照二进制方式打印,并且打印单位是一字节:

1
2
3
(gdb) x/4tb &e
0x7fffffffdbd4: 00000000 00000000 00001000 01000001
(gdb)

可以看到,变量 e 的四个字节都以二进制的方式打印出来了。

自动显示变量内容

假设我们希望程序断住时,就显示某个变量的值,可以使用 display 命令。

1
2
(gdb) display e
1: e = 8.5

那么每次程序断住时,就会打印 e 的值。要查看哪些变量被设置了 display,可以使用:

1
2
3
4
5
(gdb)info display
Auto-display expressions now in effect:
Num Enb Expression
1: y b
2: y e

如果想要清除可以使用

1
delete display num #num为前面变量前的编号,不带num时清除所有。

或者去使能:

1
disable display num  #num为前面变量前的编号,不带num时去使能所有

查看寄存器内容

1
2
3
4
5
6
7
8
9
(gdb)info registers
rax 0x0 0
rbx 0x0 0
rcx 0x7ffff7dd1b00 140737351850752
rdx 0x0 0
rsi 0x7ffff7dd1b30 140737351850800
rdi 0xffffffff 4294967295
rbp 0x7fffffffdc10 0x7fffffffdc10
(内容过多未显示完全)

总结

通过不同方式查看变量值或者内存值能够极大的帮助我们判断程序的运行是否符合我们的预期,如果发现观察的值不是我们预期的时候,就需要检查我们的代码了。

单步调试

前言

前面通过《启动调试》,《断点设置》,《变量查看》,我们已经了解了 GDB 基本的启动,设置断点,查看变量等,如果这些内容你还不知道,建议先回顾一下前面的内容。在启动调试设置断点观察之后,没有我们想要的信息怎么办呢?这个时候,就需要单步执行或者跳过当前断点继续执行等等。而本文所说的单步调试并非仅仅指单步执行,而是指在你的控制之下,按要求执行语句。

准备

老规矩,先准备一个示例程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*gdbStep.c*/
#include<stdio.h>
/*计算简单乘法,这里没有考虑溢出*/
int add(int a, int b)
{
int c = a + b;
return c;
}
/*打印从0到num-1的数*/
int count(int num)
{
int i = 0;
if(0 > num)
return 0;
while(i < num)
{
printf("%d\n",i);
i++;
}
return i;
}
int main(void)
{
int a = 3;
int b = 7;
printf("it will calc a + b\n");
int c = add(a,b);
printf("%d + %d = %d\n",a,b,c);
count(c);
return 0;
}

编译:

1
gcc -g -o gdbStep gdbStep.c

程序的功能比较简单,这里不多做解释。

特别简单说明一条命令,list(可简写为 l),它可以将源码列出来,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(gdb) list
1 #include<stdio.h>
2
3 /*计算简单乘法,这里没有考虑溢出*/
4 int add(int a, int b)
5 {
6 int c = a * b;
7 return c;
8 }
9 int main(void)
10 {
(gdb) l
11 int a = 13;
12 int b = 57;
13 printf("it will calc a * b\n");
14 int c = add(a,b);
15 printf("%d*%d = %d\n",a,b,c);
16 return 0;
17 }
(gdb)

单步执行-next

next 命令(可简写为 n)用于在程序断住后,继续执行下一条语句,假设已经启动调试,并在第 12 行停住,如果要继续执行,则使用 n 执行下一条语句,如果后面跟上数字 num,则表示执行该命令 num 次,就达到继续执行 n 行的效果了:

1
2
3
4
5
6
7
8
9
10
11
$ gdb gdbStep   #启动调试
(gdb)b 25 #将断点设置在12行
(gdb)run #运行程序
Breakpoint 1, main () at gdbStep.c:25
25 int b = 7;
(gdb) n #单步执行
26 printf("it will calc a + b\n");
(gdb) n 2 #执行两次
it will calc a + b
28 printf("%d + %d = %d\n",a,b,c);
(gdb)

从上面的执行结果可以看到,我们在 25 行处断住,执行 n 之后,运行到 26 行,运行 n 2 之后,运行到 28 行,但是有没有发现一个问题,为什么不会进入到 add 函数内部呢?那就需要用到另外一个命令啦。

单步进入-step

对于上面的情况,如果我们想跟踪 add 函数内部的情况,可以使用 step 命令(可简写为 s),它可以单步跟踪到函数内部,但前提是该函数有调试信息并且有源码信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ gdb gdbStep    #启动调试
(gdb) b 25 #在12行设置断点
Breakpoint 1 at 0x4005d3: file gdbStep.c, line 25.
(gdb) run #运行程序
Breakpoint 1, main () at gdbStep.c:25
25 int b = 7;
(gdb) s
26 printf("it will calc a + b\n");
(gdb) s #单步进入,但是并没有该函数的源文件信息
_IO_puts (str=0x4006b8 "it will calc a + b") at ioputs.c:33
33 ioputs.c: No such file or directory.
(gdb) finish #继续完成该函数调用
Run till exit from #0 _IO_puts (str=0x4006b8 "it will calc a + b")
at ioputs.c:33
it will calc a + b
main () at gdbStep.c:27
27 int c = add(a,b);
Value returned is $1 = 19
(gdb) s #单步进入,现在已经进入到了add函数内部
add (a=13, b=57) at gdbStep.c:6
6 int c = a + b;

从上面的过程可以看到,s 命令会尝试进入函数,但是如果没有该函数源码,需要跳过该函数执行,可使用 finish 命令,继续后面的执行。如果没有函数调用,s 的作用与 n 的作用并无差别,仅仅是继续执行下一行。它后面也可以跟数字,表明要执行的次数。

当然它还有一个选项,用来设置当遇到没有调试信息的函数,s 命令是否跳过该函数,而执行后面的。默认情况下,它是会跳过的,即 step-mode 值是 off:

1
2
3
4
(gdb) show step-mode
Mode of the step operation is off.
(gdb) set step-mode on
(gdb) set step-mode off

还有一个与 step 相关的命令是 stepi(可简写为 si),它与 step 不同的是,每次执行一条机器指令:

1
2
3
4
5
6
(gdb) si
0x0000000000400573 6 int c = a + b;
(gdb) display/i $pc
1: x/i $pc
=> 0x400573 <add+13>: mov -0x18(%rbp),%eax
(gdb)

继续执行到下一个断点-continue

我们可能打了多处断点,或者断点打在循环内,这个时候,想跳过这个断点,甚至跳过多次断点继续执行该怎么做呢?可以使用 continue 命令(可简写为 c)或者 fg,它会继续执行程序,直到再次遇到断点处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ gdb gdbStep
(gdb)b 18 #在count函数循环内打断点
(gdb)run
Breakpoint 1, count (num=10) at gdbStep.c:18
18 i++;
(gdb) c #继续运行,直到下一次断住
Continuing.
1

Breakpoint 1, count (num=10) at gdbStep.c:18
18 i++;
(gdb) fg #继续运行,直到下一次断住
Continuing.
2

Breakpoint 1, count (num=10) at gdbStep.c:18
18 i++;
(gdb) c 3 #跳过三次
Will ignore next 2 crossings of breakpoint 1. Continuing.
3
4
5

Breakpoint 1, count (num=10) at gdbStep.c:18
18 i++;

继续运行到指定位置-until

假如我们在 25 行停住了,现在想要运行到 29 行停住,就可以使用 until 命令(可简写为 u):

1
2
3
4
5
6
7
8
9
$ gdb gdbStep
(gdb)b 25
(gdb)run
(gdb) u 29
it will calc a + b
3 + 7 = 10
main () at gdbStep.c:29
29 count(c);
(gdb)

可以看到,在执行 u 29 之后,它在 29 行停住了。它利用的是临时断点。

跳过执行—skip

skip 可以在 step 时跳过一些不想关注的函数或者某个文件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ gdb gdbStep
(gdb) b 27
Breakpoint 1 at 0x4005e4: file gdbStep.c, line 27.
(gdb) skip function add #step时跳过add函数
Function add will be skipped when stepping.
(gdb) info skip #查看step情况
Num Type Enb What
1 function y add
(gdb) run
Starting program: /home/hyb/workspaces/gdb/gdbStep
it will calc a + b

Breakpoint 1, main () at gdbStep.c:27
27 int c = add(a,b);
(gdb) s
28 printf("%d + %d = %d\n",a,b,c);
(gdb)

可以看到,再使用 skip 之后,使用 step 将不会进入 add 函数。
step 也后面也可以跟文件:

1
(gdb)skip file gdbStep.c

这样 gdbStep.c 中的函数都不会进入。

其他相关命令:

  • skip delete [num] 删除 skip
  • skip enable [num] 使能 skip
  • skip disable [num] 去使能 skip

其中 num 是前面通过 info skip 看到的 num 值,上面可以带或不带该值,如果不带 num,则针对所有 skip,如果带上了,则只针对某一个 skip。

总结

本文主要介绍了一些简单情况的单步调试方法或常见命令使用,但这些已经够用了,毕竟大部分程序的执行或停止都在我们的掌控之中了。

源码查看

前言

我们在调试过程中难免要对照源码进行查看,如果已经开始了调试,而查看源码或者编辑源码却要另外打开一个窗口,那未免显得太麻烦。文本将会介绍如何在 GDB 调试模式下查看源码或对源码进行编辑

准备工作

为了说明后面的内容,我们先准备一些源码,分别是 main.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//main.c
#include<stdio.h>
#include"test.h"
int main(void)
{
printf("it will print from 5 to 1\n");
printNum(5);
printf("print end\n");

printf("it will print 1 to 5\n");
printNum1(5);
printf("print end\n");
return 0;
}

头文件 test.h:

1
2
3
4
5
6
#ifndef _TEST_H
#define _TEST_H
#include<stdio.h>
void printNum(int n);
void printNum1(int n);
#endif

以及 test.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include"test.h"
void printNum(int n)
{
if( n < 0)
return;
while(n > 0)
{
printf("%d\n",n);
n--;
}
}

void printNum1(int n)
{
if( n < 0)
return;
int i = 1;
while(i <= n)
{
printf("%d\n",i);
i++;
}
}

编译运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ gcc -g  -o main  main.c test.c
$ chmod +x main
$ ./main
it will print from 5 to 1
5
4
3
2
1
print end
it will print 1 to 5
1
2
3
4
5
print end

程序功能比较简单,用来打印 5 到 1 的数以及 1 到 5 的数,这里也就不多做解释。

列出源码

首先要介绍的就是 list 命令(可简写为 l),它用来打印源码。

直接打印源码

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ gdb main
(gdb) l
1 //main.c
2 #include<stdio.h>
3 #include"test.h"
4 int main(void)
5 {
6 printf("it will print from 5 to 1\n");
7 printNum(5);
8 printf("print end\n");
9
10 printf("it will print 1 to 5\n");
(gdb)

直接输入 l 可从第一行开始显示源码,继续输入 l,可列出后面的源码。后面也可以跟上+或者-,分别表示要列出上一次列出源码的后面部分或者前面部分。

列出指定行附近源码

l 后面可以跟行号,表明要列出附近的源码:

1
2
3
4
5
6
7
8
9
10
11
(gdb) l 9
4 int main(void)
5 {
6 printf("it will print from 5 to 1\n");
7 printNum(5);
8 printf("print end\n");
9
10 printf("it will print 1 to 5\n");
11 printNum1(5);
12 printf("print end\n");
13 return 0;

在这里,l 后面跟上 9,表明要列出第 9 行附近的源码。

列出指定函数附近的源码

这个很容易理解,而使用也很简单,l 后面跟函数名即可,例如:

1
2
3
4
5
6
7
8
9
10
11
(gdb) l printNum
1 #include"test.h"
2 void printNum(int n)
3 {
4 if( n < 0)
5 return;
6 while(n > 0)
7 {
8 printf("%d\n",n);
9 n--;
10 }

在这里,l 后面跟上函数名 printNum,它便列出了 printNum 函数附近的源码。

设置源码一次列出行数

不知道你有没有发现,在列出函数源码的时候,它并没有列全,因为 l 每次只显示 10 行,那么有没有方法每次列出更多呢?
我们可以通过 listsize 属性来设置,例如设置每次列出 20 行:

1
2
3
(gdb) set listsize 20
(gdb) show listsize
Number of source lines gdb will list by default is 20.

这样每次就会列出 20 行,当然也可以设置为 0 或者 unlimited,这样设置之后,列出就没有限制了,但源码如果较长,查看将会不便。

列出指定行之间的源码

list first,last
例如,要列出 3 到 15 行之间的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) l 3,15
3 {
4 if( n < 0)
5 return;
6 while(n > 0)
7 {
8 printf("%d\n",n);
9 n--;
10 }
11 }
12
13 void printNum1(int n)
14 {
15 if( n < 0)

启始行和结束行号之间用逗号隔开。两者之一也可以省略,例如:

1
2
3
4
5
6
7
8
9
10
11
(gdb) list 3,
3 {
4 if( n < 0)
5 return;
6 while(n > 0)
7 {
8 printf("%d\n",n);
9 n--;
10 }
11 }
12

省略结束行的时候,它列出从开始行开始,到指定大小行结束,而省略开始行的时候,到结束行结束,列出设置的大小行,例如默认设置为 10 行,则到结束行为止,总共列出 10 行。前面我们也介绍了修改和查看默认列出源码行数的方法。

列出指定文件的源码

前面执行 l 命令时,默认列出 main.c 的源码,如果想要看指定文件的源码呢?可以

1
l location

其中 location 可以是文件名加行号或函数名,因此可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) l test.c:1
1 #include"test.h"
2 void printNum(int n)
3 {
4 if( n < 0)
5 return;
6 while(n > 0)
7 {
8 printf("%d\n",n);
9 n--;
10 }
(gdb)

来查看指定文件指定行,或者指定文件指定函数:

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) l test.c:printNum1
9 n--;
10 }
11 }
12
13 void printNum1(int n)
14 {
15 if( n < 0)
16 return;
17 int i = 1;
18 while(i <= n)
(gdb)

或者指定文件指定行之间:

1
2
3
4
5
(gdb) l test.c:1,test.c:3
1 #include"test.h"
2 void printNum(int n)
3 {
(gdb)

指定源码路径

在查看源码之前,首先要确保我们的程序能够关联到源码,一般来说,我们在自己的机器上加上-g 参数编译完之后,使用 gdb 都能查看到源码,但是如果出现下面的情况呢?

源码被移走

例如,我现在将 main.c 移动到当前的 temp 目录下,再执行 l 命令:

1
2
3
(gdb) l
1 main.c: No such file or directory.
(gdb)

它就会提示找不到源码文件了,那么怎么办呢?
我们可以使用 dir 命名指定源码路径,例如:

1
2
(gdb) dir ./temp
Source directories searched: /home/hyb/workspaces/gdb/sourceCode/./temp:$cdir:$cwd

这个时候它就能找到源码路径了。我这里使用的是相对路径,保险起见,你也可以使用绝对路径。

更换源码目录

例如,你编译好的程序文件,放到了另外一台机器上进行调试,或者你的源码文件全都移动到了另外一个目录,怎么办呢?当然你还可以使用前面的方法添加源码搜索路径,也可以使用 set substitute-path from to 将原来的路径替换为新的路径,那么我们如何知道原来的源码路径是什么呢?借助 readelf 命令可以知道:

1
2
3
4
5
6
$ readelf main -p .debug_str
[ 0] long unsigned int
[ 12] short int
[ 1c] /home/hyb/workspaces/gdb/sourceCode
[ 40] main.c
(显示部分内容)

main 为你将要调试的程序名,这里我们可以看到原来的路径,那么我们现在替换掉它:

1
2
3
4
5
(gdb) set substitute-path /home/hyb/workspaces/gdb/sourceCode /home/hyb/workspaces/gdb/sourceCode/temp
(gdb) show substitute-path
List of all source path substitution rules:
`/home/hyb/workspaces/gdb/sourceCode' -> `/home/hyb/workspaces/gdb/sourceCode/temp'.
(gdb)

设置完成后,可以通过 show substitute-path 来查看设置结果。这样它也能在正确的路径查找源码啦。

需要注意的是,这里对路径做了字符串替换,那么如果你有多个路径,可以做多个替换。甚至可以对指定文件路径进行替换。

最后你也可以通过 unset substitute-path [path]取消替换。

编辑源码

为了避免已经启动了调试之后,需要编辑源码,又不想退出,可以直接在 gdb 模式下编辑源码,它默认使用的编辑器是/bin/ex,但是你的机器上可能没有这个编辑器,或者你想使用自己熟悉的编辑器,那么可以通过下面的方式进行设置:

1
2
$ EDITOR=/usr/bin/vim
$ export EDITOR

/usr/bin/vim 可以替换为你熟悉的编辑器的路径,如果你不知道你的编辑器在什么位置,可借助 whereis 命令或者 witch 命令查看:

1
2
3
4
$ whereis vim
vim: /usr/bin/vim /usr/bin/vim.tiny /usr/bin/vim.basic /usr/bin/vim.gnome /etc/vim /usr/share/vim /usr/share/man/man1/vim.1.gz
$ which vim
/usr/bin/vim

设置之后,就可以在 gdb 调试模式下进行编辑源码了,使用命令 edit location,例如:

1
2
3
(gdb)edit 3  #编辑第三行
(gdb)edit printNum #编辑printNum函数
(gdb)edit test.c:5 #编辑test.c第五行

可自行尝试,这里的 location 和前面介绍的一样,可以跟指定文件的特定行或指定文件的指定函数。
编辑完保存后,别忘了重新编译程序:

1
(gdb)shell gcc -g -o main main.c test.c

这里要注意,为了在 gdb 调试模式下执行 shell 命令,需要在命令之前加上 shell,表明这是一条 shell 命令。这样就能在不用退出 GDB 调试模式的情况下编译程序了。

另外一种模式

启动时,带上 tui(Text User Interface)参数,会有意想不到的效果,它会将调试在多个文本窗口呈现:

1
gdb main -tui

但是本文不作介绍,有兴趣的可以探索一下。

相关文档:

nftables 和 iptables 一样,依赖的底层机制都是 Netfilter,但是 nftables 各个方面都比 iptables 更优秀,未来一定会成为主流,所以有必要好好学习。

升级 nftables

CentOS8 自带的 nftables 版本是 0.9.3,而官网最新的版本是 0.9.6,所以需要先升级:

  1. 卸载系统自带的 nftables

    1
    2
    3
    [root@centos8 src]$yum remove nftables.x86_64
    [root@centos8 src]$rpm -qa | grep ntf
    [root@centos8 src]$
  2. 去官网下载最新版本

    1
    2
    3
    4
    5
    6
    7
    8
    # 官网:http://netfilter.org/projects/nftables/downloads.html
    [root@centos8 ~]$cd /usr/local/src/
    [root@centos8 src]$yum -y install bzip2
    [root@centos8 src]$tar jxvf nftables-0.9.6.tar.bz2
    [root@centos8 src]$ll
    total 844
    drwxr-xr-x. 10 lujinkai lujinkai 4096 Jun 15 16:24 nftables-0.9.6
    -rw-r--r--. 1 root root 859481 Sep 27 06:49 nftables-0.9.6.tar.bz2
  3. 安装

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    [root@centos8 src]$cd nftables-0.9.6/
    [root@centos8 nftables-0.9.6]$
    [root@centos8 nftables-0.9.6]$mkdir /usr/local/nftables
    [root@centos8 src]$yum -y install gcc
    [root@centos8 nftables-0.9.6]$./configure --prefix=/usr/local/nftables
    ...
    # 报错,缺少libmnl依赖
    checking for LIBMNL... no
    configure: error: Package requirements (libmnl >= 1.0.4) were not met:
    ....
    # 安装libmnl参考:https://centos.pkgs.org/8/centos-baseos-x86_64/libmnl-1.0.4-6.el8.x86_64.rpm.html
    [root@centos8 nftables-0.9.6]$dnf install libmnl
    # 安装libmnl-devel参考:https://centos.pkgs.org/8/centos-powertools-x86_64/libmnl-devel-1.0.4-6.el8.x86_64.rpm.html
    [root@centos8 nftables-0.9.6]$dnf --enablerepo=PowerTools install libmnl-devel
    [root@centos8 nftables-0.9.6]$ldconfig
    [root@centos8 nftables-0.9.6]$./configure --prefix=/usr/local/nftables
    ...
    # 报错,缺少libnftnl依赖
    checking for LIBNFTNL... no
    configure: error: Package requirements (libnftnl >= 1.1.7) were not met:

    No package 'libnftnl' found

    Consider adjusting the PKG_CONFIG_PATH environment variable if you
    installed software in a non-standard prefix.

    Alternatively, you may set the environment variables LIBNFTNL_CFLAGS
    and LIBNFTNL_LIBS to avoid the need to call pkg-config.
    See the pkg-config man page for more details.
    ...
    # 上面的libmnl通过dnf安装,这个libnftnl就通过编译安装吧,从nftables官网下载最新的libnftnl源码包
    [root@centos8 nftables-0.9.6]$cd ..
    [root@centos8 src]$tar jxvf libnftnl-1.1.7.tar.bz2
    [root@centos8 src]$cd libnftnl-1.1.7/
    [root@centos8 libnftnl-1.1.7]$./configure --prefix=/usr/local/libnftnl-1.1.7
    [root@centos8 libnftnl-1.1.7]$make && make install
    ...
    # 根据提示,要指定libnftnl的lib和include,这个要查看具体的pkg-config文件
    # vim /usr/local/libnftnl-1.1.7/lib/pkgconfig/libnftnl.pc
    # ...
    # 15 Libs: -L${libdir} -lnftnl
    # 16 Cflags: -I${includedir}
    [root@centos8 nftables-0.9.6]$(LIBNFTNL_CFLAGS=-I/usr/local/libnftnl-1.1.7/include/ LIBNFTNL_LIBS="-L/usr/local/libnftnl-1.1.7/lib/ -lnftnl" ./configure --prefix=/usr/local/nftables)
    ...
    # 报错,缺少libgmb依赖
    checking for __gmpz_init in -lgmp... no
    configure: error: No suitable version of libgmp found
    [root@centos8 nftables-0.9.6]$yum -y install gmp gmp-devel
    # 继续
    [root@centos8 nftables-0.9.6]$./configure --prefix=/usr/local/nftables
    ...
    # 缺少 libreadline 依赖
    checking for readline in -lreadline... no
    configure: error: No suitable version of libreadline found
    [root@centos8 nftables-0.9.6]$dnf -y install readline readline-devel
    [root@centos8 nftables-0.9.6]$./configure --prefix=/usr/local/nftables
    # 安装成功
    nft configuration:
    cli support: readline
    enable debugging symbols: yes
    use mini-gmp: no
    enable man page: yes
    libxtables support: no
    json output support: no
    enable Python: no
  4. 配置 systemctl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    [root@centos8 ~]$vim /usr/lib/systemd/system/nftables.service
    # 从另一台机器上拷贝一份yum安装自动生成的nftables.service文件,稍作改动
    [Unit]
    Description=Netfilter Tables
    Documentation=man:nft(8)
    Wants=network-pre.target
    Before=network-pre.target

    [Service]
    Type=oneshot
    ProtectSystem=full
    ProtectHome=true
    ExecStart=/usr/local/nftables/sbin/nft -f /usr/local/nftables/etc/nftables/all-in-one.nft
    ExecReload=/usr/local/nftables/sbin/nft 'flush ruleset; include "/usr/local/nftables/etc/nftables/all-in-one.nft";'
    ExecStop=/usr/local/nftables/sbin/nft flush ruleset
    RemainAfterExit=yes

    [Install]
    WantedBy=multi-user.target

    # 设置开机自启
    [root@centos8 ~]$systemctl enable --now nftables.service
  5. 配置环境变量

    1
    2
    3
    4
    # 将 /usr/local/nftables/sbin 添加到PATH(位于/etc/profile)
    export PATH=/usr/local/nftables/sbin:/usr/local/nginx/sbin:$PATH
    # 重载
    . /etc/profile

nftables 和 iptables 的区别

  1. 1
  2. 2
  3. 3

nf* 和 xt*

nf* 是 netfilter 的缩写,nf_ 前缀的模块是 netfilter 的核心模块(以下称 nf 模块),nftables 之前,netfilter 提供了 iptables、ip6tables、arptables 等用户空间工具,xtables 就是这些工具的统称,缩写为 xt。可能是为了便于理解或者什么理由,专为 xtables 开发的模块以 xt*前缀命名(以下称 xt 模块),当然,xtables 也可以调用 nf 模块。

安装 iptables 后,会生成/usr/lib64/xtables/目录,目录下就是各种 xt 模块。

xtables 既能调用 xt 模块,也能调用 nf 模块,为了方便我们把 xt 模块和 nf 模块统称为扩展。

现在,这些 xtables 工具被认为是过时的,推荐使用 nftables,nftables 是 xtables 的全面替代品,nftables 调用的都是 nf 模块,所以安装后自然也不会生成 /usr/lib64/xtables/目录。

因为并没有找到参考资料,以上都是个人理解和猜测,说的不一定准确。

表(Tables)

与 iptables 类似,在 nftables 中,的容器,nftables 不包含任何的表和链,所以开始使用 nftables 时,首先需要添加至少一个表,然后向表里添加链,然后往链里添加规则。

地址簇与钩子函数

什么是地址族?
地址族:又叫地址簇,AF,Address;不要太纠结这个名称,可以认为就是 地址类型。
另外,还有协议族:又叫协议簇,PF,Protocol Family;协议族等价于地址族。

netfilter 中六个勾子函数(hook),前面 iptables 章节一节已经介绍过了,这里再说一遍,六个钩子函数是:

1
INGRESS、PREROUTING、INPUT、OUTPUT、FORWARD、POSTROUTING

添加表

语法:

1
nft {add | create} table [<family>] <table>
  • family:表支持的地址类型,有六种,每个表只能指定一种,默认是 ip,表示 IPv4

    family 表示的地址族 iptables 中对应的命令行工具
    ip IPv4 地址 iptables
    ip6 IPv6 地址 ip6tables
    inet IPv4 和 IPv6 地址 iptables 和 ip6tables
    arp 地址解析协议(ARP)地址 arptables
    bridge 处理桥接数据包 ebtables
    netdev 内核 4.2 之后可用。只能使用 ingress 钩子,在路由之前进行过滤,可以替代tc命令

范例:

1
2
3
4
# 添加一个 IPv4 的 foo 表
nft add table ip foo
# 添加一个 IPv6 的 bar 表
nft add table ip6 bar

列出表

1
2
3
4
5
6
7
8
9
10
11
# 列出所有表
[root@centos8 ~]$nft list tables
table ip foo
table ip6 bar
# 列出一个或多个地址族的所有表,注意这时是tables
[root@centos8 ~]$nft list tables ip
table ip foo
# 列出一个表中的所有链
[root@centos8 ~]$nft list table ip foo
table ip foo { # 目前表中还没链
}

删除表

1
[root@centos8 ~]$nft delete table ip6 bar

说明:内核 3.18 以前,需要先清空表中的内容,再删除表

清空表

1
2
# 清空表中所有链中的所有规则
[root@centos8 ~]$nft flush table ip foo

链(Chains)

ntfables 没有内置的链,所有的链都需要手动创建,链有两种类型:基本类型和常规类型。
其中基本类型又分类三类:filter、nat、route

  • 基本链:base chains,数据包的入口,需要指定钩子函数和优先级,类似 iptables 中的内置链

    type 钩子函数 描述
    filter all all 过滤
    nat ip、ip6、inet prerouting、input、output、postrouting 地址转换,第一个包总是命中这个链,因此不要用此链进行过滤
    route ip、ip6 output 策略路由选择器,等价于 iptables 中的 mangle 表,但是仅用于 output 钩子函数
  • 常规链:regular chains,不需要指定钩子函数和优先级,可以用来做跳转,从逻辑上对规则进行分类,类似 iptables 中的自定义类

创建链

语法:

1
2
3
4
# 创建常规链
nft add chain [<family>] <table> <chain>
# 创建基本链
nft add chain [<family>] <table> <chain> {type <type> hook <hook> [device <device>] priority <priority>; [policy <policy>;] }
  • type:可以选择 filter、nat、route

  • hook:钩子函数,六个钩子函数,只能指定一个,不同的表 family 支持不同的钩子函数:

    family hook
    ip、ip6、inet prerouting、input、forward、output、postrouting
    arp input、output
    bridge prerouting、input、forward、output、postrouting
    netdev ingress

    小总结:netdev 只支持 ingress + filter 一种组合;arp 支持 { input | output } + filter 两种组合

  • device:netdev 表只存在于每个传入接口,所以它需要指定 device 参数

  • priority:优先级,可以接受 整数值 或者 标准优先级名。整数值可以为负,数值越低优先级越高;有以下数值可以选择

    • NF_IP_PRI_CONNTRACK_DEFRAG (-400): priority of defragmentation
    • NF_IP_PRI_RAW (-300): traditional priority of the raw table placed before connection tracking operation
    • NF_IP_PRI_SELINUX_FIRST (-225): SELinux operations
    • NF_IP_PRI_CONNTRACK (-200): Connection tracking operations
    • NF_IP_PRI_MANGLE (-150): mangle operation
    • NF_IP_PRI_NAT_DST (-100): destination NAT
    • NF_IP_PRI_FILTER (0): filtering operation, the filter table
    • NF_IP_PRI_SECURITY (50): Place of security table where secmark can be set for example
    • NF_IP_PRI_NAT_SRC (100): source NAT
    • NF_IP_PRI_SELINUX_LAST (225): SELinux at packet exit
    • NF_IP_PRI_CONNTRACK_HELPER (300): connection tracking at exit
  • policy:链的策略,支持的策略值是 accept、drop,默认是 accept

范例:

1
2
3
4
# 添加常规链
[root@centos8 ~]$nft add chain ip foo chain_1
# 添加基本链
[root@centos8 ~]$nft add chain ip foo chain_2 {type filter hook input priority 0\;}

列出链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 列出一个表中的所有链
[root@centos8 ~]$nft list table ip foo
table ip foo {
chain chain_1 {
}

chain chain_2 {
type filter hook input priority filter; policy accept;
}
}
# 列出一个链中的所有规则
[root@centos8 ~]$nft list chain ip foo chain_1
table ip foo {
chain chain_1 { # 目前链中还没有规则
}
}
[root@centos8 ~]$nft list chain ip foo chain_2
table ip foo {
chain chain_2 { # 目前链中还没有规则
type filter hook input priority filter; policy accept;
}
}

编辑链

把添加链语法的 add 或者 create 关键字去掉,就是编辑链的语法

1
2
3
4
# 编辑常规链
nft chain [<family>] <table> <chain>
# 编辑基本链
nft chain [<family>] <table> <chain> {[type <type> hook <hook> [device <device>] priority <priority> ; [policy policy ;]] }

范例:

1
2
# 将chain_1链策略从accept更改为drop
nft chain ip foo chain_1 { policy drop \; }

删除链

要删除的链不能包含任何规则或者跳转目标。

范例:

1
2
3
4
5
6
7
[root@centos8 ~]$nft delete chain ip foo chain_1
[root@centos8 ~]$nft list table ip foo
table ip foo {
chain chain_2 {
type filter hook input priority filter; policy accept;
}
}

清空链

范例:

1
2
# 清空chain_2链中的所有规则
[root@centos8 ~]$nft flush chain ip foo chain_2

规则(Rules)

iptables 和 nftables 的区别

参考博客:https://blog.csdn.net/dog250/article/details/41526421?locationNum=8

iptables

iptables 是由表、链、规则组成,其中规则又由 match、target 组成。

iptables 的规则是按照配置顺序顺序匹配的,在每一张表的每一个链上依次匹配每一条规则,在每一条规则依次匹配每一个 match,全部匹配的 match 执行该规则的 target,由 target 决定:

  • 继续匹配下一条规则
  • 对数据包做一些修改
  • 跳转到其它的链(即开始从该链依次匹配该链上的每一条规则)
  • 返回引发跳转的链(即继续匹配跳转前的链的下一条规则)
  • 丢弃数据包
  • 接收数据包(即不再继续往下匹配,直接返回)
  • 记录日志

整个 iptables 框架的执行流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
循环1:
static breakrule = 0;
遍历一个chain的每一条rule {
nomatch = 0;
循环2:遍历一条rule的每一个match {
result = rule->match[curr](skb, info);
if(result != MATCH) {
nomatch = 1;
break;
}
}
if (nomatch == 1) {
continue该chain的下一条rule;
}
result = rule->target(skb, info);
if (result == DROP) {
break丢弃数据包
} else if (result == ACCEPT) {
break接受数据包
} else if (result == GOTO) {
breakrule = rule;
跳转到相应的chain,执行循环1
} else if (result == RETURN) {
break返回调用chain,执行其breakrule的下一条rule
} ...
}

可以发现,包过滤的流程最终要落实到规则匹配,而过滤的动作最终落实到了该规则的 target,前面的所有的 match 匹配返回结果就是 0 或者非 0 表示是否匹配,只有所有的 match 均匹配,才会执行 target。这也就导致了如果你想实现多个 target,就不得不写多条规则。

nftables

nftables 的规则去掉了“匹配所有的 match 之后执行一个 target”这样的限制,使一条规则真的成了“为一个数据包做一些事情”这样灵活的命令。我们来看一下 nftables 框架的执行流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
循环1:
static breakrule = 0;
遍历一个chain的每一条rule {
nomatch = 0;
reg[MAX]
循环2:遍历一条rule的每一个expression {
void rule->expression[curr]->operations->expr(skb, info, reg)
if(reg[VERDICT] != CONTINUE) {
break;
}
}
if (reg[VERDICT] == CONTINUE) {
continue该chain的下一条rule;
} else if (reg[VERDICT] == DROP) {
break丢弃数据包
} else if (reg[VERDICT] == ACCEPT) {
break接受数据包
} else if (reg[VERDICT] == GOTO) {
breakrule = rule;
跳转到相应的chain,执行循环1
} else if (reg[VERDICT] == RETURN) {
break调用chain,执行其breakrule的下一条rule
} ...
}

可以看到,nftables 没有 match 和 target,而是将一条 rule 抽象成了若干条的表达式,即 expression,所谓的表达式就是一个主语加谓词的式子,它是“可执行”的,它可以“做任何事情”,而不仅仅是计算一个匹配结果。除此之外,nftables 内置了一组寄存器,其中之一是 verdict 寄存器,它指示了“下一步要怎么做”。每一条 expression 执行完了之后,会取出该寄存器,由该寄存器的值来采取下一步的行动。这个 verdict 寄存器替换了 iptables 中 target 返回值,这就可以在一条 rule 中采取多个动作,每条动作可以解析成一个 expression,只要每一个 expression 在执行后将 verdict 寄存器设置为 CONTINUE 即可。

除了执行流程的显著区别之外,nftables 最大的意义在于它对 expression 进行了抽象,nftables 的内核框架可以注册很多种的 expression,其中 expr 回调函数执行具体的 expression 表达式。

表达式 expression

规则由表达式组成,表达式有匹配表达式,和动作表达式两种,但官方文档不是这么划分的,而是分成表达式和语句,其实表达式就是匹配表达式,语句就是动作表达式,下文如果没有特别说明,采用的是官方的说法。

例如规则:

1
ip saddr 10.1.1.1 tcp dport ssh accept

就是由ip saddtcp dpor两条表达式和accept一条语句组成

每个表达式都有一个数据类型

describe

describe 命令可以显示表达式 或 数据类型的信息

语法:

1
2
3
4
# 列出表达式的信息
describe <expression>
# 列出数据类型的信息
describe <datatype>

范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# describe <expression>
[root@centos8 ~]$nft describe ct state
ct expression, datatype ct_state (conntrack state) (basetype bitmask, integer), 32 bits

pre-defined symbolic constants (in hexadecimal):
invalid 0x00000001
new 0x00000008
established 0x00000002
related 0x00000004
untracked 0x00000040
# describe <datatype>
[root@centos8 ~]$nft describe ct_state
datatype ct_state (conntrack state) (basetype bitmask, integer), 32 bits

pre-defined symbolic constants (in hexadecimal):
invalid 0x00000001
new 0x00000008
established 0x00000002
related 0x00000004
untracked 0x00000040
# describe <expression>
[root@centos8 ~]$nft describe tcp dport
payload expression, datatype inet_service (internet network service) (basetype integer), 16 bits
# describe <datatype>
[root@centos8 ~]$nft describe inet_service
datatype inet_service (internet network service) (basetype integer), 16 bits

datatype

数据类型分级别,全局数据类型是最低阶的,有 integer、string 两种,高阶的数据类型从低阶的派生而来,例如:bitmask 从 integer 派生而来,ct_state 从 bitmask 派生而来。

大部分数据类型固定长度,少部分不固定。

一些高阶的数据类型具有预定义的符号常量,例如:boolean 类型,固定 size 1bit,取值 0 或 1,预定义了 exists 和 missing 两个字符常量,会被自动替换为 1 和 0;

  • integer:用于数值,可以指定为十进制、十六进制或八进制,没有固定大小
  • string:用于字符串,以字母开头,此外,双引号内的任何内容都被认为是字符串
  • bitmask:用于位掩码,基于 integer,至于什么是位掩码,自行百度或参考文章:什么是掩码 BitMask
  • boolean:用于布尔值,固定 size 1bit
  • inet_service:用于服务端口,固定 size 16bit
  • ipv4_addr:用于 IPv4 地址,固定 size 32bit,预定义字符常量 localhost
  • icmp_type:用于 ICMP 类型,固定 size 8bit,取值 0-18,预定义了 echo-reply、echo-request 等 19 个字符常量,会被自动替换为 0-18
  • 链接跟踪
    • ct_state
    • ct_status

expression

分为主表达式和有效负载表达式,不知道有什么区别,看起来好像在一条规则中,主表达式的位置在有效负载表达式的前面。

以下是对所有表达式做一个全面但简略的记录,详细信息参考:https://jlk.fjfi.cvut.cz/arch/manpages/man/nft.8

PRIMARY EXPRESSIONS
  • meta 表达式

    meta 就是数据包的元数据,元表达式有限定和非限定两种,限定不能省略 meta 关键字,非限定则可以省略

    1
    2
    3
    4
    # mete 关键字不可省略
    meta {length | nfproto | l4proto | protocol | priority}
    # meta 关键字可以省略
    [meta] {mark | iif | iifname | iiftype | oif | oifname | oiftype | skuid | skgid | nftrace | rtclassid | ibrname | obrname | pkttype | cpu | iifgroup | oifgroup | cgroup | random | ipsec | iifkind | oifkind | time | hour | day }

    范例:

    1
    2
    3
    4
    5
    6
    [root@centos8 ~]$nft describe meta iif
    meta expression, datatype iface_index (network interface index) (basetype integer), 32 bits
    [root@centos8 ~]$nft describe meta oif
    meta expression, datatype iface_index (network interface index) (basetype integer), 32 bits
    [root@centos8 ~]$nft describe meta iifname
    meta expression, datatype ifname (network interface name) (basetype string), 16 characters
  • socket 表达式

    套接字表达式用于搜索已打开的 TCP/UDP 套接字,及其与数据包关联的属性

    1
    socket {transparent | mark}
  • osf 表达式

    被动操作系统指纹识别。此表达式将数据包中的某些数据(窗口大小、MSS、选项及其顺序、DF 等)与 SYN 位集进行比较

    1
    osf [ttl {loose | skip}] {name | version}
    • ttl:对包进行 TTL 检查以确定操作系统
    • version:对包进行 OS 版本检查
    • name:要匹配的操作系统签名的名称。所有的签名都可以在 pf.os 文件中找到。对表达式无法检测到的操作系统签名使用“unknown”
  • fib 表达式

    1
    fib {saddr | daddr | mark | iif | oif} [. ...] {oif | oifname | type}
  • routing 表达式

    路由表达式指的是与数据包关联的路由数据

    1
    rt [ip | ip6] {classid | nexthop | mtu | ipsec}
  • ipsec 表达式

    1
    2
    ipsec {in | out} [ spnum NUM ]  {reqid | spi}
    ipsec {in | out} [ spnum NUM ] {ip | ip6} {saddr | daddr}
  • numgen 表达式

    1
    numgen {inc | random} mod NUM [ offset NUM ]
  • hash 表达式

    使用一个哈希函数来生成一个数字。可用的函数是 jhash(Jenkins 散列)和 symhash(对称散列)。
    jhash 和 symhash 的典型用例是负载均衡

    1
    2
    3
    jhash {ip saddr | ip6 daddr | tcp dport | udp sport | ether saddr} [. ...] mod NUM [ seed NUM ] [ offset NUM ]

    symhash mod NUM [ offset NUM ]
PRIMARY EXPRESSIONS
  • 以太网报头表达式

    1
    ether {daddr | saddr | type}
  • VLAN 头表达式

    1
    vlan {id | cfi | pcp | type}
  • ARP 头表达式

    1
    arp {htype | ptype | hlen | plen | operation | saddr { ip | ether } | daddr { ip | ether }
  • IPV4 头表达

    1
    ip {version | hdrlength | dscp | ecn | length | id | frag-off | ttl | protocol | checksum | saddr | daddr }
  • ICMP 头表达式

    1
    icmp {type | code | checksum | id | sequence | gateway | mtu}
  • IGMP 头表达式

    1
    igmp {type | mrt | checksum | group}
  • IPV6 报头表达式

    1
    ip6 {version | dscp | ecn | flowlabel | length | nexthdr | hoplimit | saddr | daddr}
  • ICMPV6 头表达

    1
    icmpv6 {type | code | checksum | parameter-problem | packet-too-big | id | sequence | max-delay}
  • TCP 报头表达式

    1
    tcp {sport | dport | sequence | ackseq | doff | reserved | flags | window | checksum | urgptr}
  • UDP 报头表达式

    1
    udp {sport | dport | length | checksum}
  • UDP-LITE 头表达

    1
    udplite {sport | dport | checksum}
  • SCTP 头表达式

    1
    sctp {sport | dport | vtag | checksum}
  • DCCP 头表达式

    1
    dccp {sport | dport}
  • 验证标题表达

    1
    ah {nexthdr | hdrlength | reserved | spi | sequence}
  • 加密的安全有效负载头表达式

    1
    esp {spi | sequence}
  • IPCOMP 头表达式

    1
    comp {nexthdr | flags | cpi}
  • 原始载荷表达式

    1

  • 扩展头表达式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    hbh {nexthdr | hdrlength}
    frag {nexthdr | frag-off | more-fragments | id}
    rt {nexthdr | hdrlength | type | seg-left}
    dst {nexthdr | hdrlength}
    mh {nexthdr | hdrlength | checksum | type}
    srh {flags | tag | sid | seg-left}
    tcp option {eol | noop | maxseg | window | sack-permitted | sack | sack0 | sack1 | sack2 | sack3 | timestamp} tcp_option_field
    ip option { lsrr | ra | rr | ssrr } ip_option_field
    #
    exthdr {hbh | frag | rt | dst | mh}
    tcp option {eol | noop | maxseg | window | sack-permitted | sack | sack0 | sack1 | sack2 | sack3 | timestamp}
    ip option { lsrr | ra | rr | ssrr }
  • CONNTRACK 表达式

    1
    2
    3
    4
    ct {state | direction | status | mark | expiration | helper | label}
    ct [original | reply] {l3proto | protocol | bytes | packets | avgpkt | zone | id}
    ct {original | reply} {proto-src | proto-dst}
    ct {original | reply} {ip | ip6} {saddr | daddr}

添加规则

语法:

1
nft {add|insert} rule [<family>] <table> <chain> [handle <handle>] <expressions> <statements> [comment <comment>]
  • handle:规则句柄,句柄是不变的,除非规则被删除,这就为规则提供了稳定的索引。add 添加规则到句柄规则后面,insert 添加规则到句柄规则的前面。如果省略,add 默认添加规则到链末尾,insert 默认添加规则到链的开头
  • expression:负责匹配数据包
  • statement:负责对匹配的数据包做后续处理
  • comment:备注

expressions

expr 回调函数每处理完一条表达式,就修改 verdict 寄存器的值为 CONTINUE,继续处理下一条。

上文 expression 章节中的表达式总结的很全面,下面对常用的表达式做详细的说明:

IP

上图是 IPv4 报头图,除了最后的可选字段,其他每个字段 matches 都可以匹配,下面挑常用的介绍,后面的 ip6、tcp、udp 等等,也都是只介绍常用的,这点就不再赘述:

  • protocol :上层协议

    1
    2
    3
    4
    ip protocol tcp
    ip protocol 6
    ip protocol != tcp
    ip protocol { icmp, esp, ah, comp, udp, udplite, tcp, dccp, sctp }
  • saddr :Source IP Address

    1
    2
    3
    4
    5
    6
    7
    ip saddr 192.168.2.0/24
    ip saddr != 192.168.2.0/24
    ip saddr 192.168.3.1 ip daddr 192.168.3.100
    ip saddr != 1.1.1.1
    ip saddr 1.1.1.1
    ip saddr & 0xff == 1
    ip saddr & 0.0.0.255 < 0.0.0.127
  • daddr :Destination IP Address

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    ip daddr 192.168.0.1
    ip daddr != 192.168.0.1
    ip daddr 192.168.0.1-192.168.0.250
    ip daddr 10.0.0.0-10.255.255.255
    ip daddr 172.16.0.0-172.31.255.255
    ip daddr 192.168.3.1-192.168.4.250
    ip daddr != 192.168.0.1-192.168.0.250
    ip daddr { 192.168.0.1-192.168.0.250 }
    ip daddr { 192.168.5.1, 192.168.5.2, 192.168.5.3 }
Ip6

  • flowlabel

    1
    2
    3
    4
    ip6 flowlabel 22
    ip6 flowlabel != 233
    ip6 flowlabel { 33, 55, 67, 88 }
    ip6 flowlabel { 33-55 }
  • saddr :Source Address

    1
    2
    3
    4
    ip6 saddr 1234:1234:1234:1234:1234:1234:1234:1234
    ip6 saddr ::1234:1234:1234:1234:1234:1234:1234
    ip6 saddr ::/64
    ip6 saddr ::1 ip6 daddr ::2
  • daddr :Destination Address

    1
    2
    ip6 daddr 1234:1234:1234:1234:1234:1234:1234:1234
    ip6 daddr != ::1234:1234:1234:1234:1234:1234:1234-1234:1234::1234:1234:1234:1234:1234
Tcp

  • sport :Source port

    1
    2
    3
    4
    5
    6
    tcp sport 22
    tcp sport != 33-45
    tcp sport { 33, 55, 67, 88}
    tcp sport { 33-55}
    tcp sport vmap { 25:accept, 28:drop }
    tcp sport 1024 tcp dport 22
  • dport :Destination port

    1
    2
    3
    4
    5
    6
    tcp dport 22
    tcp dport != 33-45
    tcp dport { 33-55 }
    tcp dport {telnet, ssh, http, https }
    tcp dport vmap { 22 : accept, 23 : drop }
    tcp dport vmap { 25:accept, 28:drop }
  • flags :TCP flags

    1
    2
    3
    tcp flags { fin, syn, rst, psh, ack, urg, ecn, cwr}
    tcp flags cwr
    tcp flags != cwr
Udp

  • sport :Source port
  • dport :Destination port
Udplite
Sctp
Dccp
Ah
Esp
Comp
Icmp


  • type <type>:ICMP packet type,也可以写数值,例如icmp type echo-reply等价于icmp type 0

    1
    2
    icmp type {echo-reply, destination-unreachable, source-quench, redirect, echo-request, time-exceeded, parameter-problem, timestamp-request, timestamp-reply, info-request, info-reply, address-mask-request, address-mask-reply, router-advertisement, router-solicitation}
    icmp type 8 reject # 禁止别人ping我
  • code <code>:ICMP packet code

    1
    2
    3
    icmp code 111
    icmp code != 33-55
    icmp code { 2, 4, 54, 33, 56}
  • mtu :ICMP packet mtu,最大传输单元

  • gateway :ICMP packet gateway

Icmpv6
Ether
Dst
Frag
Hbh
Mh
Rt
Vlan
Arp
Ct

Ct 是 Conntrack 的缩写,链接跟踪

  • state : State of the connection,这个匹配条件的作用和 iptables 的 state 模块一样

    1
    2
    3
    4
    ct state { new, established, related, invalid,untracked }
    ct state != related
    ct state established
    ct state 8
  • direction : Direction of the packet relative to the connection,数据包相对于链接的方向

    1
    2
    3
    ct direction original
    ct direction != original
    ct direction {reply, original}
  • status :Status of the connection

    1
    2
    3
    ct status expected
    ct status != expected
    ct status { expected,seen-reply,assured,confirmed,snat,dnat,dying }
  • mark [set]:Mark of the connection

  • expiration: Connection expiration time,连接过期时间

  • helper ““:Helper associated with the connection

    1
    ct helper "ftp"
  • [original|reply] bytes

    1
    2
    ct original bytes > 100000
    ct bytes > 100000
  • [original|reply] packets

    1
    ct reply packets < 100
  • [original | reply] saddr

    1
    2
    3
    4
    ct original saddr 192.168.0.1
    ct reply saddr 192.168.0.1
    ct original saddr 192.168.1.0/24
    ct reply saddr 192.168.1.0/24
  • [original | reply] daddr

    1
    2
    3
    4
    ct original daddr 192.168.0.1
    ct reply daddr 192.168.0.1
    ct original daddr 192.168.1.0/24
    ct reply daddr 192.168.1.0/24
  • [original | reply] l3proto

    1
    ct original l3proto ipv4
  • [original | reply] protocol

    1
    ct original protocol 6
  • [original | reply] proto-dst

    1
    ct original proto-dst 22
  • [original | reply] proto-src

    1
    ct reply proto-src 53
Meta

匹配(某些情况下可以 设置)数据包的元信息,虽然我现在也不知道元信息是什么?

  • 不能省略 meta 关键字

    • length – packet lenght
    • protocol – packet protocol (as in skb->protocol)
    • nfproto – netfilter packet protocol family (like ipv4, ipv6, etc..).
    • l4proto – layer 4 protocol (tcp, udp, etc..)
    • priority – packet priority, tc handle. Can be set.
    • random – match against a single/simple random number
    • secmark – packet secmark. Can be set.
    • ibrvproto – match the bridge protocol
    • ibrpvid – match the bridge pvid
  • 可以省略 meta 关键字

    • mark – packet mark. Can be set.
    • iif – input interface index
    • iifname – input interface name
    • iiftype – input interface type
    • oif – output interface index
    • oifname – output interface name
    • oiftype – output interface type
    • skuid – socket uid
    • skgid – socket gid
    • nftrace – nftrace debugging bit. Can be set.
    • rtclassid – realm
    • ibriport – input bridge port
    • obriport – output bridge port
    • ibridgename – input bridge name
    • obridgename – output bridge name
    • pkttype – packet type. Can be set.
    • cpu – cpu number
    • iifgroup – input interface group
    • oifgroup – output interface group
    • cgroup – cgroup number
    • ipsec – ipsec (secpath) packet or not
    • time – packet timestamp
    • day – packet timestamp
    • hour – packet timestamp

Matching packets by interface name 通过接口名称匹配数据包

1
nft add rule filter input meta oifname lo accept

Matching packets by packet mark 通过数据标记匹配数据包

1
nft add rule filter output meta mark 123 counter

Matching packets the socket UID 匹配套接字 UID 的数据包

1
2
nft add rule filter output meta skuid pablo counter
nft add rule filter output meta skuid 1000 counter

Matching packet priority 匹配数据包的优先级

1
2
nft add rule filter forward meta priority abcd:1234
nft add rule filter forward meta priority none

statements

语句,分为两种,终结和非终结,终结语句不会再继续处理后面的表达式和规则,防火墙完成本次任务,而非终结语句会继续处理后面的表达式或者规则。

  • Verdict:重定向
  • Log:记录日志并继续处理请求
  • Reject:停止处理并拒绝请求
  • Counter:计数
  • Limit:如果达到了接收数据包的匹配限制,则根据规则处理数据包
  • Nat:NAT,分为 snat 和 dnat
  • Queuea:停止处理并发送数据包到用户空间程序
verdict

每条 verdict 语句都与 verdict 寄存器的预设值一一对应,只负责控制已匹配数据包的重定向,如果在决定数据包的去向之前有任何操作,交给其他类型的语句负责

expr 处理完 verdict 语句,会跳出当前表达式的循环(即上文 nftables 框架的执行流程中的循环 2)这样不管最终重定向到哪里,都会就此结束当前规则,所以 verdict 语句,要写在规则的最后。

  • accept:终结语句,接受数据包
  • drop:终结语句,接受数据包
  • queue:将数据包排队到用户空间,queue 本身不是终结语句,但是需要搭配 accept 或者 drop 使用
  • continue:跳过本条规则后面的表达式和语句,处理下一条规则
  • return:
    • 从一个 CHAIN 里可以 jump 到另一个 CHAIN, jump 到的那个 CHAIN 是子 CHAIN
    • 从子 CHAIN return 后,回到触发 jump 的那条规则,从那条规则的下一条继续匹配
    • 如果不是在子 CHAIN 里 return,而是在 main CHAIN,那么就以默认规则进行
  • jump :跳转到指定的规则链,当执行完成或者返回时,返回到调用的规则链
  • goto :类似于跳转,发送到指定规则链但不返回
log
  • level [over] [burst ]:Log level

    1
    2
    3
    4
    5
    6
    7
    8
    9
    log
    log level emerg
    log level alert
    log level crit
    log level err
    log level warn
    log level notice
    log level info
    log level debug
  • group [queue-threshold ] [snaplen ] [prefix ““]

    1
    2
    3
    log prefix aaaaa-aaaaaa group 2 snaplen 33
    log group 2 queue-threshold 2
    log group 2 snaplen 33
reject
  • with type

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    reject
    reject with icmp type host-unreachable
    reject with icmp type net-unreachable
    reject with icmp type prot-unreachable
    reject with icmp type port-unreachable
    reject with icmp type net-prohibited
    reject with icmp type host-prohibited
    reject with icmp type admin-prohibited
    reject with icmpv6 type no-route
    reject with icmpv6 type admin-prohibited
    reject with icmpv6 type addr-unreachable
    reject with icmpv6 type port-unreachable
    reject with icmpx type host-unreachable
    reject with icmpx type no-route
    reject with icmpx type admin-prohibited
    reject with icmpx type port-unreachable
    ip protocol tcp reject with tcp reset
counter

按照数据包的大小递增字节计数器以及包计数器的值

  • packets bytes

    1
    2
    3
    4
    # counter 和 counter packets 0 bytes 0 是等价的,都表示从0开始计数,
    # 过一段时间再 nft list ruleset 查看过滤规则,packets 和 bytes 就会显示通过过滤的数据计数
    counter
    counter packets 0 bytes 0
Limit

等同于 iptables 中的 limit 条件匹配模块

  • rate [over] [burst ]:Rate limit,令牌桶算法限速

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    limit rate 400/minute
    limit rate 400/hour
    limit rate over 40/day
    limit rate over 400/week
    limit rate over 1023/second burst 10 packets
    limit rate 1025 kbytes/second
    limit rate 1023000 mbytes/second
    limit rate 1025 bytes/second burst 512 bytes
    limit rate 1025 kbytes/second burst 1023 kbytes
    limit rate 1025 mbytes/second burst 1025 kbytes
    limit rate 1025000 mbytes/second burst 1023 mbytes
nat

适用于 nat 链

  • dnat :Destination address translation,目标地址转换

    1
    2
    dnat 192.168.3.2
    dnat ct mark map { 0x00000014 : 1.2.3.4}
  • snat :Source address translation 源地址转换

    1
    2
    snat 192.168.3.2
    snat 2001:838:35f:1::-2001:838:35f:2:::100
  • masquerade [] [to :]:Masquerade,特殊的 snat,可以把 MASQUERADE 理解为动态的、自动化的 SNAT,如果没有动态 SNAT 的需求,没有必要使 MASQUERADE,因为 SNAT 更加高效

    1
    2
    3
    4
    masquerade
    masquerade persistent,fully-random,random
    masquerade to :1024
    masquerade to :1024-2048
queueaz
  • num

    1
    2
    3
    4
    5
    6
    queue
    queue num 2
    queue num 2-3
    queue num 4-5 fanout bypass
    queue num 4-5 fanout
    queue num 4-5 bypass
tproxy
synproxy
flow
dup
fwd
set
map
vmap

范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 新建filter表
[root@centos8 ~]$nft add table ip filter

# 添加input、forward、output三个基本链,input和forward的默认策略是drop,output的默认策略是accpet
nft add chain inet filter input {type filter hook input priority 0\; policy drop\;}
nft add chain inet filter forwad {type filter hook forward priority 0\; policy drop\;}
nft add chain inet filter output {type filter hook output priority 0\; policy accept\;}

# 添加两个与TCP和UDP关联的常规链
nft add chain inet filter TCP
nft add chain inet filter UDP

# related 和 established 的流量都accept
nft add rule inet filter input ct state related,established accept

# loopback 接口流量会 accept,省略meta关键字
nft add rule inet filter input iif lo accept

# 无效的流量都 drop
nft add rule inet filter input ct state invalid drop

# 新的echo请求(ping)accept
nft add rule inet filter input ip protocol icmp icmp type echo-request ct state new accept

# 新的UDP流量跳转到UDP链
nft add rule inet filter input ip protocol udp ct state new jump UDP
# 新的TCP流量跳转到TCP链
# &:按位与;|:按位或
# fin、syn、rst、ack四个值按位或,统计有这四个值有几个为1,数据包与统计结果的flags按位与(为什么不按位或呢,因为flags有8位,其他四位不关心),如果结果只有syn位为1,就说明是tcp第一次握手
nft add rule inet filter input ip protocol tcp tcp flags \& \(fin\|syn\|rst\|ack\) == syn ct state new jump TCP

# 未由其他规则处理的所有流量会reject
nft add rule inet filter input ip protocol udp reject
nft add rule inet filter input ip protocol tcp reject with tcp reset
nft add rule inet filter input counter reject with icmp type prot-unreachable

# 由TCP和UDP链处理,打开web服务器的连接端口
nft add rule inet filter TCP tcp dport 80 accept
nft add rule inet filter TCP tcp dport 443 accept
# 允许SSH连接端口22
nft add rule inet filter TCP tcp dport 22 accept
# 允许传入DNS请求
nft add rule inet filter TCP tcp dport 53 accept
nft add rule inet filter UDP udp dport 53 accept

替换规则

指定 handle,替换规则

1
nft replace rule [<family>] <table> <chain> handle <handle> <matches> <statements>

列出规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 新建filter表
[root@centos8 ~]$nft add table ip filter
# 新建INPUT链
[root@centos8 ~]$nft add chain ip filter INPUT {type filter hook input priority 0\;}
# 添加规则,禁ping
[root@centos8 ~]$nft add rule ip filter INPUT icmp type 8 reject

# 列出指定链中的规则
[root@centos8 ~]$nft list chain ip filter INPUT
table ip filter {
chain INPUT {
type filter hook input priority filter; policy accept;
icmp type echo-request reject
}
}
# -a,--handle:显示句柄值;-n,--numeric:数字化输出
[root@centos8 ~]$nft -an list chain ip filter INPUT
table ip filter {
chain INPUT { # handle 4
type filter hook input priority 0; policy accept;
icmp type 8 reject # handle 6
}
}
1
2
# 列出所有的规则
nft list ruleset

删除规则

删除规则只能通过 handle 值,所以删除之前需要先查询规则的 handle 值

1
2
3
4
# 语法
nft delete rule [<family>] <table> <chain> handle <handle>
# 范例
[root@centos8 ~]$nft delete rule ip filter INPUT handle 7

清空规则

1
2
3
4
# 清空链中的规则
nft flush chain [<family>] <table> <chain>
# 清空所有规则
nft flush ruleset

数据报文分组、分类的数据结构

表与命名空间

在 nftables 中,每个表都是独立的命名空间,这就意味着不同的表中的链、集合、字典等名字可以相同。不同的应用就可以在相互不影响的情况下管理自己表中的规则,不过要注意,由于 nftables 将每个表都视为独立的防火墙,一个数据包必须被所有表中的规则放行才能真正通过,如果在一个 hook,出现两条优先级相同的链,就会进入竞争状态。

集合(sets)

集合可以用来匹配多个 IP 地址、端口号、网卡或其他任何条件。
相对 iptables 来说,nftables 原生支持集合,并不需要借助 ipset 来实现。
nftables 的集合分为匿名集合与命名集合。

集合中的元素可能是连续的,也可能是不连续的,连续的使用 - 划分范围,不连续的使用 , 分割

匿名集合

范例:

1
nft add rule ip filter output tcp dport { 22, 23 } counter

如果写在文件中,通过 -f 加载,可以使用 define 关键字定义,使用 $ 关键字调用,范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
define CDN_EDGE = {
192.168.1.1,
192.168.1.2,
192.168.1.3,
10.0.0.0/8
}

define CDN_MONITORS = {
192.168.1.10,
192.168.1.20
}

define CDN = {
$CDN_EDGE,
$CDN_MONITORS
}
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
tcp dport { http, https } ip saddr $CDN accept
udp dport { http, https } ip saddr $CDN accept
}
}
命名集合
新建命名集合
1
add set [<family>] <table> <set> { type <type> | typeof <expression> ; [flags <flags> ;] [timeout <timeout> ;] [gc-interval <gc-interval> ;] [elements = { <element>[, ...] } ;] [size <size> ;] [policy <policy> ;] [auto-merge ;] }

上文的 表达式 expression 章节介绍了 datatype 和 expression。集合中的 type 和 typeof 关键字功能类似,type 指定 datatype,typeof 指定 expression,推荐使用 typeof,更易读

  • type:指定集合中元素的类型,当前支持的数据类型有:

  • ipv4_addr : IPv4 地址

    • ipv6_addr : IPv6 地址
    • ether_addr : 以太网(Ethernet)地址
    • inet_proto : 网络协议
    • inet_service : 网络服务
    • mark : 标记类型
  • typeof:typeof 关键字从 0.9.4 版本开始用有效,允许使用表达式(参考下文的表达式章节),让 nftables 解析基本类型

  • flags:标志

    • constant:内容不可变
    • interval:内容包含区间集
    • timeout:可以给元素设置一个过期时间
  • timeout:设置元素的过期时间

  • gc-interval:

  • elements:给初始化的集合填充元素

  • size:限制集合中元素的数量

  • policy:集合策略

    • performance [default]
    • memory
  • auto-merge:相邻/重叠集元素自动合并(仅适用于区间集)

  • counter:给每个元素设置计数器

范例:

使用 @ 关键字调用命名集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
flush ruleset
table inet filter {
set ip_saddrs {
typeof ip saddr
flags interval
elements = { 10.0.0.0/24, 192.168.248.0/24 }
}

set tcp_dports {
typeof tcp dport
elements = { 22, 80, 443 }
}

chain input {
type filter hook input priority filter; policy drop;
ip saddr @ip_saddrs tcp dport @tcp_dports accept
}
}
列出命名集合
1
2
3
4
# 默认列出全部集合
nft list sets [<family>]
# 列出一个集合
nft list set [<family>] <table> <set>

映射(Maps)

映射的元素形式是 key-value,key 和 vaule 之间的映射关系取决于映射的 type 或者 typeof

配置文件中的语法格式:

1
2
3
4
5
6
7
8
9
10
11
12
map <map> {
type <type> : <type>
# 或
typeof <expression> : <expression>
# 或
type <type> : verdict

[flags <flags> ;]
[elements = { <element>[, ...] } ;]
[size <size> ;]
[policy <policy> ;]
}

映射用于 map 和 vmap 语句,当键值均为表达式时,用于 map,当值为 verdict 语句时,用于 vmap。

map 通常用于 nat 地址转换。

  • type:指定 datatype,支持以下 datatype,注意 counter 和 quota 不能用于键
    • ipv4_addr : IPv4 地址
    • ipv6_addr : IPv6 地址
    • ether_addr : 以太网(Ethernet)地址
    • inet_proto : 网络协议
    • inet_service : 网络服务
    • mark : 标记类型
    • counter:
    • quota:
  • typeof:指定 expression
  • flags:标志
    • constant
    • interval
  • elements:初始化映射中的元素
  • size:限制映射中元素的数量
  • policy:策略
    • performance [default]
    • memory

范例:

使用 @ 关键字调用映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
flush ruleset
table inet filter {
set tcp_dports {
typeof tcp dport
elements = { 22, 80, 443 }
}

map test_vmap {
type ipv4_addr : verdict
flags interval
elements = {
192.168.248.0/24 : drop,
10.0.0.1/24 : accept
}
}

chain input {
type filter hook input priority filter; policy drop;
ip saddr vmap @test_vmap tcp dport @tcp_dports accept
}
}

Concatenations

Metering

以前称之为 flow tables

Updating sets from the packet path

Math operations

Stateful objects

Flowtable (the fastpath network stack bypass)

脚本

可以将规则写在文件中,通过nft -f file加载。推荐这种方式

练习

说明:以下练习 INPUT 和 OUTPUT 默认策略均为 DROP

1、限制本地主机的 web 服务器在周一不允许访问;新请求的速率不能超过 100 个每秒;web 服务器包含了 admin 字符串的页面不允许访问;web 服务器仅允许响应报文离开本机

2、在工作时间,即周一到周五的 8:30-18:00,开放本机的 ftp 服务给 172.16.0.0 网络中的主机访问;数据下载请求的次数每分钟不得超过 5 个

3、开放本机的 ssh 服务给 172.16.x.1-172.16.x.100 中的主机,x 为你的学号,新请求建立的速率一分钟不得超过 2 个;仅允许响应报文通过其服务端口离开本机

4、拒绝 TCP 标志位全部为 1 及全部为 0 的报文访问本机

5、允许本机 ping 别的主机;但不开放别的主机 ping 本机

6、判断下述规则的意义

从指定安装源创建新虚拟机

1
virt-install --name NAME --memory MB STORAGE INSTALL [options]
  • –help:帮助信息
  • –version:版本信息
  • –connect URI:通过 libvirt URI 连接到虚拟机管理程序(hypervisor)

通用选项:

  • -n NAME, –name NAME 客户机实例名称

  • –memory MEMORY:配置虚拟机内存分配,默认 m 为单位

    1
    2
    --memory 1024 (in MiB)
    --memory memory=1024,currentMemory=512
  • –vcpus VCPUS:为虚拟机配置的 vcpus 数

    1
    2
    3
    --vcpus 5
    --vcpus 5,maxvcpus=10,cpuset=1-4,6,8
    --vcpus sockets=2,cores=4,threads=2
  • –cpu CPU:CPU 型号及功能

  • –metadata METADATA:配置客户机元数据

    1
    2
    --metadata name=foo,title="My pretty title",uuid=...
    --metadata description="My nice long description"

安装方法选项:

  • –cdrom CDROM:光驱安装介质,用不到

  • -l | –location LOCATION:安装源,常用

    1
    2
    3
    nfs:host:/path
    http://host/path
    ftp://host/path
  • –pxe:使用 PXE 协议从网络引导

  • –import:在已有的磁盘镜像中构建客户机

  • -x | –extra-args EXTRA_ARGS:将附加参数添加到由 –location 引导的内核中

  • –initrd-inject INITRD_INJECT:添加指定文件到由 –location 指定的 initrd 根中

  • –boot BOOT:配置客户机引导设置

    1
    2
    --boot hd,cdrom,menu=on    # hd是harddisk,cdrom是光盘
    --boot init=/sbin/init (针对容器)
  • –idmap IDMAP:为 LXC 容器启用用户名称空间

    1
    --idmap uid_start=0,uid_target=1000,uid_count=10
  • –unattended [UNATTENDED]:Perform an unattended installation

  • –install INSTALL:Specify fine grained install options

OS 选项:

  • –os-variant OS_VARIANT:在其中安装 OS 变体的虚拟机,比如:’fedora18’、’rhel6’、’winxp’ 等等

设备选项:

  • –disk DISK:指定存储的各种选项

    1
    2
    3
    4
    5
    6
    7
    --disk size=10 (在默认位置创建 10GiB 镜像)
    --disk /my/existing/disk,cache=none
    --disk device=cdrom,bus=scsi
    --disk=?

    --disk path=/var/lib/libvirt/images/centos7-pxe1.qcow2,bus=virtio
    --disk path=/dev/vm_images_lvm/lv2,bus=virtio
  • -w | –network NETWORK:配置客户机网络接口

    1
    2
    3
    4
    5
    --network bridge=mybr0
    --network network=my_libvirt_virtual_net
    --network network=mynet,model=virtio,mac=00:11...
    --network none
    --network help
  • –graphics GRAPHICS:配置虚拟机显示设置

    1
    2
    3
    --graphics spice
    --graphics vnc,port=5901,listen=0.0.0.0
    --graphics none
  • –controller CONTROLLER:配置虚拟机控制程序设备

    1
    2
    --controller type=usb,model=qemu-xhci
    --controller virtio-scsi
  • –input INPUT:配置客户机输入设备

    1
    2
    --input tablet
    --input keyboard,bus=usb
  • –serial SERIAL:配置客户机串口设备

  • –parallel PARALLEL:配置客户机并口设备

  • –channel CHANNEL:配置客户机通信通道

  • –console CONSOLE:配置文本控制台连接主机与客户机

  • –hostdev HOSTDEV:配置物理 USB/PCI 等主机设备与客户机共享

  • –filesystem FILESYSTEM:传递主机目录到客户机

    1
    2
    --filesystem /my/source/dir,/dir/in/guest
    --filesystem template_name,/,type=template
  • –sound [SOUND]:配置客户机声音设备仿真

  • –watchdog WATCHDOG:配置客户机 watchdog 设备

  • –video VIDEO:配置客户机视频硬件

  • –smartcard SMARTCARD:配置客户机智能卡设备

    1
    --smartcard mode=passthrough
  • –redirdev REDIRDEV:

    1
    --redirdev usb,type=tcp,server=192.168.1.1:4000
  • –memballoon MEMBALLOON:配置客户机 memballoon 设备

    1
    --memballoon model=virtio
  • –tpm TPM:配置客户机 TPM 设备

    1
    --tpm /dev/tpm
  • –rng RNG:Configure a guest RNG device

    1
    --rng /dev/urandom
  • –panic PANIC:配置客户机 panic 设备

    1
    --panic default
  • –memdev MEMDEV:Configure a guest memory device

    1
    --memdev dimm,target.size=1024
  • –vsock VSOCK:Configure guest vsock sockets

    1
    2
    --vsock cid.auto=yes
    --vsock cid.address=7

客户机配置选项:

  • –iothreads IOTHREADS:Set domain and configuration

  • –seclabel | –security SECLABEL:Set domain seclabel configuration

  • –cputune CPUTUNE:Tune CPU parameters for the domain process

  • –numatune NUMATUNE:为域进程调整 NUMA 策略

  • –memtune MEMTUNE:为域进程调整内存策略

  • –blkiotune BLKIOTUNE:为域进程调整 blkio 策略。

  • –memorybacking MEMORYBACKING:为域进程设置内存后备策略

    1
    --memorybacking hugepages=on
  • –features FEATURES:Set domain XML

    1
    2
    --features acpi=off
    --features apic=on,apic.eoi=on
  • –clock CLOCK:设置域 XML

    1
    --clock offset=localtime,rtc_tickpolicy=catchup
  • –pm PM:配置 VM 电源管理功能

  • –events EVENTS:配置 VM 生命周期管理策略

  • –resource RESOURCE:配置 VM 资源分区(cgroups)

  • –sysinfo SYSINFO:Configure SMBIOS System Information

    1
    2
    --sysinfo host
    --sysinfo bios.vendor=MyVendor,bios.version=1.2.3,...
  • –qemu-commandline QEMU_COMMANDLINE:Pass arguments directly to the qemu emulator

    1
    2
    --qemu-commandline='-display gtk,gl=on'
    --qemu-commandline env=DISPLAY=:0.1
  • –launchSecurity | -launchsecurity LAUNCHSECURITY:Configure VM launch security (e.g. SEV memory encryption)

    1
    2
    --launchSecurity type=sev,cbitpos=47,reducedPhysBits=1,policy=0x0001,dhCert=BASE64CERT
    --launchSecurity sev

虚拟化平台选项:

  • -v, –hvm:这个客户机应该是一个全虚拟化客户机
  • -p, –paravirt:这个客户机应该是一个半虚拟化客户机
  • –container:这个客户机应该是一个容器客户机
  • –virt-type VIRT_TYPE:要使用的管理程序名称 (kvm, qemu, xen, …)
  • –arch ARCH:模拟 CPU 架构
  • –machine MACHINE:机器类型为仿真类型,要模拟的机器类型

其它选项:

  • –autostart:主机启动时自动启动域。

  • –transient:Create a transient domain.

  • –destroy-on-exit:Force power off the domain when the console viewer is closed.

  • –wait [WAIT]:请等待数分钟以便完成安装

  • –noautoconsole:不要自动尝试连接到客户端控制台

  • –noreboot:安装完成后不启动客户机

  • –print-xml [XMLONLY]:打印生成的 XML 域,而不是创建客户机。

  • –dry-run:运行安装程序,但不创建设备或定义客户机。

  • –check CHECK:启用或禁用验证检查

    1
    2
    --check path_in_use=off
    --check all=off
  • -q, –quiet:禁止无错误输出

  • -d, –debug:输入故障排除信息

最后

使用 ‘–option=?’ 或 ‘–option help’ 来查看可用的子选项
请参考 man 手册,以便了解示例和完整的选项语法。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
lujinkai@Z510:~$ sudo virt-install --network help
--network options:
clearxml
address.base
address.bus
address.controller
address.cssid
address.devno
address.domain
address.function
address.iobase
address.irq
address.multifunction
address.port
address.reg
address.slot
address.ssid
address.target
address.type
address.unit
address.zpci.fid
address.zpci.uid
alias.name
boot.loadparm
boot.order
bridge
driver.ats
driver.iommu
driver.name
driver.queues
filterref.filter
link.state
mac
mac.address
model
model.type
mtu.size
network
rom.bar
rom.file
source
source.mode
source.path
source.portgroup
source.type
target.dev
trustGuestRxFilters
type
virtualport.parameters.instanceid
virtualport.parameters.interfaceid
virtualport.parameters.managerid
virtualport.parameters.profileid
virtualport.parameters.typeid
virtualport.parameters.typeidversion
virtualport.type

1
2
3
4
5
lujinkai@Z510:~$ man virt-install
...
内容太多
...
/--network # 搜索对应的内容

virsh 能实现的,virt-manger 都能实现

常用子命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
help               # 打印基本帮助信息
attach-device # 使用XML文件中的设备定义在虚拟机中添加设备
attach-disk # 在虚拟机中附加新磁盘设备
attach-interface # 在虚拟机中附加新网络接口
create # 从 XML 配置文件生成虚拟机并启动新虚拟机
define # 为虚拟机输出XML配置文件
destroy # 强制虚拟机停止
detach-device # 从虚拟机中分离设备,使用同样的XML 描述作为命令
attach-device
detach-disk # 从虚拟机中分离磁盘设备
detach-interface # 从虚拟机中分离网络接口
domblkstat # 显示正在运行的虚拟机的块设备统计
domid # 显示虚拟机ID
domifstat # 显示正在运行的虚拟机的网络接口统计
dominfo # 显示虚拟机信息
domname # 显示虚拟机名称
domstate # 显示虚以机状态
domuuid # 显示虚拟机UUID
dumpxml # 输出虚拟机 XML配置文件
list # 列出所有虚拟机
migrate # 将虚拟机迁移到另一台主机中
nodeinfo # 有关管理程序的输出信息
quit # 退出这个互动终端
reboot # 重新启动虚拟机
restore # 恢复以前保存在文件中的虚拟机
resume # 恢复暂停的虚拟机
save # 将虚拟机当前状态保存到某个文件中
setmaxmem # 为管理程序设定内存上限
setmem # 为虚拟机设定分配的内存
setvcpus # 修改为虚拟机分配的虚拟CPU数目
shutdown # 关闭某个虚拟机
start # 启动未激活的虚拟机
suspend # 暂停虚拟机
undefine # 删除与虚拟机关联的所有文件
vcpupin # 控制虚拟机的虚拟CPU亲和性
version # 显示virsh版

查看帮助:

1
2
virsh help    # virsh帮助
virsh help COMMAND # virsh子命令帮助

管理存储池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# virsh help pool

find-storage-pool-sources-as # 通过参数查找存储池源 find potential storage pool sources
find-storage-pool-sources # 通过XML文档查找存储池源找到潜在存储池源
pool-autostart # 自动启动某个池
pool-build # 建立池
pool-create-as # 在一组变量中定义池
pool-create # 从一个XML文件中创建一个池
pool-define-as # 从一组变量中创建一个池
pool-define # 在一个XML文件中定义(但不启动)一个池或修改已经有池
pool-delete # 删除池
pool-destroy # 销毁(停止)池
pool-dumpxml # 将池信息保存到XML文件中的
pool-edit # 为存储池编辑XML配置
pool-info # 存储池信息
pool-list # 列出池
pool-name # 把一个池名称转换为池UUID
pool-refresh # 刷新池
pool-start # 启动一个(以前定义的)非活跃的池
pool-undefine # 取消定义一个不活跃的池
pool-uuid # 将池UUID转换为池名称
pool-event # Storage Pool Events
pool-capabilities # storage pool capabilities
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# virsh help pool-create-as

virsh pool-create-as <name> <type> [OPTIONS]

每一个存储池都对应一个xml配置文件,其中的标签和下面的option都有对应
OPTIONS:
[--name] <string> # 储存池名称
[--type] <string> # 储存池类型
--print-xml # 打印xml文档
--source-host <string> # 底层存储的源主机
--source-path <string> # 底层存储的源路径
--source-dev <string> # 用于底层存储的源设备
--source-name <string> # 底层存储的源名称
--target <string> # 底层存储的目标
--source-format <string> # 底层存储格式
--auth-type <string> # 用于底层存储的认证类型
--auth-username <string> # 用于底层存储的认证用户名
--secret-usage <string> # 用于底层存储的认证秘密用法
--secret-uuid <string> # 用于底层存储的秘密UUID
--adapter-name <string> # 用于基础存储的适配器名称
--adapter-wwnn <string> # 用于底层存储的适配器wwnn
--adapter-wwpn <string> # 用于底层存储的适配器wwpn
--adapter-parent <string> # 用于底层vHBA存储的适配器父scsi_hostN
--adapter-parent-wwnn <string> # 用于底层vHBA存储的适配器父scsi_hostN wwnn
--adapter-parent-wwpn <string> # 用于底层vHBA存储的适配器父scsi_hostN wwpn
--adapter-parent-fabric-wwn <string> # 用于底层vHBA存储的适配器父scsi_hostN fabric_wwn
--source-protocol-ver <string> # nfsvers值NFS储存池挂载选项
--build # build the pool as normal
--no-overwrite # do not overwrite any existing data
--no-overwrite # overwrite any existing data

基于 dir 的 pool

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. 创建存储池 vm_images_dir
virsh pool-define-as vm_images_dir dir --target "/data/vm_images"
# 2. /data/vm_images目录如果没有提前创建,可以使用pool-build自动创建
virsh pool-build vm_images_dir
# 3. 启动存储池
virsh pool-start vm_images_dir
# 4. 设置存储池自动启动
virsh pool-autostart vm_images_dir
# 5. 停止存储池,删除之前必须先停止
virsh pool-destroy --pool vm_images_dir
# 6. 删除存储池
virsh pool-delete --pool vm_images_dir # 删除数据
virsh pool-undefine --pool vm_images_dir # 删除配置

基于 fs 的 pool

基于已经创建文件系统的分区

1
2
3
4
5
6
7
8
9
10
# 1. 准备分区并创建文件系统
fdisk /dev/sdb
mkfs.ext4 /dev/sdb6
# 2. 准备存储池的挂载点目录,如果没有创建,创建存储池后执行`virsh pool-build vm_images_fs`会自动生成
mkdir /vm_images
# 3. 创建存储池 vm_images_fs
virsh pool-define-as vm_images_fs fs --source-dev "/dev/sdb6" --target "/vm_images"
# --source-dev:块设备名
# --target:mount到的目录名,libvirtd 会自动mount
# 4、5、6 启动、自动启动,停止、删除等操作所有类型的对于所有类型的存储池都一样,这里就不重复了

基于 disk 的 pool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 准备工作:确认/dev/sdb磁盘的格式为gpt
# 2. 创建存储池
virsh pool-define-as vm_images_disk disk --source-dev "/dev/sdb" --source-format gpt --target "/dev"
# 或者直接编辑xml配置文件
<pool type='disk'>
<name>vm_images_disk</name>
<source>
<device path='/dev/sdb'/>
<format type='gpt'/>
</source>
<target>
<path>/dev</path>
</target>
</pool>
# 通过xml配置文件创建存储池
virsh pool-define vm_images_disk.xml

基于 LVM 的 pool

没有卷组,直接创建存储池:

1
2
virsh pool-define-as vm_images_lvm logical --source-dev=/dev/sdb
virsh pool-build vm_images_lvm

基于已有卷组,创建存储池(推荐):

1
virsh pool-define-as vm_images_lvm logical --source-name=vm_images_lvm

基于 NFS 的 pool

1
2
3
# 在宿主机执行:
virsh pool-define-as vm_images_nfs netfs \
--source-host 10.0.0.18 --source-path /data/kvmdata --target /data/vm_images_nfs

管理存储卷

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# virsh help volume

vol-clone # 克隆卷
vol-create-as # 通过一组参数创建卷
vol-create # 通过XML文件创建卷
vol-create-from # 通过输入的其他卷创建—个新的卷
vol-delete # 删除一个卷
vol-download # 下载卷的内容到一个文件
vol-dumpxml # 保存卷信息的信息到xML文件中
vol-info # 存储卷的信息
vol-key # 根据卷名或路径返回卷的key
vol-list # 列出卷
vol-name # 根据卷的key或路径返回卷名
vol-path # 根据卷名或key返回卷的路径
vol-pool # 根据卷的key或路径返回存储池
vol-resize # 调整卷大小
vol-upload # 上传文件内容到一个卷
vol-wipe # wipe ─个卷
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# virsh help vol-create-as

virsh vol-create-as <pool> <name> <capacity> [OPTIONS]

OPTIONS
[--pool] <string> pool name
[--name] <string> name of the volume
[--capacity] <string> size of the vol, as scaled integer (default bytes)
--allocation <string> initial allocation size, as scaled integer (default bytes)
--format <string> file format type raw,bochs,qcow,qcow2,qed,vmdk
--backing-vol <string> the backing volume if taking a snapshot
--backing-vol-format <string> format of backing volume if taking a snapshot
--prealloc-metadata preallocate metadata (for qcow2 instead of full allocation)
--print-xml print XML document, but don't define/create

基于文件系统的存储池,创建其中的存储卷,可以使用 touchmkdirqemu-img create等命令,这些命令只能创建特定的存储卷,例如touch创建 raw 格式的文件,mkdir创建目录,qemu-img create创建磁盘镜像。

基于磁盘的存储池,创建其中的存储卷,也有对应命令,例如 disk 格式的存储池,使用fdisk创建分区;logical 格式的存储池,使用lvcreate,等等

virsh vol-create-as 配置项丰富,可以给各种类型的存储池,创建各种格式的存储卷,可以说相当于上述各种命令的合体

基于目录的 pool 的 volume 管理

1
2
3
4
# 新建
virsh vol-create-as vm_images_dir test1.qcow2 1g --format qcow2
# 删除
virsh vol-delete test1.qcow2 vm_images_dir

基于 LVM 的 pool 的 volume 管理

1
2
3
4
# 新建
virsh vol-create-as vm_images_lvm lvvol1 10g
# 删除
virsh vol-delete lvvol1 vm_images_lvm

克隆存储卷

克隆比直接 cp 复制文件要好

1
virsh vol-clone <vol> <newname> [--pool <string>] [--prealloc-metadata] [--reflink]

向虚拟机添加存储卷

1
2
3
4
virsh attach-disk <domain> <source> <target> [OPTIONS]

virsh attach-disk \
--domain centos7 --sourcetype block --source /dev/vm_images_lvm/lvvol1 --target vdb

虚拟化基础

Hypervisor

  • Hyperisor 是一种运行在基础物理服务器和操作系统之间的中间软件层,其可以允许多个操作系统和应用共享底层的内存、CPU、磁盘等物理硬件,也可以叫做 VMM(vritual machine moitor),即 虚拟机监视器
  • Hypervisor 是所有虚拟化技术的核心,非中断地支持多工作负载迁移的能力是 Hypervisor 的基本功能,当服务器启动并执行 Hypervisor 时,它会给每一台虚拟机分配适量的内存、CPU、网络和磁盘,并加载所有虚拟机的客户操作系统

X86 CPU 的保护环:

Hypervisor 分类

  • 裸金属型
  • 宿主型

虚拟化技术分类

模拟器 / 软件仿真

通过软件模拟完整的硬件环境来虚拟化来宾平台,可以模拟 X86、ARM 、PowerPC 等多种 CPU

性能比较低

产品或方案:QEMU

全虚拟化

全虚拟化不做 CPU 和内存模拟,只对 CPU 和内存做相应的分配等操作

软件辅助的全虚拟化

在 Intel 等 CPU 厂商还没有发布 x86 CPU 虚拟化技术之前,完全虚拟化都是通过软件辅助的方式来实现的

硬件辅助的完全虚拟化

2005 年 Intel 提出并开发了由 CPU 直接支持的虚拟化技术,CPU 可以明确的分辨出来自 GuestOS 的特权指令,并针对 GuestOS 进行特权操作,而不会影响到 HostOS。

将虚拟化模块加载到 HostOS 的内核中,例如:KVM。
KVM 通过在 HostOS 内核中加载 KVM Kernel Module 来将 HostOS 转换成为一个 VMM。所以此时 VMM 可以看作是 HostOS,反之亦然。

半虚拟化

半虚拟化要求 guest OS 知道自己运行在虚拟化环境中,因此 guest OS 的系统架构必须和宿主机的系统架构相同,并且要求对 guest OS 的内核做相应的修改。

如果不修改内核而实现半虚拟化,可以在每一个 GuestOS 中安装半虚拟化软件,如 VMTools、RHEVTools。

云计算

美国国家标准与技术研究院(NIST)定义:

云计算是一种按使用量付费的模式,这种模式提供可用的、便捷的、按需的网络访问, 进入可配置的计算资源共享池(资源包括网络,服务器,存储,应用软件,服务),这些资源能够被快速提供,只需投入很少的管理工作,或与服务供应商进行很少的交互

  • 公有云:阿里云、腾讯云等,个人都可以付费使用,不需要关心底层硬件,但是数据安全需要考虑
  • 私有云:在自己公司内部或 IDC 自建 Openstack、VMware 等环境
  • 混合云:既要使用公有云,又要使用私有云,即自己的私有云的部分业务和公有云有交接,这部分称为混合云

云计算分层:SaaS、PaaS、IaaS

  • IaaS:按需创建主机
  • PaaS:按需创建应用程序并部署应用程序

虚拟化和云计算:

云计算是一种服务模式,虚拟化是一种技术;

虚拟化是云计算的重要支撑技术。

虚拟化
定义 技术 方法
目的 从 1 个物理硬件系统创建多个模拟环境 汇聚并自动化分配虚拟资源以供按需使用
用途 针对具体用途为特定用户提供打包资源 针对多种用途为用户群组提供不同资源
配置 基于镜像 基于模板
使用寿命 数年(长期) 数小时至数月(短期)
成本 资本支出(CAPEX)高、运营支出(OPEX)低 私有云:CAPEX 高、OPEX 低 公共云:CAPEX 低、OPEX 高
可扩展性 纵向扩展 横向扩展
工作负载 有状态 无状态
租赁 单一租户 多个租户

参考:https://www.redhat.com/zh/topics/cloud-computing/cloud-vs-virtualization

虚拟化技术之 KVM 架构和部署

KVM 架构

  • kvm 只提供了三个模块:kvm.ko、kvm_intel.ko、kvm_amd.ko,后两个模块是根据物理主机的 CPU 所属厂家自动匹配的

  • kvm 开发团队借用了 qemu 代码,并作了一些修改,形成了一套工具,也就是 qemu-kvm

  • /dev/kvm 设备是 kvm 虚拟出来的一个设备文件

  • /dev/kvm 只是 kvm 内核模块提供给用户空间的一个接口,这个接口被 qemu-kvm 调用,通过 ioctl 系统调用就可以给用户提供一个工具用以创建,删除,管理虚拟机等

KVM 体系结构:

  • KVM:初始化 CPU 硬件,打开虚拟机模式,负责 CPU、内存、中断控制器、时钟。由内核模块 kvm_xxx.ko 实现,工作在 Hypervisor,设备/dev/kvm,是一个字符设备,在用户空间可以通过 ioctl()系统调用来完成 KVM 创建、启动、为 VM 分配内存、读写 VCPU(虚拟 CPU)的寄存器、向 CPU 注入中断、时钟等管理功能
  • QEMU:工作在用户空间,主要用于实现模拟 IO 设备,如显卡、网卡、硬盘等,qemu-kvm 进程,用于实现一个虚拟机实例
  • libvirt:后文详细介绍

KVM 模块载入后的系统的运行模式:

  • 内核模式:GuestOS 执行 I/O 类操作,或其他的特殊指令的操作;又称为“来宾-内核”模式
  • 用户模式:代表 GuestOS 请求 I/O 类操作
  • 来宾模式:GuestOS 的非 I/O 类操作;有被称为“来宾-用户“模式,称作虚拟机的用户模式更贴切

KVM 的组件:

KVM 功能:

  • 支持 CPU 和 Memory 超分(Overcommit)
  • 支持半虚拟化 I/O (virtio)
  • 支持热插拔 (cpu、块设备、网络设备等)
  • 支持对称多处理(Symmetric Multi-Processing 缩写为 SMP )
  • 支持实时迁移(Live Migration)
  • 支持 PCI 设备直接分配和 单根 I/O 虚拟化 (SR-IOV)
  • 支持内核同页合并 (KSM )
  • 支持 NUMA (Non-Uniform Memory Access 非一致存储访问结构 )

QEMU:

Qemu 是纯软件实现的虚拟化模拟器,几乎可以模拟任何硬件设备,能够模拟一台能够独立运行操作系统的虚拟机,因为 Qemu 是纯软件实现的,所有的指令都要经 Qemu,性能非常低,所以,在生产环境中,大多数的做法都是配合 KVM 来完成虚拟化工作,KVM 完成复杂及要求比较高的设备虚拟化,而 Qemu 完成像鼠标、键盘等设备的虚拟化。

KVM 和 Xen:

总的来说,Xen 性能更高,但是 KVM 更简单易用,所以 KVM 更流行

目前在各大公有云厂商新购买的虚拟机基本运行在 KVM 环境下,就连早期一直使用 Xen 的 AWS 也在 2017 年开始逐渐转换到 KVM 环境

RHEL(CentOS)虚拟设备三种工作模式

Red Hat Enterprise Linux 7 的虚拟化功能为虚拟机提供了三种不同形式的系统设备。这些硬件设备都被显示为物理连接到虚拟机,但设备的驱动以不同方式工作。这三种形式包括:

  • 虚拟和仿真设备
  • 半虚拟化设备
  • 物理共享设备

虚拟和仿真设备:

完全使用软件实现的虚拟化设备

半虚拟化设备:

  • KVM 为虚拟机提供准虚拟化设备,它使用 Virtio API 作为 VMM 和 GuestOS 的中间层
  • virtio 设备由两部分组成:主机设备和客机驱动。半虚拟化设备驱动允许 GuestOS 访问 HostOS 上的物理设备
  • 半虚拟化设备的驱动必须安装在客机操作系统上
  • 当虚拟机运行大小需要密集 I/O 操作的应用程序时,推荐使用半虚拟化设备,而不是使用仿真设备
1
2
3
4
5
6
7
8
半虚拟化网络设备:virtio-net
半虚拟化块设备:virtio-blk
半虚拟化控制器设备:virtio-scsi
半虚拟化时钟:
半虚拟化串口设备:virtio-serial
气球设备:virtio-balloon
半虚拟化随机数字生成器:virtio-rng
半虚拟化图形卡:

透传物理主机设备:

特定硬件平台允许虚拟机直接访问多种硬件设备及组件。在虚拟化中,此操作被称为“设备分配 ”(device assignment)。设备分配又被称作 “传递 ”(passthrough)

1
2
3
4
VFIO 设备分配:
USB 传递:
SR-IOV:
NPIV:

KVM 集中管理和控制

  • oVirt:功能强大,Redhat 虚拟化管理平台 RHEV 的开源版
  • WebVirtMgr:virt-manager 的 Web 模式的替代品
  • OpenStack:最主流的开源虚拟化管理平台
  • Proxmox virtualization environment:简称 PVE,开源免费的基于 linux 的企业级虚拟化方案,可以认为是简化版的 OpenStack

重点学习 OpenStack 和 PVE

安装 KVM 工具包

  • qemu-kvm:为 kvm 提供底层仿真支持
  • libvirt-daemon:libvirtd 守护进程,管理虚拟机
  • libvirt-client:用户端软件,提供客户端管理命令
  • libvirt-daemon-driver-qemu:libvirtd 连接 qemu 的驱动
  • libvirt:提供统一 API,是使用最多的 KVM 虚拟化管理工具,通过 libvirt 调用 KVM 创建虚拟机,其不但能管理 KVM,还能管理 VMware、Xen、Hyper-V、virtualBox 等虚拟化方案
  • virt-manager:图形界面管理工具,其底层是调用 libvirt API 来完成对虚拟机的各种操作
  • virt-install:虚拟机命令行安装工具
  • virsh:基于 libvirt API 创建的命令行工具,它可以作为图形化的 virt-manager 应用的备选工具。
  • virt-viewer:通过 VNC 和 SPICE 协议显示虚拟机器图形控制台的最小工具。该工具来自 virtviewer 程序包
  • cockpit:CentOS8 专门提供的基于 Web 的虚拟机管理界面

libvirt 包功能

libvirt 程序包提供:

  1. 一个稳定的通用层来安全地管理主机上的虚拟机
  2. 一个管理本地系统和联网主机的通用接口

Libvirt API 是相对独立与 VMM 的虚拟化应用程序接口,在 VMM 支持的情况下,调用 Libvirt API 进行部署、创建、修改、监测、控制、迁移以及停止虚拟机等操作。

尽管 libvirt 可同时访问多个主机,但 API 只限于单节点操作。

libvirt 主要的功能是管理单节点主机,列举、监测和使用管理节点上的可用资源,其中包括 CPU、内存、储存、网络和非一致性内存访问(NUMA)分区。

virt-manager 与 virsh 是基于 Libvirt API 构建的高级管理工具。管理工具可以位于独立于主机的物理机上,并通过安全协议和主机进行交流。

libvirt 结构图:

安装 KVM 相关包

  • CentOS
1
2
3
4
5
yum -y install qemu-kvm libvirt virt-manager virt-install  virt-viewer
systemctl start --now libvirtd

# CentOS 8 还提供基于Web的虚拟机管理方式
yum -y install cockpit
  • Ubuntu

官方文档:https://ubuntu.com/server/docs/virtualization-libvirt

1
2
# kvm-ok 验证是否支持kvm,只有Ubuntu支持,CentOS 不支持
sudo apt -y install qemu-kvm virt-manager libvirt-daemon-system

创建虚拟机

三种方式:

  1. virt-manger 工具交互式安装
  2. virt-install 命令安装
  3. 基于现有虚拟机磁盘为模版创建新的虚拟机

重点掌握 virt-install 命令配合 PXE 自动实现自动安装系统

virt-install 命令

1
virt-install --help

virt-install 的选项很多,需要好好研究,但是这里就不一一列举了

qemu-img 命令

利用 qemu-img 命令创建虚拟磁盘,注意: qemu-img create 会覆盖原文件,所以要先检查是否有同名文件存在

1
sudo qemu-img create -f qcow2 /var/lib/libvirt/images/centos7-pxe1.qcow2 20G

范例:virt-install & PXE

HostOS:ubuntu20.04 (物理机)

GuestOS:Centos7

  1. 配置 PXE 环境,参考前面的笔记

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    install
    xconfig --startxonboot
    keyboard --vckeymap=us --xlayouts='us'
    lang en_US.UTF-8
    rootpw --iscrypted $1$YyodG.X7$LzhHqxWONgSh0NBoC730o0
    url --url="http://10.0.0.1/centos/7/os/x86_64"
    auth --useshadow --passalgo=sha512
    text
    firstboot --enable
    selinux --disabled
    skipx
    services --disabled="chronyd"
    ignoredisk --only-use=vda
    firewall --disabled
    network --bootproto=dhcp --device=eth0
    network --hostname=c7
    reboot
    timezone Asia/Shanghai --nontp
    bootloader --append="net.ifnames=0" --location=mbr --boot-drive=vda
    zerombr
    clearpart --all --initlabel
    part /boot --fstype="ext4" --ondisk=vda --size=300
    part pv.01 --size 1 --grow
    volgroup volgrp pv.01
    logvol swap --vgname=volgrp --name=swap --fstype="swap" --size=2048
    logvol / --vgname=volgrp --name=root --fstype="ext4" --size=1 --grow
    %post
    useradd lujinkai
    %end
    %packages
    @^minimal
    @core
    %end
  2. qemu-img 命令创建虚拟磁盘

    1
    sudo qemu-img create -f qcow2 /var/lib/libvirt/images/centos7-pxe1.qcow2 20G
  3. virt-install 命令安装

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    sudo virt-install \
    --virt-type kvm \
    --name centos7-pxe1 \
    --memory 2048 \
    --vcpus 1 \
    --disk path=/var/lib/libvirt/images/centos7-pxe1.qcow2,bus=virtio \
    --network network=default \
    --graphics vnc,listen=0.0.0.0 \
    --location=/usr/local/src/CentOS-7-x86_64-Minimal-2003.iso \
    --extra-args="ks=http://10.0.0.1/ks/centos7.cfg"

    注意: –disk 的 bus 选项一定要指定,因为默认 bus=scsi,而这里直接在 ubutu 物理机中安装,硬盘是 sata,可以指定 bus=sata,不过这样是全虚拟化,而指定 bus=virtio 是半虚拟化,性能更好

  4. 安装成功,初始化配置,这里只说一点:

    关于 ssh 连接慢的问题,宿主机 ubuntu 修改/etc/ssh/ssh_conf:

    1
    2
    3
    4
    5
    GSSAPIAuthentication no

    ServerAliveInterval 55
    ServerAliveCountMax 9

    来宾机 centos 修改/etc/ssh/sshd_conf:

    1
    UseDNS no # 跳过dns验证

范例:基于现有虚拟机磁盘为模版创建新的虚拟机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 先复制
lujinkai@Z510:~$ cd /var/lib/libvirt/images/
lujinkai@Z510:/var/lib/libvirt/images$ sudo cp centos7-pxe1.qcow2 centos7-pxe3.qcow2
# 然后创建
lujinkai@Z510:/var/lib/libvirt/images$ sudo virt-install \
> --virt-type kvm \
> --name centos7-pxe3 \
> --memory 2048 \
> --vcpus 1 \
> --disk bus=virtio,path=/var/lib/libvirt/images/centos7-pxe3.qcow2 \
> --network network=default,model=virtio \
> --graphics vnc,listen=0.0.0.0 \
> --noautoconsole \
> --autostart \
> --boot hd
WARNING 未检测到操作系统,虚拟机性能可能会受到影响。使用 --os-variant 选项指定操作系统以获得最佳性能。

开始安装......
域创建完成。

这样相当于直接拷贝镜像,开机后会发现 ip 冲突,需要手动修改 ip,不过 MAC 没有冲突

管理虚拟机

使用半虚拟化驱动 virtio

为了提高内存、硬盘、网络的性能,需要支持半虚拟化

virtio 工作原理

virtio 是一种 I/O 半虚拟化解决方案,是一套通用 I/O 设备虚拟化的程序,是对半虚拟化 Hypervisor 中的一组通用 I/O 设备的抽象,提供了一套上层应用与各 Hypervisor 虚拟化设备(KVM,Xen,VMware)之间的通信框架和编程接口,减少跨平台所带来的兼容性问题,大大提高驱动程序开发效率,windows 系统需要单独安装 virtio 驱 动,linux 系统自带 virtio 驱动

virtio 使用 virtqueue 来实现 I/O 机制,每个 virtqueue 就是一个承载大量数据的队列,具体使用多少个队列取决于需求,例如,virtio 网络驱动程序(virtio-net)使用两个队列(一个用于接受,另一个用于发送),而 virtio 块驱动程序(virtio-blk)仅使用一个队列

使用统一的 virtio 接口,可以支持多种硬件设备:

  • 不同的虚拟设备和不同的虚拟机可以有不同的前端驱动
  • 不同的硬件设备可以有不同的后端驱动
  • 两者之间的交互遵循 virtio 的标准

virtio 驱动安装

Linux 自带,Windows 需要额外安装

1
2
3
4
5
# 如果有红帽的RHN订阅,可以从以下位置下载virtio-win包
https://rhn.redhat.com/rhn/software/packages/details/Overview.do?pid=868414
# 从社区获得
http://www.linux-kvm.org/page/Downloads
https://docs.fedoraproject.org/en-US/quick-docs/creating-windows-virtual-machines-using-virtio-drivers/index.html

安装与配置 QEMU guest agent

在 GuestOS(VM)中安装 QEMU guest agent,HostOS 就可以调用 libvirt 向 VM 发送命令,例如:“冻结”、“释放文件系统、虚拟 CPU 的热添加及移除等

  • RHEL/CentOS 中有相应的安装包:qemu-guest-agent-xxx.rpm
  • Windows 需要手工安装
1
2
3
4
# 安装
[root@c71 ~]$yum install qemu-guest-agent.x86_64
# 启动
[root@c71 ~]$systemctl start qemu-guest-agent.service
1
2
3
4
5
6
7
8
9
10
virsh shutdown --mode=agen # 比--mode=acpi更加安全地关闭操作系统
virsh snapshot-create -quiesce # 在创建快照之前面,将缓存的内容刷入到磁盘
virsh domfsfreeze # 静默文件系统
virsh domfsthaw # 恢复静默的文件系统
virsh domfstrim # 让虚拟机trim文件系统
virsh domtime # 获得虚拟机的时间
virsh setvcpus # 配置虚拟机的vCPU
virsh domifaddr --source agent # 查询虚拟机的IP地址
virsh domfsinfo # 显示虚拟机的文件系统列表
virsh set-user-password # 设置虚拟机用户的密码

范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
lujinkai@Z510:~$ virsh domfsinfo centos7-pxe1
Mountpoint Name Type Target
------------------------------------
/ dm-0 ext4 vda
/boot vda1 ext4 vda

lujinkai@Z510:~$ virsh domifaddr --source agent centos7-pxe1
Name MAC address Protocol Address
-------------------------------------------------------------------------------
lo 00:00:00:00:00:00 ipv4 127.0.0.1/8
- - ipv6 ::1/128
eth0 52:54:00:5b:08:5d ipv4 10.0.0.71/24
- - ipv6 fe80::5054:ff:fe5b:85d/64

安装和配置 SPICE agent

在 VM 操作系统中安装 SPICE client、SPICE agent 可以让 virt-manager 等图形应用程序更加流畅

  • 在 virt-manager 中调整窗口尺寸,SPICE agent 自动调整 X 会话的分辨率
  • 在 Host 与 Guest 之间复制与粘贴
  • 防止鼠标拖尾等

libvirt 管理虚拟机

主要通过 virsh 和 virst-manager 这两个工具调用 libvirt 来实现虚拟机的管理

注意: 如果 libvirtd 服务意外关闭,将导致相关工具,如 virt-manager 和 virsh 等无法和虚拟机连接,但虚拟机仍会正常运行

1
systemctl start libvirtd.service

virsh 命令

virsh 可以认为是 virt-manager 的命令行工具

两种工作模式:交互式和非交互式,重点掌握非交互式

1
2
virsh --help # 查看帮助
virsh help command # 查看子命令帮助
  • 启动和关闭虚拟机
1
2
3
4
virsh list      # 查看运行中的虚拟机
virsh list --all # 查看所有虚拟机
virsh start # 启动虚拟机
virsh shutdown # 关闭虚拟机
  • 暂停和恢复虚拟机

暂停时,将内存、寄存器,缓存等全部状态保存到硬盘
恢复时,将保存下来的数据读取回去,所以就能按原来的状态运行了

1
2
virsh suspend  # 暂停虚拟机
virsh resume # 回复虚拟机
  • 配置虚拟机开机自启动:
1
virsh autostart centos7-pxe1
  • 查看虚拟机配置:

每个虚拟机的配置默认存放在 /etc/libvirt/qemu 目录下的 xml 文件中

查看虚拟机配置:相当于查看 /etc/libvirt/qemu/centos7-pxe1.xml 文件

1
virsh dumpxml --domain centos7-pxe1
  • 删除虚拟机配置:

删除虚拟机配置(qemu/xxx.xml),但是不删除磁盘文件(images/xxx.qcow2)

1
virsh undefine centos7-pxe1

存储管理

KVM 中,存储主要有两个概念:

  1. 存储池
  2. 存储卷

存储池

libvirt 以存储池的形式对存储进行统一管理,简化操作

存储池有很多种类型,这取决于存储池是基于什么“介质”创建的,以下是存储池支持的“介质”,分为两种:文件系统 和 磁盘:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 文件系统:
dir:文件系统目录,就是一个目录
fs:预格式化设备,就是一个创建了文件系统的分区,例如:/dev/sdb6
netfs:网络导出的目录,例如 nfs、samba

# 磁盘:
disk:物理磁盘设备,直接读写硬盘设备,例如: /dev/sdb
scsi:本地SCSI存储,例如:host0(/sys/class/scsi_host/host0)
iscsi:网络共享iSCSI存储
logical:LVM卷组,最常用,推荐

# 不太清楚文件系统还是磁盘:
gluster:Gluster 文件系统
mpath:多路径设备枚举器
rbd:RADOS 设备块/Ceph
sheepdog:Sheepdog文件系统,分布式文件系统
zfs:ZFS pool

存储卷

存储卷在存储池中,存储卷的类型取决于存储池的类型:

如果存储池基于文件系统,其中的存储卷就是文件,例如 dir、fs、netfs;
如果存储池是基于磁盘,其中的存储卷就是其对应的数据块,例如 logical 对应逻辑卷、disk 对应分区;

qcow2 和 lv

稀疏文件:从表现形式上来看,当 ls 和 du 命令查看文件,显示的大小不一样,那这个文件就是稀疏文件

虚拟机可以安装在不同的介质上,重点掌握两种:磁盘镜像 和 逻辑卷

  • 磁盘镜像

磁盘镜像就是个文件,可以存储在各种文件系统中

磁盘镜像的格式有好几种,最常用的是 qcow2 格式,qcow2 格式的镜像文件功能丰富,支持如快照、压缩及加密等功能,缺点就是性能不太好

上文创建虚拟机用的就是磁盘镜像

  • 逻辑卷

虚拟机直接安装在磁盘,性能最好,LVM 性能稍差,但是更加灵活,所以推荐 LVM,将虚拟机安装在逻辑卷中

使用 virt-manager 将虚拟机安装在逻辑卷,关键点在 step 4,选择自定义的存储,即选择逻辑卷

安装完后启动可能会报错,重启 libvirt 就好了

virt-install 安装虚拟机到逻辑卷,和前面安装虚拟机到磁盘镜像中的区别就是–disk 选项,其他都一样:

1
--disk path=/dev/vm_images_lvm/lv2,bus=virtio \

离线工具

不启动虚拟机的情况下,直接访问管理虚拟机对应的磁盘,即离线访问

离线工具应用

  • 拯救和修复客户无法启动或需要更改启动配置的虚拟机
  • 准备新的磁盘映像,其中包含文件、目录、文件系统、分区、逻辑卷和其他选项
  • 查看或下载虚拟机磁盘中的文件;编辑或上传文件到虚拟机磁盘
  • 通过克隆和修改模板来部署虚拟机
  • 读取或写入虚拟机配置
  • 监控虚拟机的磁盘使用情况

guestfs

guestfish 是一个基于 libguestfs API 的交互 shell

libguestfs 提供了一个简单地访问虚机磁盘镜像文件的方法,即使是在虚拟机无法启动的情况下

libguestfs 是由一组丰富的工具集组成,可以让管理员访问虚机文件,甚至调整和挽救文件

1
2
3
4
5
6
7
8
9
10
11
dnf -y install libguestfs-tools

# 查看虚拟机的磁盘文件
virsh domblklist centos7

# 只读方式打开虚拟磁盘,选项-i可以实现自动探查后进行自动挂载
guestfish --ro -a /var/lib/libvirt/images/centos7.qcow2 -i # 交互式

# 读写方式打开虚拟磁盘
guestfish -d centos7 -i # 交互式

其他离线工具

1
2
3
4
5
virt-df    # 监视磁盘使用
virt-resize # 离线调整虚拟磁盘大小
virt-inspector # 虚拟机检视
virt-win-reg # Windows注册表读取和修改
virt-sysprep # 虚拟机设置重置

网络管理

官方文档:https://wiki.libvirt.org/page/VirtualNetworking

nmcli 实现网桥

1
2
3
4
5
6
7
8
9
10
11
12
# 创建网桥
nmcli connection add type bridge con-name br0 ifname br0
nmcli connection modify br0 ipv4.addresses 10.0.0.100/24 ipv4.method manual
nmcli connection reload
nmcli connection up br0

#加入物理网卡
nmcli con add type bridge-slave con-name br0-port0 ifname eth0 master br0
nmcli con add type bridge-slave con-name br0-port1 ifname eth1 master br0
nmcli connection reload
nmcli con up br0-port0
nmcli con up br0-port1

qemu-kvm 支持的网络

默认网络架构

默认虚拟机网络配置为 NAT 模式,相当于 wmware 的 ANT 模式

自定义网桥架构

自定义网桥架构:桥接网络可以让运行在宿主机上的虚拟机使用和宿主机相同网段的 IP,并且可以从外部直接访问到虚拟机,目前企业中大部分场景都使用桥接网络

网桥(或者交换机)一定是有网卡接口的,给网卡接口配置 ip,才能实现桥接的功能,默认的 nat 模式下,虚拟网桥 virbr0 的网卡接口是 virbr0-nic;自定义网桥架构中,网卡接口是 eth0

ip a显示 nat 模式下,virbr0-nic 没有 ip,自定义网桥模式下,eth0 没有 ip,而虚拟网桥 virbr0 和 virbr1 显示有 ip,这时为什么呢? 我猜这是因为虽然我们常说给网卡配置 ip,但其实 ip 是配置在内核的,所以不论显示在哪里,都是网桥的 ip,形式不重要

用户自定义的隔离的虚拟网络

实战案例

集群和分布式

集群(cluster):为了解决某个特定问题将多台服务器组合起来形成的单个系统

cluster 分为三种类型:负载均衡(LB)、高可用(HA)、高性能(HPC,例如天河一号)

集群和分布式:

集群是一个业务系统部署在多台服务器上,分布式是拆分业务系统,然后分别部署在不同服务器

分布式是指通过网络连接的多个组件,通过交换信息协作而形成的系统。而集群,是指同一种组件的多个实例,形成的逻辑上的整体

cluster 负载均衡 LB

按实现方式:

  • 硬件:F5,非常昂贵,一个 F5 就 18W,配置一套主从高可用就是 36W
  • 软件:LVS、Nginx、haproxy

基于工作的协议层划分:

  • 传输层:LVS、Nginx、haproxy
  • 应用层:针对特定协议,常称为 proxy server
    • http:nginx、httpd、haproxy…
    • fastcgi:nginx、httpd…
    • mysql:mysql-proxy、mycat…

cluster 高可用集群 HA

keepalived

LVS 简介

Linux Virtual Server,官网: http://www.linuxvirtualserver.org/

四层负载均衡,转发效率极高,具有处理百万计并发连接请求的能力。

阿里的四层 SLB ( Server Load Balance )是基于 LVS + keepalived 实现

LVS = IPVS(内核空间) + ipvsadm(用户空间)

  • IPVS:集成在内核,工作在 netfilter 框架上,具体来讲就是工作在 INPUT 链上,将发往 INPUT 的流量进行 “处理”
  • ipvsadm:管理 ipvs 规则的用户空间软件

负载均衡的应用场景为高访问量的业务,提高应用程序的可用性和可靠性

  1. 应用于高访问量的业务
  2. 扩展应用程序:随时添加和移除 ECS 实例来扩展应用系统的服务能力
  3. 消除单点故障
  4. 同城容灾 (多可用区容灾)
  5. 跨地域容灾

LVS 集群

LVS 集群体系架构图

术语

  • CIP:Client IP
  • VS:Virtual Server;Director Server(DS)、Dispatcher(调度器)、Load Balancer
  • RS:Real Server;upstream server(nginx)、backend server(haproxy)
  • DIP:Director IP,VS 内网 IP
  • VIP:Virtual server IP,VS 和 RS 的外网 IP
  • RIP:Real server IP,RS 的 IP

访问流程:CIP <–> VIP==DIP <–> RIP

IPVS 工作模式

  • nat:修改请求报文的目标 IP
  • fullnat:修改请求报文的源和目标 IP,默认不支持
  • dr:操纵封装新的 mac 地址
  • tun:在原请求 IP 报文之外新加一个 IP 首部

nat

本质是多目标 IP 的 DNAT,通过将请求报文中的目标地址和目标端口修改为某挑出的 RS 的 RIP 和 PORT 实现转发

缺点:请求报文和响应报文都必须经由 VS 转发,VS 容易成为系统瓶颈

fullnat

lvs-nat 模式下,DIP 和 RIP 应该在同一个 IP 网段,RS 的网关要指向 DIP

而 lvs-fullnat 模式下,DIP 和 RIP 通常不在一个网段,只要能通信就行,当然 RS 的网关也不指向 DIP

注意:此类型 kernel 默认不支持

dr (重点)

Direct Routing 直接路由,LVS 默认模式,应用也最广泛,重点掌握。

通过为请求报文重新封装一个 MAC 首部进行转发,源 MAC 是 DIP 的 MAC,目标 MAC 是一个挑选出的 RS 的 RIP 的 MAC 地址;源 IP/PORT 以及目标 IP/PORT 均保持不变

lvs-dr 模式的特点:

  • Director 和各 RS 都配置同一个公网 IP(VIP),通常配置在 lo 上

  • RS 和 Director 要在同一个物理网络(同一个机房)

  • 请求报文要经由 Director,但响应报文不经由 Director,而由 RS 直接发往 Client

  • 所以为了保证目标为 VIP 的请求发往 VS 而不是 RS,需要在 RS 上修改 arp_ignore 和 rp_announce 参数,从而隐藏 RS

  • RIP 和 DIP 在同一 IP 网段,RS 网关不能指向 DIP(避免响应报文经过 VS)

  • 不支持端口映射

  • 无需开启 ip_forward

tun

一般来说,TUN 模式常会用来负载调度缓存服务器组,这些缓存服务器一般放置在不同的网络环境,可以就近折返给客户端。在请求对象不在 Cache 服务器本地命中的情况下,Cache 服务器要向源服务器发送请求,将结果取回,最后将结果返回给用户。

LAN 环境一般多采用 DR 模式,WAN 环境虽然可以用 TUN 模式,但是一般在 WAN 环境下,请求转发更多的被 haproxy、nginx、DNS 等实现。因此,TUN 模式实际应用的很少,跨机房的应用一般专线光纤连接或 DNS 调度

总结和比较

NAT TUN DR
Real Server any Tunneling Non-arp device
Real server network private LAN/WAN LAN
Real server number low (10~20) High (100) High (100)
Real server gateway load balancer own router own router
优点 端口转换 WAN 性能最好
缺点 性能瓶颈 支持隧道 不支持跨网段

IPVS 负载均衡调度算法

根据其调度时是否考虑各 RS 当前的负载状态,分为两种:静态方法和动态方法

静态算法

仅根据算法本身进行调度

静态算法 说明
RR roundrobin 轮询,常用
WRR weighted rr 加权轮询,常用
SH 源 IP hash,将来自于同一个 IP 地址的请求始终发往第一次挑中的 RS,从而实现会话绑定
DH 目标 IP hash,第一次轮询调度至 RS,后续将发往同一个目标地址的请求始终转发至第一次挑中的 RS,典型使用场景是正向代理缓存场景中的负载均衡,如:web 缓存

动态算法

主要根据每 RS 当前的负载状态及调度算法进行调度 Overhead=value,较小的 RS 将被调度

动态算法 说明
LC least connections 适用于长连接应用
WLC Weighted LC 默认调度方法,较常用
SED Shortest Expection Delay 初始连接高权重优先,只检查活动连接,而不考虑非活动连接
NQ Never Queue 第一轮均匀分配,后续 SED
LBLC Locality-Based LC,动态的 DH 算法,使用场景:根据负载状态实现正向代理,实现 Web Cache 等
LBLCR LBLC with Replication 带复制功能的 LBLC,解决 LBLC 负载不均衡问题,从负载重的复制到负载轻的 RS,实现 Web Cache 等
FO 内核 4.15 之后新增
OVF 内核 4.15 之后新增

ipvsadm

ipvsadm 的核心功能:管理集群服务 和 集群服务的 RS

1
2
3
4
5
6
7
8
[root@c71 ~]$yum -y install ipvsadm
[root@c71 ~]$rpm -ql ipvsadm
/etc/sysconfig/ipvsadm-config # 配置文件
/usr/lib/systemd/system/ipvsadm.service #
/usr/sbin/ipvsadm # 主程序
/usr/sbin/ipvsadm-restore # 规则重载工具
/usr/sbin/ipvsadm-save # 规则保存工具
...

管理集群服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ipvsadm -A|E -t|u|f service-address [-s scheduler] [-p [timeout]] [-M netmask] [--pe persistence_engine] [-b sched-flags]

-A # add
-E # edit

service-address # vip:port 如:10.0.0.100:80
-t # tcp协议
-u # udp协议
-f # firewall mark,标记,一个数字

-s # 指定集群的调度算法,默认为wlc

# 范例
ipvsadm -A -t 10.0.0.100:80 -s wrr
1
2
3
4
ipvsadm -D -t|u|f service-address  # 删除
ipvsadm –C # 清空定义的所有内容
ipvsadm –R # 重载,相当于ipvsadm-restore
ipvsadm -S [-n] # 保存,相当于ipvsadm-save

管理集群上的 RS

1
2
3
4
5
6
7
8
9
10
11
12
ipvsadm -a|e -t|u|f service-address -r server-address [-g|i|m] [-w weight]

-a # add
-e # edit
-r # rip[:port] 如省略port,不作端口映射
-g # gateway,dr类型,默认
-i # ipip,tun类型
-m # masquerade,nat类型
-w # 权重

# 范例
ipvsadm -a -t 10.0.0.100:80 -r 10.0.0.8:8080 -m -w 3
1
2
ipvsadm -d -t|u|f service-address -r server-address  # 删除
ipvsadm -Z [-t|u|f service-address] # 清空计数器
1
2
3
4
5
6
7
8
ipvsadm -L|l [options] # 查看

options:
-n: 以数字形式输出地址和端口号
--exact: 扩展信息,精确值
-c: 当前IPVS连接输出
--stats: 统计信息
--rate: 输出速率信息

ipvs 规则

1
/proc/net/ip_vs

ipvs 连接

1
/proc/net/ip_vs_conn

保存规则

ipvs 调度规则建议保存至 /etc/sysconfig/ipvsadm.rules

1
2
3
ipvsadm-save > /etc/sysconfig/ipvsadm.rules
ipvsadm -S > /etc/sysconfig/ipvsadm.rules # ipvsadm -S 和 ipvsadm-save等价
systemctl stop ipvsadm.service # 会自动保存规则至/etc/sysconfig/ipvsadm.rules

重载

1
2
ipvsadm-restore < /etc/sysconfig/ipvsadm.rules
systemctl start ipvsadm.service # #会自动加载/etc/sysconfig/ipvsadm.rules中规则

防火墙标记

FWM:FireWall Mark

借助防火墙标记来分类报文,而后基于标记定义集群服务,可将多个不同的应用使用同一个集群服务进行调度

实现方法:

1
2
3
4
5
# 1. 在VS主机打标记
iptables -t mangle -A PREROUTING -d $vip -p $proto -m multiport --dports $port1,$port2,... -j MARK --set-mark NUMBER

# 2. 在VS主机基于标记定义集群服务
ipvsadm -A -f NUMBER [options]

范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@c71 ~]$iptables -t mangle -A PREROUTING -d 172.16.0.100 -p tcp -m multiport --dports 80,443 -j MARK --set-mark 10
[root@c71 ~]$ipvsadm -C
[root@c71 ~]$ipvsadm -A -f 10 -s rr
[root@c71 ~]$ipvsadm -a -f 10 -r 10.0.0.72 -g
[root@c71 ~]$ipvsadm -a -f 10 -r 10.0.0.73 -g
[root@c71 ~]$ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
FWM 10 rr
-> 10.0.0.72:0 Route 1 0 0
-> 10.0.0.73:0 Route 1 0 0
[root@c71 ~]$cat /proc/net/ip_vs
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
FWM 0000000A rr
-> 0A000049:0000 Route 1 0 0
-> 0A000048:0000 Route 1 0 0

持久标记

session 绑定:对共享同一组 RS 的多个集群服务,需要统一进行绑定,IPVS 调度算法无法实现

持久连接模板:无论使用任何调度算法,在一段时间内(默认 360s),能够实现将来自同一地址的请求始终发往同一个 RS

实现方法:ipvsadm 指定 -p 选项即可

1
2
3
4
5
ipvsadm -A|E -t|u|f service-address [-s scheduler] -p [timeout] [-M netmask] [--pe persistence_engine] [-b sched-flags]

# 范例:
ipvsadm -A|E -f 10 -p
ipvsadm -A|E -f 10 -p 3600

LVS 实战案例

重点掌握 LVS-DR 模式即可,其他模式如果用到再复习

LVS-DR 模式单网段案例

DR 模式中各主机上均需配置 VIP,解决地址冲突的方式有三种,最常用的是修改 RS 的内核参数:

  • arp_ignore:用于处理接收到的 arp 请求,

    • 设置为 1:请求的目的 IP 地址为接收网卡上的 IP,才响应。

      举例来讲:服务器上配置两个网卡 A 和 B,A 接收到 arp 请求,只要请求的目的 IP 不是 A 的 IP 就不响应。

  • arp_announce:用于处理发送的 arp 请求

    • 设置为 2:请求的源 IP 为发送网卡的 IP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
环境:五台主机

一台客户机:eht0:仅主机 192.168.10.6/24 GW:192.168.10.200

一台路由器:
eht0:NAT 10.0.0.200/24
eht1:仅主机 192.168.10.200/24
启用ip_forward

# dr模式下,vs只负责转发请求,不往外网发送请求,所以网关可以随便设置一个ip,但是不能不配置,因为如果不配置,vs会认为自己和外网没有通信的可能,就不会转发请求了
一台VS:eht0:NAT 10.0.0.8/24 GW:10.0.0.200 lo:10.0.0.100/32

两台RS:
RS1:eth0:NAT 10.0.0.7/24 GW:10.0.0.200 lo:10.0.0.100/32
RS2:eth0:NAT 10.0.0.17/24 GW:10.0.0.200 lo:10.0.0.100/32
  • RS 要可以出网,所以配置了网关

  • VIP 是 10.0.0.100,给 VS 和 RS 的 lo 网卡都配置上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 客户机配置过程略。。。
# 路由器配置过程略。。。
# RS配置:将arp_ignore设置为1、arp_announce设置为2;lo的ip配置为VIP
echo 1 > /proc/sys/net/ipv4/conf/all/arp_ignore
echo 1 > /proc/sys/net/ipv4/conf/lo/arp_ignore
echo 2 > /proc/sys/net/ipv4/conf/all/arp_announce
echo 2 > /proc/sys/net/ipv4/conf/lo/arp_announce
ip a add 10.0.0.100/32 dev lo label lo:1
# VS配置:lo的ip配置为VIP,配置ipvs规则
ipvsadm -A -t 10.0.0.100:80 -s rr
ipvsadm -a -t 10.0.0.100:80 -r 10.0.0.7:80 -g
ipvsadm -a -t 10.0.0.100:80 -r 10.0.0.17:80 -g

# 测试访问:客户机多次执行 curl 10.0.0.100

LVS-DR 模式多网段案例

前面的单网段案例中,lo 和 eth0 在同一个网段,如果把 lo 和 eth0 的 ip 设置在不同的网段就是多网段,这种情况是物理上单网段,逻辑上多网段

实现过程略。。。

LVS 高可用实现

DS 不可用时

  • keepalived
  • heartbeat/corosync

RS 不可用时

某 RS 不可用时,Director 依然会调度请求至此 RS

解决方案: 由 Director 对各 RS 健康状态进行检查,失败时禁用,成功时启用

常用解决方案:

  • keepalived
  • heartbeat/corosync
  • ldirectord

检测方式:

  • 网络层检测:icmp
  • 传输层检测:端口探测
  • 应用层检测:请求某关键资源

ldirectord

pure-ftpd.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# 限制所有用户在其主目录中,change root directory(更改根目录),参考chroot命令
ChrootEveryone yes
# 如果前一个指令被设置为了 "no",下面组的成员(GID)就不受主目录的限制了。而其他的用户还是会被限制在自己的主目录里。如果你不想把任何用户限制在自己的主目录里,只要注释掉 ChrootEveryone 和 TrustedGID 就可以了
# TrustedGID 100
BrokenClientsCompatibility no # 兼容ie等比较非正规化的ftp客户端
MaxClientsNumber 50 # 服务器总共允许同时连接的最大用户数
Daemonize yes # 做为守护(doemon)进程运行(Fork in background)
# 同一IP允许同时连接的用户数(Maximum number of sim clients with the same IP address)
MaxClientsPerIP 8
VerboseLog no # 如果你要记录所有的客户命令,设置这个指令为 "yes"
DisplayDotFiles yes # 即使客户端没有发送 '-a' 选项也列出隐藏文件
AnonymousOnly no # 是否只让匿名用户登录
NoAnonymous no # 不允许匿名连接,仅允许认证用户使用
SyslogFacility ftp # 缺省的facility 是 "ftp"。 "none" 将禁止日志
# 定制用户登陆后的显示信息(Display fortune cookies)
# FortunesFile /usr/share/fortune/zippy
# 在日志文件中不解析主机名。日志没那么详细的话,就使用更少的带宽。在一个访问量很大的站点中,设置这个指令为 "yes" ,如果你没有一个能工作的DNS的话
DontResolve yes
MaxIdleTime 15 # 客户端允许的最大的空闲时间(分钟,缺省15分钟)
# LDAPConfigFile /etc/pureftpd-ldap.conf # LDAP配置文件 (参考README.LDAP)
# MySQLConfigFile /etc/pureftpd-mysql.conf # MySQL配置文件 (参考README.MySQL)
# PGSQLConfigFile /etc/pureftpd-pgsql.conf # Postgres配置文件 (参考README.PGSQL)
# PureDB /etc/pureftpd.pdb # PureDB用户数据库 (参考README.Virtual-Users)
# ExtAuth /var/run/ftpd.sock # pure-authd 的socket 路径(参考README.Authentication-Modules)
# 如果你要启用 PAM 认证方式, 去掉下面行的注释
# PAMAuthentication yes
# 如果你要启用 简单的 Unix系统 认证方式(/etc/passwd), 去掉下面行的注释,就是允许系统用户登录
# UnixAuthentication yes
# 'ls' 命令的递归限制。第一个参数给出文件显示的最大数目。第二个参数给出最大的子目录深度
LimitRecursion 10000 8
AnonymousCanCreateDirs no # 允许匿名用户创建新目录
# 如果系统被 loaded 超过下面的值,匿名用户会被禁止下载
MaxLoad 4
# PassivePortRange 30000 50000 # 被动连接响应的端口范围
# ForcePassiveIP 192.168.0.1 # 强制一个IP地址使用被动响应
# AnonymousRatio 1 10 # 匿名用户的上传/下载的比率
# UserRatio 1 10 # 所有用户的上传/下载的比率
# 不接受所属者是 "ftp" 的文件的下载。例如:那些匿名用户上传后未被本地管理员验证的文件
AntiWarez yes
# Bind 127.0.0.1,21 # 服务监听的IP 地址和端口。(缺省是所有IP地址和21端口)
# AnonymousBandwidth 8 # 匿名用户的最大带宽(KB/s)
# UserBandwidth 8 # 所有用户的最大带宽(KB/s),包括匿名用户
# 新建目录及文件的属性掩码值:<文件掩码>:<目录掩码>,如果不放心,就直接设置177:077
Umask 133:022
MinUID 100 # 认证用户允许登陆的最小组ID(UID)
AllowUserFXP no # 仅允许认证用户进行 FXP 传输
AllowAnonymousFXP no # 对匿名用户和非匿名用户允许进行匿名 FXP 传输
# 用户不能删除和写点文件(以 '.' 开头的文件),即使用户是文件的所有者也不行
ProhibitDotFilesWrite no
# 禁止读点文件(文件名以 '.' 开头的文件)
ProhibitDotFilesRead no
# 永不覆盖文件。当上传的文件,其文件名已经存在时,自动重命名,如: file.1, file.2, file.3, ...
AutoRename no
AnonymousCantUpload no # 不接受匿名用户上传新文件( no = 允许上传)
# 仅允许来自以下IP地址的非匿名用户连接。你可以使用这个指令来打开几个公网IP来提供匿名FTP,而保留一个私有的防火墙保护的IP来进行远程管理。你还可以只允许一内网地址进行认证,而在另外一个IP上提供纯匿名的FTP服务。
# TrustedIP 10.1.1.1
# 如果你要为日志每一行添加 PID 去掉下面行的注释。
# LogPID yes

# 使用类似于 Apache的格式创建一个额外的日志文件,如:
# fw.c9x.org - jedi [13/Dec/1975:19:36:39] "GET /ftp/linux.tar.bz2" 200 21809338
# 这个日志文件能被 www 流量分析器处理
# AltLog clf:/var/log/pureftpd.log
# 使用优化过的格式为统计报告创建一个额外的日志文件
# AltLog stats:/var/log/pureftpd.log
# 使用标准的W3C格式创建一个额外的日志文件。(与大部分的商业日志分析器兼容)
# AltLog w3c:/var/log/pureftpd.log
# NoChmod yes # 不接受 chmod 命令。用户不能更改他们文件的属性
# KeepAllFiles yes # 允许用户恢复和上传文件,却不允许删除他们
# CreateHomeDir yes # 用户主目录不存在的话,自动创建
# 启用虚拟的磁盘限额。第一个数字是最大的文件数,第二个数字是最大的总的文件大小(单位:Mb),所以,1000:10 就是限制每一个用户只能使用 1000 个文件,共10Mb
# Quota 1000:10
# 如果你的 pure-ftpd 编译时加入了独立服务器( standalone 支持,你能够改变 pid 文件的位置。缺省位置是 /var/run/pure-ftpd.pid
# PIDFile /var/run/pure-ftpd.pid

# 如果你的pure-ftpd编译时加入了pure-uploadscrīpt支持,这个指令将会使pure-ftpd发送关于新上传的情况信息到/var/run/pure-ftpd.upload.pipe,这样 pure-uploadscrīpt就能读然后调用一个脚本去处理新的上传
# CallUploadScript yes
# 这个选项对允许匿名上传的服务器是有用的。当 /var/ftp 在 /var 里时,需要保留一定磁盘空间来保护日志文件。当所在磁盘分区使用超过百分之 X 时,将不在接受新的上传
MaxDiskUsage 99
# NoRename yes # 如果你不想要你的用户重命名文件的话,就设置为 'yes'
# 如果你所有的用户都有基本的Unix知识的话,这个特性将没什么用了。不过,如果你是一个主机提供商的话,启用它
CustomerProof yes
# 每一个用户的并发限制。只有在添加了 --with-peruserlimits 编译选项进行编译后,这个指令才起作用。(大部分的二进制的发布版本就是例子),格式是 : <每一个用户最大允许的进程>:<最大的匿名用户进程>,例如: 3:20 意思是同一个认证用户最大可以有3个同时活动的进程。而且同时最多只能有20个匿名用户进程
# PerUserLimits 3:20

# NoTruncate yes
# TLS 1
# TLSCipherSuite HIGH
# CertFile /etc/ssl/private/pure-ftpd.pem
# CertFileAndKey "/etc/pure-ftpd.pem" "/etc/pure-ftpd.key"
# ExtCert /var/run/ftpd-certs.sock
# IPV4Only yes
# IPV6Only yes

请注意:LDAPConfigFile, MySQLConfigFile, PAMAuthentication 和 UnixAuthentication 这些指令只能被使用一次,不过,他们能被混合在一起用。例如:如果你使用了 MySQLConfigFile 和 UnixAuthentication,那么 SQL 服务器将被访问。如果因为用户名未找到而使 SQL 认证失败的话,就会在/etc/passwd 和 /etc/shadow 中尝试另外一种认证,如果因为密码错误而使 SQL 认证失败的话,认证就会在此结束了。认证方式由它们被给出来的顺序而被链接了起来

命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
lujinkai@Z510:/usr/local/pureftpd$ ll bin/ sbin/
bin/:
总用量 100
drwxr-xr-x 2 root root 4096 11月 11 11:04 ./
drwxr-xr-x 6 root root 4096 11月 11 11:04 ../
-rwxr-xr-x 1 root root 50248 11月 11 11:04 pure-pw*
-rwxr-xr-x 1 root root 17400 11月 11 11:04 pure-pwconvert*
-rwxr-xr-x 1 root root 17416 11月 11 11:04 pure-statsdecode*

sbin/:
总用量 364
drwxr-xr-x 2 root root 4096 11月 11 11:04 ./
drwxr-xr-x 6 root root 4096 11月 11 11:04 ../
-rwxr-xr-x 1 root root 17200 11月 11 11:04 pure-authd*
-rwxr-xr-x 1 root root 28320 11月 11 11:04 pure-certd*
-rwxr-xr-x 1 root root 213928 11月 11 11:04 pure-ftpd*
-rwxr-xr-x 1 root root 17248 11月 11 11:04 pure-ftpwho*
-rwxr-xr-x 1 root root 17424 11月 11 11:04 pure-mrtginfo*
-rwxr-xr-x 1 root root 27768 11月 11 11:04 pure-quotacheck*
-rwxr-xr-x 1 root root 27904 11月 11 11:04 pure-uploadscript*
lujinkai@Z510:
lujinkai@Z510:/usr/local/pureftpd$ ll share/man/man8/
总用量 84
drwxr-xr-x 2 root root 4096 11月 11 11:04 ./
drwxr-xr-x 3 root root 4096 11月 11 11:04 ../
-rw-r--r-- 1 root root 4146 11月 11 11:04 pure-authd.8
-rw-r--r-- 1 root root 2889 11月 11 11:04 pure-certd.8
-rw-r--r-- 1 root root 29533 11月 11 11:04 pure-ftpd.8
-rw-r--r-- 1 root root 2425 11月 11 11:04 pure-ftpwho.8
-rw-r--r-- 1 root root 2101 11月 11 11:04 pure-mrtginfo.8
-rw-r--r-- 1 root root 3280 11月 11 11:04 pure-pw.8
-rw-r--r-- 1 root root 781 11月 11 11:04 pure-pwconvert.8
-rw-r--r-- 1 root root 2164 11月 11 11:04 pure-quotacheck.8
-rw-r--r-- 1 root root 1125 11月 11 11:04 pure-statsdecode.8
-rw-r--r-- 1 root root 4156 11月 11 11:04 pure-uploadscript.8

pure-pw

Manage virtual users files for Pure-FTPd.

Virtual users is a simple mechanism to store a list of users, with their password, name, uid, directory, etc. It’s just like /etc/passwd. But it’s not /etc/passwd. It’s a different file, only for FTP.
It means that you can easily create FTP-only accounts without messing your system accounts.
Additionnaly, virtual users files can store individual quotas, ratios, bandwidth, etc. System accounts can’t do this.
Thousands of virtual users can share the same system user, as long as they all are chrooted, and they have their own home directory.

pure-pwconvert

Generate a virtual users file from system accounts.

This program scans system accounts (/etc/passwd) and outputs a FTP virtual users list, suitable to the pure-pw command.

pure-statsdecode

Show human-readable dates from a “stats” logfile.

This program decodes Pure-FTPd’s “stats” log files and converts timestamps into human-readable dates.

pure-authd

External authentication agent for Pure-FTPd

pure-authd is a daemon that forks an authentication program, waits for an authentication reply, and feed them to an application server.
pure-authd listens to a local Unix socket. A new connection to that socket should feed pure-authd the following structure:
​ account:xxx
​ password:xxx
​ localhost:xxx
​ localport:xxx
​ peer:xxx

pure-certd

TLS certificate agent for Pure-FTPd.

pure-certd is a daemon that forks an authentication program, waits for a certificate path as a reply, and returns it to an application server.
pure-certd listens to a local Unix socket. A new connection to that socket should send pure-authd the following structure:
sni_name:xxx end
These content is passed to the authentication program, as an environment variable:
CERTD_SNI_NAME
The authentication program should take appropriate actions to select a TLS certificate, and reply to the standard output with the fol‐lowing format:
action:strict cert_file:/path/to/cert.pem key_file:/path/to/cert.pem end

pure-ftpd

simple File Transfer Protocol server

Pure-FTPd is a small, simple server for the old and hairy File Transfer Protocol, designed to use less resources than older servers,be smaller and very secure, and to never execute any external program.
It support most-used features and commands of FTP (including many modern extensions), and leaves out everything which is deprecated,meaningless, insecure, or correlates with trouble.
IPv6 is fully supported.

pure-ftpwho

Report current FTP sessions.

pure-ftpwho shows current Pure-FTPd client sessions. Only the system administrator may run this. Output can be text (default), HTML, XML data and parser-optimized. The server has to be compiled with –with-ftpwho to support this command.

pure-mrtginfo

provide an MRTG-graphable user count for ftpd.

Pure-Mrtginfo counts the number of clients currently connected to ftpd(8) and output the format in a format graphable by MRTG.

pure-quotacheck

Update virtual quota files for Pure-FTPd.

pure-quotacheck create a .ftpquota file in the specified directory.
This file contains the current file and size of the directory, and it is used by Pure-FTPd when virtual quotas are enabled.
It’s recommended to periodically run pure-quotacheck for every user, in crontabs.

pure-uploadscript

Automatically run an external program after a successful upload.

If Pure-FTPd is compiled with –with-uploadscript (default in binary distributions), and if the -o (or –uploadscript) is passed to the server, a named pipe called /var/run/pure-ftpd.upload.pipe is created. You will also notice an important file called /var/run/pure-ftpd.upload.lock, used for locking.
After a successful upload, the file name is written to the pipe.
pure-uploadscript reads this pipe to automatically run any program or script to process the newly uploaded file.