Chion82 / kcptun-raw

Kcptun with raw socket and fake TCP headers.
GNU General Public License v3.0
417 stars 102 forks source link

内核态filter支持 #15

Closed wangyu- closed 6 years ago

wangyu- commented 7 years ago

目前判断raw socket收到的数据包的端口这段逻辑是在用户态实现的。也就是说,所有包都要从内核态传到用户态,影响性能。可以考虑加上bpf filter.这个我测试过,openwrt和桌面linux都是支持的。 代码如下:

struct sock_filter code_tcp[] = {
{ 0x5, 0, 0, 0x00000001 },//0    //jump to 2,dirty hack from tcpdump -d's output
{ 0x5, 0, 0, 0x00000000 },//1
{ 0x30, 0, 0, 0x00000009 },//2
{ 0x15, 0, 6, 0x00000006 },//3
{ 0x28, 0, 0, 0x00000006 },//4
{ 0x45, 4, 0, 0x00001fff },//5
{ 0xb1, 0, 0, 0x00000000 },//6
{ 0x48, 0, 0, 0x00000002 },//7
{ 0x15, 0, 1, 0x0000fffe },//8   //modify this fffe to the port you listen on
{ 0x6, 0, 0, 0x0000ffff },//9
{ 0x6, 0, 0, 0x00000000 },//10
};
int code_tcp_port_index=8;
void init_filter(int port)
{
    sock_fprog bpf;

    bpf.len = sizeof(code_tcp)/sizeof(code_tcp[0]);
    code_tcp[code_tcp_port_index].k=port;
    bpf.filter = code_tcp;
    int dummy;

    int ret=setsockopt(raw_recv_fd, SOL_SOCKET, SO_DETACH_FILTER, &dummy, sizeof(dummy)); //in case i forgot to remove
    if (ret != 0)
    {
        mylog(log_debug,"error remove fiter\n");
        //perror("filter");
        //exit(-1);
    }
    ret = setsockopt(raw_recv_fd, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));
    if (ret != 0)
    {
        mylog(log_fatal,"error set fiter\n");
        //perror("filter");
        myexit(-1);
    }
}
Chion82 commented 7 years ago

非常感谢,BPF也是下个版本希望引入的优化。

wangyu- commented 7 years ago

这两行可以删掉,效果跟原来一样的。

{ 0x5, 0, 0, 0x00000001 },//0    //jump to 2,dirty hack from tcpdump -d's output
{ 0x5, 0, 0, 0x00000000 },//1

然后把code_tcp_port_index从8改成6

int code_tcp_port_index=6;

这个代码是用tcpdump -i eth1 ip and tcp and dst port 65534 -dd生成然后我自己手改的。(tcpdump是用的PF_PACKET , SOCK_RAW,不能直接用在PF_PACKET , SOCK_DGRAM上,要改。)本来我以为删了这两行要改后面的标号的,怕麻烦就没改,后来发现原来所有地址都是相对地址,可以直接删。

wangyu- commented 7 years ago

直接贴改后的吧:

struct sock_filter code_tcp[] = {
//////{ 0x5, 0, 0, 0x00000001 },//0    //jump to 2,dirty hack from tcpdump -d's output
//////{ 0x5, 0, 0, 0x00000000 },//1
{ 0x30, 0, 0, 0x00000009 },//2
{ 0x15, 0, 6, 0x00000006 },//3
{ 0x28, 0, 0, 0x00000006 },//4
{ 0x45, 4, 0, 0x00001fff },//5
{ 0xb1, 0, 0, 0x00000000 },//6
{ 0x48, 0, 0, 0x00000002 },//7
{ 0x15, 0, 1, 0x0000fffe },//8   //modify this fffe to the port you listen on
{ 0x6, 0, 0, 0x0000ffff },//9
{ 0x6, 0, 0, 0x00000000 },//10
};
int code_tcp_port_index=6;
void init_filter(int port)
{
    sock_fprog bpf;

    bpf.len = sizeof(code_tcp)/sizeof(code_tcp[0]);
    code_tcp[code_tcp_port_index].k=port;
    bpf.filter = code_tcp;
    int dummy;

    int ret=setsockopt(raw_recv_fd, SOL_SOCKET, SO_DETACH_FILTER, &dummy, sizeof(dummy)); //in case i forgot to remove
    if (ret != 0)
    {
        mylog(log_debug,"error remove fiter\n");
        //perror("filter");
        //exit(-1);
    }
    ret = setsockopt(raw_recv_fd, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));
    if (ret != 0)
    {
        mylog(log_fatal,"error set fiter\n");
        //perror("filter");
        myexit(-1);
    }
}
Chion82 commented 7 years ago

OK,该patch将并入 #11 ,预计下个月Merge

Chion82 commented 7 years ago

精简了一下BPF bytecode,最终如下:

struct sock_filter code_tcp[] = {
    { 0x30, 0, 0, 0x00000009 },
    { 0x15, 0, 4, 0x00000006 },
    { 0xb1, 0, 0, 0x00000000 },
    { 0x48, 0, 0, 0x00000002 },
    { 0x15, 0, 1, 0x0000fffe /*destination port*/ },
    { 0x6, 0, 0, 0x0000ffff },
    { 0x6, 0, 0, 0x00000000 },
};

目前仍有一个问题:无论采用哪种BPF bytecode,都会存在比较明显的kcp片段接收延迟,导致更频繁的延迟抖动,进而降低传输速率,性能反而不如不使用BPF,目前仍在排查中。临时解决:默认关闭BPF,增加了--bpf开关。

wangyu- commented 7 years ago

@Chion82

能想到的一个问题是MTU。有可能因为MTU的原因导致UDP被分片了,上面的这些BPF代码不能处理UDP分片:

https://stackoverflow.com/questions/16897319/libpcap-cant-capture-ip-fragments

原版的kcptun有一个-mtu选项,从kcptun-raw的说明文档里我没看到mtu相关的选项,不知从内部是否可以把MTU调小。建议调小MTU测试一下,排除分片的问题。

wangyu- commented 7 years ago

上面的这些BPF代码不能处理UDP分片

对每个udp包,第一个ip fragment会被接受,后续的ip fragment会被bpf挡在外面。

wangyu- commented 7 years ago

忽略以上回答吧= =。

现在kcptun-raw的代码也没有写处理分片的逻辑,如果产生了分片,即使不被BPF挡掉也会被丢弃。应该不是MTU的问题。

Chion82 commented 7 years ago

@wangyu- kcptun-raw本身没有分片逻辑,依靠kcp层进行分片,kcp的每个segment已通过动态参数严格控制在1400 MTU之内,理应不会发生该问题。而且如果发生分片丢失,应该会严重影响上层速率,但实验发现速率下降不明显,只是存在延迟抖动,对上层VPN类应用程序影响才比较明显。

Chion82 commented 7 years ago

问题已定位。增加BPF后可读回调触发频率降低,没有及时调用ikcp_update()导致kcp更新不及时进而降低上层性能。动态调整kcp更新频率后即可解决。https://github.com/Chion82/kcptun-raw/commit/b8cd93833d158897d00279cf4ea13ecf456df243

ccsexyz commented 7 years ago

tcp是不在ip层分片的 为毛不用ikcp-check

Chion82 commented 7 years ago

@ccsexyz 实测ikcp_check()更新时效不能满足,效率太低

Chion82 commented 7 years ago

严格意义上说,是kcp层的分段而不是IP分片,不过目前kcptun-raw的实现禁用了kcp的stream模式,kcp不会进行自动分段(超过MTU的segment会被丢弃,不发送),并在TCP层控制recv()缓冲区在MTU - header_len的长度内

wangyu- commented 7 years ago

建议加个MTU选项。1400不一定够小。有的时候链路可能会穿过VPN之类的东西,引入额外的overhead。

或者改得足够小。我抓包看过,finalspeed默认的MTU只有1000。

Chion82 commented 7 years ago

如果是VPN的话肯定会对IP报文再次拆分封装的,这个问题不太需要担心,以ip addr回报的接口MTU为准即可。降低MTU会牺牲性能,后续会添加该选项满足某些极端情境

wangyu- commented 7 years ago

kcpraw_client 108.0.0.1 888 192.168.1.100 8388 --mode fast2 将108.0.0.1替换为服务器IP,192.168.1.100替换为客户端IP(通常是路由器分配的内网IP,不能使用127.0.0.1)

@Chion82

我repo里还有个get_src_adress函数,可以根据目标ip获得raw socket用的源ip地址。这样就不必手动填这个192.168.1.100了。可以用127.0.0.1和0.0.0.0这样的ip。

repo里还有在2层发包的实现。不少用户机器上都有一些奇怪的iptabes规则,造成在3层发包失败。2层发包可以免去iptables规则不兼容的问题。

如果有需要,代码尽管copy。

@ccsexyz

Go 语言我不懂= =。不知道对你有没有用,如果有需要也尽管copy。

ccsexyz commented 7 years ago

额,这个功能项目开始第一天就有了

wangyu- commented 7 years ago

如果是VPN的话肯定会对IP报文再次拆分封装的,这个问题不太需要担心,以ip addr回报的接口MTU为准即可。降低MTU会牺牲性能,后续会添加该选项满足某些极端情境

如果是openvpn,默认不会拆分,会直接丢掉。昨天我还在openvpn mail list 里面看到一个人因为MTU导致了奇怪的问题。

以ip addr回报的接口MTU为准即可

据我理解,ip addr回报的这个MTU只是到你网关的这一跳的MTU。检测真正的MTU是个动态的过程,因为路由路径可能随时会变。 动态mtu检测可以参考openvpn的--mtu-test --mtu-disc,真的是个很麻烦的过程。

我觉得最方便可行的方案就是像openvpn和kcptun一样,提供一个MTU的选项,由用户设置一个保守的值。

最好本身的默认值也足够保守,以免用户发现不能用,直接把软件删了。

降低MTU会牺牲性能

实际上只要MTU不设得特别低,比如500这样。基本感受不到性能差别。

ccsexyz commented 7 years ago

从tcp mss字段可以判断中间链路的mtu

wangyu- commented 7 years ago

从tcp mss字段可以判断中间链路的mtu

你的意思是说开一个tcp连接来测MTU?这个可行。

但是注意,这个MSS不是固定的,可能会变。如果TCP一直发送失败,会尝试缩小这个MSS。

最简单的办法还是设置一个保守值。

wangyu- commented 7 years ago

有兴趣可以Google一下mtu path discovery,真的是个很麻烦的过程,而且可能有兼容性问题。

ccsexyz commented 7 years ago

你并没有理解我的意思

wangyu- commented 7 years ago

你并没有理解我的意思

愿闻其详。具体怎么利用tcp mss字段来判断中间链路的MTU。

==UPDATE== 更新下我的理解。

如果用标准TCP,那么基本不用担心MTU问题。TCP会帮你做好。你说的可能是这个意思?

如果你用raw socket模拟TCP,那么需要有一个额外的办法来决定MTU。

Chion82 commented 7 years ago

@wangyu- 这可能是openvpn的设计问题了,我尝试过tinc VPN会动态处理链路中的mtu问题,应该已经内置了MTU发现过程了。 关于2层发包,虽然我觉得必要性不大,这部分用户应该是针对某种flag的TCP片段进行了屏蔽处理,让用户自行添加排除规则即可,而且2层报文还需要考虑非常多的以太帧头格式

wangyu- commented 7 years ago

关于2层发包,虽然我觉得必要性不大,这部分用户应该是针对某种flag的TCP片段进行了屏蔽处理,让用户自行添加排除规则即可,而且2层报文还需要考虑非常多的以太帧头格式

现在的实现是在2层收包,3层发包。 基本上不需要特别奇怪的规则,只要涉及conntrack的规则,就会不兼容。因为3层发包的过程会经过netfilter,2层收包的过程对netfilter不可见。只有一个方向经过netfiler会导致conntrack产生奇怪的问题。

常见的SNAT 和DNAT命令,都隐含conntrack。

还有如果有人用来加速SS-redir,但是又没把例外添加全。在3层发包会被netfiler劫持,产生环路。(比如梅林固件,如果用kcptun-raw来加速,那么ss server的ip会被填成127.0.0.1,这样真正的server ip不会被自动添加例外。在梅林固件上添加个自定义iptables规则很麻烦。)

而且2层报文还需要考虑非常多的以太帧头格式

我测试了。几乎没什么特殊处理,同一段代码 在lo,插网线的以太网,wifi,pppoe,openvpn的tun接口都能正常工作。

==update= 不过2层发包必要性确实不强,只要iptables例外添加对了,就不会有问题。 ==update2== 附上一个会导致3层发包2层收包失败的iptables规则,有一个用户发给我的,运行在他的服务器上:

iptables -t nat -I POSTROUTING -o ens3 -d 0.0.0.0/0 -s serverip1 -j SNAT --to-source serverip2

虽然这个规则很奇怪,但是ss用起来没有问题,我的程序连不通。后来用2层发包解决了。

wangyu- commented 7 years ago

这可能是openvpn的设计问题了,我尝试过tinc VPN会动态处理链路中的mtu问题,应该已经内置了MTU发现过程了。

如果你的包在中间路由(包括vpn链路)被分片了,那么可能会产生更大的性能问题。2个分片,只要有一个被丢弃,整个IP包就丢了,需要重传整个IP包。(据我理解,中间路由一般只管分片,不管重组。重组一般只有最终的host做。)

不如在高层协议设置个保守(比如1200)的MTU,避免分片。

ccsexyz commented 7 years ago

tcp在建立连接的时候会进行mss协商,如果中间的设备良心的话会修改mss的值,使最后的IP包不超过MTU。 当然这不是完美的办法(实际上也没有),但是肯定是现在成本最低的方法了。

Chion82 commented 7 years ago

@wangyu- conntrack跟踪TCP连接,在家用路由器固件的SNAT是表现为在握手阶段修改IP和TCP报文,另外还有MSS钳制(TCP报头长度可能会被改变),netfilter做的这个事情理应是不会干扰到我们的工作的,我的kcptun-raw也是运行在家用路由器上,跟openwrt一样,通过SNAT进行地址转换。遇到这种情况,在可行的情况下,可以排查是哪个conntrack related的规则所致的,比如MWAN和shadowsocks添加的规则。

比如梅林固件,如果用kcptun-raw来加速,那么ss server的ip会被填成127.0.0.1,这样真正的server ip不会被自动添加例外。

对于这种情况,kcptun-raw的LOCAL_IP字段应该填写与接口绑定的IP地址,比如br-lan上始终固定的192.168.1.1,经由内核可以进行正常路由。 自动探测源地址的功能会参考你的方法,后续实现,可以放宽该参数的配置。

wangyu- commented 7 years ago

这样真正的server ip不会被自动添加例外

这个说的是kcptun-raw的server ip。是个公网ip,比如44.55.66.77。

如果你用梅林固件直接连44.55.66.77上面搭的ss-server,那么44.55.66.77会被自动添加例外。

假设下载你在44.55.66.77上面运行了kcptun-raw server,在路由器上运行了kcptun-raw client来加速ss。

现在你要用梅林固件连kcptun-raw映射到本地的ss-server端口,那么你填的ip地址是本地的ip地址,所以44.55.66.77不会被添加例外。如果44.55.66.77不被添加例外,kcptun-raw client到kcptun-raw server的流量会被ss-redir劫持。因为这时梅林固件不知道你有个server ip是44.55.66.77。

wangyu- commented 7 years ago

@ccsexyz

tcp在建立连接的时候会进行mss协商,如果中间的设备良心的话会修改mss的值,使最后的IP包不超过MTU。

MSS检测是个贯穿在整个TCP连接的动态过程。 因为国际网络路由策略很复杂,随时可能变。中间路径可能随时会被切换到一个有更低MTU的链路上。

用TCP建立连接时检测出来的这个值,不能一直代表链路的真正MTU。

我觉得我们的分歧在: 因为国际网络路由策略很复杂,随时可能变。中间路径可能随时会被切换到一个有更低MTU的链路上 这个问题会不会经常发生,有没有必要真正担心它。

Chion82 commented 7 years ago

@wangyu- 这个实际上就是openwrt的shadowsocks包的启动脚本动态添加的iptables脚本,具体是这条规则:

Chain SS_SPEC_WAN_AC (2 references)
 pkts bytes target     prot opt in     out     source               destination
 2458  152K RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set ss_spec_wan_ac dst

44.55.66.77这个IP在ss_spec_wanac这个ipset中。要想实现在openwrt上通过kcptun-raw加速ss-redir,最好是自行写一个启动脚本来配置iptables并放置于/etc/init.d,在shadowsocks启动之前执行初始化配置。在这个脚本中,我们在nat表中手动添加过滤规则,比如-A PREROUTING -d 44.55.66.77 -j RETURN(此后shadowsocks的启动脚本会在后续添加多个以`SS`前缀的链,这样就可以不去修改ss的脚本),并在filter表的INPUT链中添加README所述的DROP。

wangyu- commented 7 years ago

@Chion82

最好是自行写一个启动脚本来配置iptables并放置于/etc/init.d

这样可以。

在openwrt上更好的办法是放在/etc/firewall.user中。因为用户从luci 改了配置,点了commit以后,iptables 规则可能会被清空,重新加。/etc/firewall.user在每次Iptables变化后会自动调用。如果用/etc/init.d来实现的话,可能就需要用脚本定期检查iptables有没有变了。

在梅林上应该添加到/jffs/scripts/firewall-start里面,比较烦人的一点是在梅林上这个文件默认是被禁用了的。

==update== 我举了几个例子,主要是想说二层发包用起来方便些= =。不是说这几种情况下三层发包不能工作。

ccsexyz commented 7 years ago

小概率错误是可以容忍的

Chion82 commented 7 years ago

@wangyu-

iptables -t nat -I POSTROUTING -o ens3 -d 0.0.0.0/0 -s serverip1 -j SNAT --to-source serverip2

这看上去是很正常的SNAT,在我的客户端和服务器上都有类似的SNAT。需要确认:serverip1 和 serverip2 分别是什么地址,raw socket在发包时以哪个IP为源地址?

wangyu- commented 7 years ago

首先回答的你的问题:

iptables -t nat -I POSTROUTING -o ens3 -d 0.0.0.0/0 -s serverip1 -j SNAT --to-source serverip2

需要确认:serverip1 和 serverip2 分别是什么地址

这台机器是个双IP的VPS。serverip1是默认的ip,serverip2是第二个ip。udp2raw server运行在这台机器上。同时运行在这台机器上的还有原版kcptun和ss,可以正常工作。

服务端运行的命令是:./udp2raw_amd64 -s -l0.0.0.0:9966 -r 127.0.0.1:8855 -a --sock-buf 1024 --log-position --log-level 4 --cipher xor -k1234

raw socket在发包时以哪个IP为源地址?

我的服务端的代码是这么写的:如果bind到了0.0.0.0,那么以哪个IP收到包,就用哪个IP发回去。其他逻辑跟你的代码基本是一样的。

我有点不确定经过他这么搞之后,到底是哪个IP收到包,我还是不猜了,可能你比我熟悉SNAT,你看下

然后补充说明一下:

他整个server上就这么一条iptables规则。

至于他为什么这么写这条规则,他原话是:我想实现连接serverip1 但别人看到我的ip是serverip2

他的client访问server时用的哪个ip他没说。他搞了很久也没通,相信他serverip1和serverip2都试过。

后来用了二层发包直接通了。我没有类似的环境,所以后续也没详细测试。

==update== 从原理上我觉得。如果SNAT和DNAT规则部署在跟client或server同一台机器时,因为他们依赖CONNTRACK,3层发2层收是有问题的(添加iptables例外可以解决)。如果SNAT和DNAT部署在中间路径的一台机器上,那么是没问题的。

ccsexyz commented 7 years ago

@wangyu- 刚看了眼你写的get_src_address函数,看来大家的思路都差不多嘛 https://github.com/ccsexyz/rawcon/blob/master/raw_linux.go#L325

Chion82 commented 7 years ago

@wangyu-

从原理上我觉得。如果SNAT和DNAT规则部署在跟client或server同一台机器时,因为他们依赖CONNTRACK,3层发2层收是有问题的(添加iptables例外可以解决)。如果SNAT和DNAT部署在中间路径的一台机器上,那么是没问题的。

是有这种可能的:2层收包的时候由于绕过了netfilter,SNAT隐含的DNAT没有生效所致(对于conntrack标记了SNAT的当前连接,收包的时候会隐式进行SNAT的逆操作)。kcptun-raw目前使用三层发包和三层收包,没有遇到conntrack跟踪失败的情况。

对于这个用户,通过路由跃点数(route metrics)的方法,增加一条以serverip2优先级更高的路由可能简单,以此避免使用这种SNAT。

wangyu- commented 7 years ago

SNAT隐含的DNAT没有生效所致(对于conntrack标记了SNAT的当前连接,收包的时候会隐式进行SNAT的逆操作)

是的。我也是这样理解的。

对于这个用户,通过路由跃点数(route metrics)的方法,增加一条以serverip2优先级更高的路由可能简单,以此避免使用这种SNAT。

我当时建议他把这个SNAT删掉。直接用ip route命令把第二个出口改成默认的。我觉得能达到一样的效果,但是他没给我反馈。我也不知道是否确实可以。

kcptun-raw目前使用三层发包和三层收包

这个不是吧。我当初的代码就是照你的写的。2个月前我看了一遍,你的也是3层发包2层收包。

我也怀疑过为什么收包不在3层做,后来发现只要在iptables 做了drop后,在3层就彻底收不到包了。就改成和你一样的3层发2层收了。

也许你后来又改了?

Chion82 commented 7 years ago

@wangyu- 对,应该也是2层收包的。

wangyu- commented 7 years ago

刚看了眼你写的get_src_address函数,看来大家的思路都差不多嘛

@ccsexyz

当时写这个函数,查资料前后浪费了我两天时间。在google上面找到的答案都说要去parse /proc/net/route这个文件,然后做路由前缀匹配。找到接口名,用接口名再查IP。这个方法太麻烦了,一直没下决心写。后来查linux的man page发现了getsockname函数,才想到这个方法。

早知道有人写过我就直接copy了= =。