CLion调试Linux Kernel

1. 编译Kernel

先按照https://www.kernel.org/doc/html/latest/dev-tools/gdb-kernel-debugging.html说明的config设定编译内核并且安装到guest上。

menuconfig中按“/”是可以搜索的,例如要找“CONFIG_GDB_SCRIPTS”,按“/”输入“GDB_SCRIPTS”。

qemu启用了virtio的话,记得选上VIRTIO_BLK和VIRTIO_NET。

2. QEMU

2.1 关闭KVM

关闭KVM,关闭KVM,关闭KVM,重要的事情说三次!不然step by step的时候,会无限跳到arch/x86/kernel/apic/apic.c的sysvec_apic_timer_interrupt。要关闭KVM,运行qemu时去掉–enalbe-kvm参数,用libvirt的话,domain的type值置qemu:

2.2 启用gdbserver

运行QEMU时加上参数-s,或者通过“-gdb tcp::端口号”参数手动指定端口,libvirt xml的设定如下:

或者:

3. CLion

点击右上角的“Select Run/Debug Configuration”,添加个GDB Remote Debug:

‘targer remote’ args填入qemu监听的gdbserver IP和端口,Symbol file选编译出的vmlinux(在源码根目录下)。

5. 调试

断点的设定跟平常无异。

启动qemu,按Debug按钮开始调试:

Debug标签页,pause后可以调用gdb console,如果kernel在运行,点左侧的“| |”即可:

栈、变量以及源码通过IDE浏览会直观许多:

6. <optimized out>

提醒下:通过-O0参数是无法成功编译Kernel的。

gdb console通过disassemble可以反汇编当前函数:

优化后的汇编其实不太好跟实际写的C代码联系起来,如果实在不行,就printk吧……

 

千兆公网带宽的TCP sendbuffer与receivebuffer调整

不知道有多少人留意过,使用千兆公网带宽传输数据时,只要距离稍微远点,就无法跑满千兆。对于这种情况,可能第一反应是“线路繁忙,公网带宽不足,跑不满很正常”。不过如果你有在不同地区的多台千兆设备间传数据的经历的话,你大概会发现,最大传输速度跟延迟成负相关。

首先丢出个公式:

TCP有ARQ机制,已发出的数据要收到ACK后才能丢弃。因为至少要等一个RTT才能收到ACK,所以sendbuffer要至少能存放一个RTT内能发出的数据量。另外,TCP的拥塞控制也会根据接收方的receivebuffer大小限制数据的发出速率。因此,如果想达到100%的带宽利用率,双方的buffer size都得符合上述公式。

根据Linux kernel的document,TCP sendbuffer max size默认为4MB,所以RTT低于多少时才能保证千兆呢?

RTT = 32ms,只要超过32ms,你的千兆带宽就不能跑满。万兆网络的话,这个数还得除以十:3.2ms,基本上只能在局域网内跑满(我猜即使buffer够了,万兆在公网还真的跑不满……)。

这里再丢出两条公式:

为什么会有另外两条公式?因为开头给出的只是理论值,实际上的buffer,并非仅用来储存payload,因此需要扣除非payload部分。

sendbuffer:

第一条计算sendbuffer的公式的系数0.7,是我测出来的,并不是准确值,为什么会是接近这个值,我还在寻找答案。

receivebuffer:

计算receivebuffer的可能有点复杂,对于tcp_adv_win_scale,kernel文档解释如下:

tcp_adv_win_scale – INTEGER
Count buffering overhead as bytes/2^tcp_adv_win_scale
(if tcp_adv_win_scale > 0) or bytes-bytes/2^(-tcp_adv_win_scale),
if it is <= 0.

Possible values are [-31, 31], inclusive.

Default: 1

值的应用以及公式的出处,可以在这里找到:https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/net/tcp.h?h=v5.10.12#n1395

简单来讲,就是receivebuffer有一部分是作为buffering overhead的,其大小通过tcp_adv_win_scale进行控制。所以扣除这一部分才是实际用来存放payload的size。

buffer在内核中的设定:

TCP的sendbuffer与receivebuffer分别在net.ipv4.tcp_wmem和net.ipv4.tcp_rmem中设定:

三个值分别是min,default和max,kernel会在min和max自动调整,程序也可以通过setsockopt() API设定。

下面是对buffer的调整尝试:

A到B之间的RTT是146ms,A的sendbuffer max size是默认4MB,测试下B从A下载文件的速度:

调整A的sendbuffer:

再到B上测试下速度:

快的确是快了,但只有21.2 MB/s,好像并不是预期的结果,如果你抓包看看A和B之间的数据包,你会发现,B的窗口大小上限是3 MB,见下图的Win:

所以这限制了未被ACK的数据量,A不能发送超出B接收能力的量,通过窗口大小和RTT可以反推出速度:3MB / 0.146s ≈ 20.5MB/s,接近curl的速度。

TCP窗口大小是receivebuffer的具体表现,查看一下B的receivebuffer:

如果把数值代入receivebuffer的公式,你可以算得receivebuffer的值是接近6MB,跟当前tcp_rmem的max size一致,所以B目前的行为是符合这两个数值的预期的。

下面调整下B的receivebuffer:

这时抓包,可以看到B的窗口大小上限是18M:

0.146s * 112MB/s = 16.352,可以确定目前的瓶颈是物理带宽而不是buffer。

再找了个与A的RTT是68ms的C:

A的sendbuffer已经调整过了,足以应付68ms的RTT,但C的receivebuffer还没调整:

新的速度接近原来的两倍,把数值代入公式:0.068s * rate = 12MB – (12MB / 2^1),得rate = 88.2353MB/s,接近实际速率。

另外,这里再提一个参数“tcp_slow_start_after_idle”,kernel document的解释如下:

tcp_slow_start_after_idle – BOOLEAN
If set, provide RFC2861 behavior and time out the congestion
window after an idle period. An idle period is defined at
the current RTO. If unset, the congestion window will not
be timed out after an idle period.

Default: 1

这个功能默认是启用的,连接空闲一个RTO后,后面会从新进入慢启动状态,长连接要保证稳定性能的话,置0。

Linux IPv4 forward自动丢弃源IP为系统已绑定的IP的问题

A是路由,其上做了策略,一部分符合特定条件的流量,是要转发到B进行封装处理,A和B之间做了数个VLAN,B根据VLAN应用不同的策略。

不过这里的问题是,B的默认网关也是A(环路问题是不存在的,已经有对应规则处理),所以B自身发出的流量也会由A应用策略,转发回给B自己。而B收到src IP是local IP(系统已绑定的IP)的包时,kernel会直接丢弃数据包,所以除了B自己的数据包,B都能正常封装。

解决这个问题的一个方法,是让A在把数据包转发给B前,做一次SNAT,这样B收到的数据包,src IP就是A的IP了。其实SNAT并没有什么影响,A对NAT也有hardware offload,但这样操作实在觉得多此一举。

翻了下kernel ip sysctl部分的文档,发现个参数accept_local:

accept_local – BOOLEAN

Accept packets with local source addresses. In combination with suitable routing, this can be used to direct packets between two local interfaces over the wire and have them accepted properly. default FALSE

所以把这个参数置1就能不依赖SNAT解决问题了:

Linux L2TPv3以及ip-xfrm的配置

L2TPv3:http://man7.org/linux/man-pages/man8/ip-l2tp.8.html

ip-xfrm:http://man7.org/linux/man-pages/man8/ip-xfrm.8.html

这两个都是kernel内置的功能,通过这两个可以直接构建加密的VPN。

本文尝试使用ip-xfrm创建加密的隧道,并基于此隧道构建L2TPv3 VPN。

1. 生成密钥与ID

不使用StrongSwan,手动配置ip-xfrm时需要用到:

复制上面四行命令后输出的内容,粘贴到left和right上运行,用于设置变量

2. 配置ip-xfrm

这里得分两种情况,一种是left和right都不经过NAT直接持有公网IP,另一种是left或者right任何一端经过NAT。

为什么要这样区分?因为ip-xfrm传输加密报文使用的协议ESP是L3的,一般情况下无法NAT,需要使用L4封装才能正常使用;其次,L4被SNAT后的源端口(source port),可能会被改变。

下面以UDP端口1801作为L2TPv3的通讯端口,对ip-xfrm进行配置。

2.1 不经过NAT

拓扑如下:

首先在left配置ip-xfrm:

然后配置right:

上面的配置命令,state是指定left到right的之间数据封装使用ESP协议,指定使用sha1算法进行签名,aes-128进行加密。policy指定当L2TPv3的UDP数据包在left和right之间传输时,使用刚刚在state里面配置的规则进行封装。

2.2 经过NAT

拓扑:

上图中left和right都被NAT过,这里需要注意:至少要保证left或者right的一个UDP端口能被对方访问,如果双方都被NAT,且路由器都没有映射端口到它们那,那这个VPN就无法构建。

这里假定right的路由把到2.3.5.6的流量都转发给了192.168.2.2。

NAT还有一个比较大的问题,就是SNAT后源端口可能会改变,假定left和right设定了ESP通过双方的UDP端口4500封装,如果left的kernel从源端口4500发送UDP封装的ESP数据包到2.3.5.6:4500,right收到的数据包的来源端口有可能不是4500,这样会内核就找不到对应的xfrm规则,不会对解除该ESP的封装。即使你通过抓包或者其它方法获取到了被SNAT后的端口,如果这个NAT记录一段时间后无任何流量,路由器会清理掉该记录,后面再SNAT的端口可能又是另一个了。所以,这就是为什么IPsec会有NAT-T。

不过,即使解决SNAT和NAT记录超时问题,也还不足,虽然可以直接手动配置espinudp的规则,但是还是得依赖外部程序,内核才会解除ESP的UDP封装。

2.2.1 方式一:使用StrongSwan配置ip-xfrm

StrongSwan的实现其实也是ip-xfrm,使用StrongSwan,需要开放UDP端口500和4500,这里假设right的路由已把这两个端口转发到right。

在left和right安装StrongSwan:

在left的/etc/ipsec.conf加入记录:

在left的/etc/ipsec.secrets加入记录(把pre-shared-key改成你自己指定的密钥)

在right的/etc/ipsec.conf加入记录(这里的auto是add不是route,因为只有right的路由器是为right映射了端口的,left没有,right是无法主动向left发起协商请求的):

在right的/etc/ipsec.secrets加入记录(left和right的PSK要一致):

 

注意,在StrongSwan的配置文件中的left和right并不代表拓扑图中的left和right,left是本机,right是对方;另外,ipsec.secrets的格式是:

上面StrongSwan加入的记录,大概意思就是该规则应用于left和right的udp端口1801之间的通讯,并且让内核在遇到匹配left和right 1801端口的通讯时自动配置ip-xfrm,此外,30秒进行一次心跳,120秒无回应则重建连接。

在两边都重启StrongSwan:

正常来说,后面构建L2TPv3后,1801端口有流量,StrongSwan会自动配置ip-xfrm,不过可以手动up试试是否成功(这里只能在left上进行,前面已提过right是无法主动向left发起协商请求的):

2.2.2 方式二:手动配置ip-xfrm

下面的配置步骤,即使全部对了,应该也是无法通的,抓包可以看到封装为UDP的ESP数据包,但是内核不会解除封装,不过如果你left和right两边都运行StrongSwan(不需要进行任何配置),监听着4500端口,就可以通。

left:

right:

3. 配置L2TPv3

在left和right上都执行一样的命令配置L2TPv3:

无误的话,在left和right上执行ip link show会看到一个名为l2tpeth100的虚拟网卡:

L2TPv3是附带以太网头的,所以还可以桥接组网。

QoS —— 交互式应用最低延迟,HTTP(S)、IMAP等常用服务优先,迅雷、P2P受限

前言

使用迅雷全速下载时,其余网络应用的体验几乎约等于断网状态。

全速下载时,迅雷并没有占用太多上行带宽,于路由器上观察上行速率,同样正常:

可见全速下载时出现的现象,问题在于下行。

迅雷全网搜索资源,这意味着会有许多来自不同地方的计算机,以高速率往我的计算机发来大量数据包。ISP处的队列早已被这些P2P数据包塞满,而SSH、网页等服务的数据包速率不高,根本不够P2P数据包竞争,这些数据包到达ISP处时,只能由于队列已满而被丢弃。

 

其实这个问题,并没有什么好的解决办法,因为ISP处的队列我们没有权限控制。

我们所能做的,只是控制P2P的速率,避免因ISP处队列满而使得数据包被丢弃。

不过,上行队列我们拥有完全控制权,倒可以自由规划。

蓝图

首先,我的主要目的是,当私有网络内有人使用迅雷等进行全速下载时,这个下载不会影响交互式应用、网页流量等,不受影响终端的包括下载者自己,其次,也要保证下载流的下行速率。

这是一项很有挑战性的任务。

下行流量大致可以分为三类:

  • 最低延迟
  • 优先传送
  • 受限

这里没有普通流量这一类,因为普通流量在P2P软件,尤其是迅雷面前尤其不好区分,我尝试过Deep Packet Inspection,迅雷的流量绝大部分被分类到了Other里面。因此,与其花心思、花系统资源去区分这些“普通流量”,还不如直接使用白名单机制,把最低延迟、优先传送的区分出来。

上行流量:

  • 最低延迟
  • 优先传送
  • 普通
  • 受限

最低延迟

最低延迟的数据包,可能会以IP Header中的TOS来标记,但抓包分析发现,基本上没有使用此字段的数据包,甚至是SSH(但内网的SSH有把TOS设为0x10,表示最低延迟,可能是公网有路由清空了该字段)。所以说,只有KCPTUN之类的会“滥用”该字段,使用该字段区分最低延迟数据包意义不大。

TCP三次握手比较关键的是SYN、SYN & ACK,带有SYN和SYN & ACK标记的我们可以归类为最低延迟,此外ACK的小包(小于64字节),我们也可以归类为最低延迟。

SSH,DNS,远程桌面,QQ以及微信等交互式服务,归类为最低延迟。

优先传送

以下协议归类为优先传送:

  • HTTP
  • HTTPS
  • SMTP
  • IMAP
  • GIT

这里有个问题,如果使用迅雷进行HTTP下载,迅雷也会全网搜索HTTP资源,这里正常的HTTP流量与迅雷的HTTP流量不是很好区分。

一个最大的特征是,迅雷可能会与同一个IP建立多个TCP连接,据此可把迅雷下载用的TCP连接归类到受限。

对于单连接的,这里没有好的办法,有以下几个原因:

根据速率区分:前面已提到迅雷会全网搜索资源,意味着迅雷会构建很多TCP连接,单个TCP连接的速率不会很高。阈值低了,影响正常网页流量,高了,无效。

根据传输流量区分:首先HTTP 1.1支持Keep Alive,这意味着对于正常网页的TCP连接来说,流量达到阈值也是有可能的。

上述二者结合:如果我一个网站同时打开多几个页面,岂不是一样被归类为受限?况且现在一个页面数MB,也很正常吧?

根据内网IP构建的TCP连接数区分:我的目的是不要让迅雷影响正常的应用,如果让某个TCP连接数多的IP归类到受限,岂不是不能实现我这个目的?

普通

本分类仅用于上行,未被归类的均归于普通类。

受限

对于下行,未被归类的,均为受限。

P2P的上行发送的都是大于1350字节的UDP数据包,把这些数据包归类为受限即可。

实现

我使用的是中国电信的带宽

下行:100 Mbps,上行:20 Mbps。

互联网接口:eth0,私有网络接口:switch0。

 

首先设置以下数个变量,方便后面的命令使用:

这里我主要的目的在于不要让P2P把ISP处的队列占满,其次对于正常的流量,并不会占用太多带宽,因此正常流量不作速率限制,仅分配不同的优先级,而P2P则限制速率,设置最低的优先级。

区分P2P上行

把大于1350字节的UDP数据包,都标记为100。

区分迅雷TCP流

P2P的不需要区分,因为下行使用的是白名单机制。

主要问题在于迅雷的HTTP下载,实难区分,这里用了一个不太尽人意的方法,把迅雷的TCP流标记为200:

首先,不能在互联网接口上对TCP流进行归类,因为在互联网接口上,所有TCP流的目的地都是我们互联网接口的IP,这导致的问题就是无法独立统计单个内网IP对单个远程IP的链接数。想一想,如果网内有多个人同时访问某个网站,是不是有可能被归类为受限?

因此这里在私有网络的接口上进行归类。

归类用到了两个模块,分别是connlimit,hashlimit。

connlimit模块仅能统计单端的链接数,hashlimit模块能双端统计,但是只能统计速率,因此配合使用,以接近限制端对端链接数的效果。

这里connlimit用到的参数,表示远程一个/24与本地构建的TCP流超过四的情况;hashlimit的参数表示远程一个/24与本地一个IP突发5MB后,数据传输速率>512kb/s的情况。

这里还用到了connmark模块,这个模块用于记录TCP流的标记,下次遇到相同的TCP流时,取上次的标记即可,无需再使用connlimit与hashlimit模块进行判断,因为这两个模块资源消耗不低。

构建上行规则

这里使用了HTB对速率进行限制,并在每个类下面安置了一个SFQ队列,以公平分配一个队列中各流的带宽:

接下来就把数据包归类,我选择使用u32选择器。

u32选择器强大,同时也难用。

u8, u16都会转换为u32,uX跟着一个值与掩码,at后面是偏移量,偏移从IP Header开始。

掩码将会与数据包偏移位置处进行位于,再与给定的值进行比较。

更多u32选择器的细节,见u32选择器的手册页:Universal 32bit classifier in tc(8) LinuxUniversal 32bit classifier in tc(8)

分类规则:

构建下行规则

队列的构建与上面同理,只是少了一个普通流量的类:

归类:

完整的Script

总结

对P2P的归类效果还是非常好的:

1:1是总的下行,1:100是P2P的类别的下行。

P2P上行也很精确地限制在了1 Mbps:

要对迅雷的全网HTTP资源多线程下载完全正确归类可能做不到,但IP对IP流量大,高数据包速率的都能受限,正常的网页流量,交互式应用受到的影响不会太大。

下行速率的控制会不太准确,毕竟我们无法控制远程计算机给我们发包的速率,需要根据实际情况适当减少rate的值。

对于延迟,就比较难保证了,这依赖于ISP的队列规则,即使我们尽量做到不让ISP处丢包,但大量的数据包排队还是会导致其余应用的延迟增加。我这里把P2P的下行速率限制为80 Mbps,P2P全速下载时,延迟也只是增加了5 – 10 ms,可以接受,ISP处的队列比较合理。