udp2raw的应用

TCP伪装成UDP报文的应用

Posted by Dandan on March 27, 2023

前言

在wg业务的应用中,又发现了一系列的问题。在现网用着用着就断了,客户投诉了好几次,不得已开始找各种解决办法,后来就找到了udp2raw,git地址:https://github.com/wangyu-/udp2raw.git,基于udp2raw进行了改进以及落地。

wg中应用

WireGuard 尽管是一个更先进、更现代的 VPN 协议,但是它在国内网络环境下会遇到一个致命的问题:UDP 封锁/限速。虽然通过 WireGuard 可以在隧道内传输任何基于 IP 的协议(TCP、UDP、ICMP、SCTP、IPIP、GRE 等),但 WireGuard 隧道本身是通过 UDP 协议进行通信的,而国内运营商根本没有能力和精力根据 TCP 和 UDP 的不同去深度定制不同的 QoS 策略,几乎全部采取一刀切的手段:对 UDP 进行限速甚至封锁。

鲁迅先生说过:羊毛出在羊身上!突破口还是在运营商身上:虽然对 UDP 不友好,但却无力深度检测 TCP 连接的真实性。
这就好办了,既然你对 TCP 连接睁一只眼闭一只眼,那我将 UDP 连接伪装成 TCP 连接不就蒙混过关了。目前支持将 UDP 流量伪装成 TCP 流量的主流工具是 udp2raw。
伪装命令:

# udp2raw --help
udp2raw-tunnel
git version:    build date:Jul 28 2023 09:50:52
repository: https://github.com/wangyu-/udp2raw-tunnel

usage:
    run as client : ./this_program -c -l local_listen_ip:local_port -r server_host:server_port  [options]
    run as server : ./this_program -s -l server_listen_ip:server_port -r remote_host:remote_port  [options]

common options,these options must be same on both side:
    --raw-mode            <string>        avaliable values:faketcp(default),udp,icmp
    -k,--key              <string>        password to gen symetric key,default:"secret key"
    --cipher-mode         <string>        avaliable values:aes128cbc(default),xor,none
    --auth-mode           <string>        avaliable values:md5(default),crc32,simple,none
    -a,--auto-rule                        auto add (and delete) iptables rule
    -g,--gen-rule                         generate iptables rule then exit,so that you can copy and
                                          add it manually.overrides -a
    --disable-anti-replay                 disable anti-replay,not suggested
client options:
    --source-ip           <ip>            force source-ip for raw socket
    --source-port         <port>          force source-port for raw socket,tcp/udp only
                                          this option disables port changing while re-connecting
other options:
    --conf-file           <string>        read options from a configuration file instead of command line.
                                          check example.conf in repo for format
    --fifo                <string>        use a fifo(named pipe) for sending commands to the running program,
                                          check readme.md in repository for supported commands.
    --log-level           <number>        0:never    1:fatal   2:error   3:warn
                                          4:info (default)     5:debug   6:trace
    --log-position                        enable file name,function name,line number in log
    --disable-color                       disable log color
    --disable-bpf                         disable the kernel space filter,most time its not necessary
                                          unless you suspect there is a bug
    --sock-buf            <number>        buf size for socket,>=10 and <=10240,unit:kbyte,default:1024
    --force-sock-buf                      bypass system limitation while setting sock-buf
    --seq-mode            <number>        seq increase mode for faketcp:
                                          0:static header,do not increase seq and ack_seq
                                          1:increase seq for every packet,simply ack last seq
                                          2:increase seq randomly, about every 3 packets,simply ack last seq
                                          3:simulate an almost real seq/ack procedure(default)
                                          4:similiar to 3,but do not consider TCP Option Window_Scale,
                                          maybe useful when firewall doesnt support TCP Option
    --lower-level         <string>        send packets at OSI level 2, format:'if_name#dest_mac_adress'
                                          ie:'eth0#00:23:45:67:89:b9'.or try '--lower-level auto' to obtain
                                          the parameter automatically,specify it manually if 'auto' failed
    --wait-lock                           wait for xtables lock while invoking iptables, need iptables v1.4.20+
    --gen-add                             generate iptables rule and add it permanently,then exit.overrides -g
    --keep-rule                           monitor iptables and auto re-add if necessary.implys -a
    --hb-len              <number>        length of heart-beat packet, >=0 and <=1500
    --mtu-warn            <number>        mtu warning threshold, unit:byte, default:1375
    --clear                               clear any iptables rules added by this program.overrides everything
    --retry-on-error                      retry on error, allow to start udp2raw before network is initialized
    -h,--help                             print this help message

配置

客户端wg配置

# cat wg_10004
[Interface]
PrivateKey=2No7YStLNZN6QF4HHkb+5LZpSZt5Ih7aDF+CVMMhsUc=
[Peer]
PublicKey=vHZv3rf4GZsALwEoOml/JugdrI4GRPE5mv3xsWDYbCQ=
PresharedKey=Yh8rHLu5OqBGOqI0j4k/Z/X3VVUxfeYu9FRJ/wMV9B8=
AllowedIPs=0.0.0.0/0
Endpoint=127.0.0.1:51820
PersistentKeepalive=25

客户端udp2raw进程:

/usr/bin/udp2raw -c -l 127.0.0.1:51820 -r 192.168.21.219:51820 --raw-mode faketcp --cipher-mode xor --log-level 0 -a --wait-lock

注意:客户端wg配置的 endpoint 已经变成了127.0.0.1, 服务端的wg配置不变。
服务端udp2raw进程:

/usr/bin/udp2raw -s -l 0.0.0.0:51820 -r 127.0.0.1:51820 --raw-mode faketcp --cipher-mode xor

原理

无伪装:
原:wg隧道 server端 wg接口监听udp port 51820,client的wg接口发送数据到server的51820端口。
伪装:

  • Client:将wg的endpoint_host 改为127.0.0.1,udp2raw进程监听wg目的地址为127.0.0.1:52820的报文,然后将报文发送到192.168.21.26:52820。
  • Server:udp2raw进程监听 0.0.0.0:52820的报文,当收到client发来的报文,转到127.0.0.1:51820接口,wg接口接收报文。

遇到的问题

服务端的链接不能正常自己关闭
现象是当客户端重新换一个端口号进行连接时,在服务端旧的连接不会消失,一直存在,当连接超过设备中最大的tcp连接数时,将不能有新的连接建立。例如,当服务端监听的端口号是61821时的现象:

netstat -an  | grep 61821
tcp      129      0 0.0.0.0:61821           0.0.0.0:*               LISTEN      
tcp        1      0 111.207.49.219:61821    111.207.49.213:51484    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51596    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51491    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51477    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51399    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51599    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51600    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51535    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51589    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51551    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51401    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51510    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51498    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51594    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51402    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51534    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51464    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51490    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51606    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51389    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51403    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51514    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51505    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51461    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51433    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51486    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51449    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51528    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51506    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51450    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51504    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51529    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51422    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51393    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51492    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51445    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51603    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51434    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51446    CLOSE_WAIT  
tcp       45      0 111.207.49.219:61821    111.207.49.213:35297    ESTABLISHED 
tcp        1      0 111.207.49.219:61821    111.207.49.213:51481    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51474    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51468    CLOSE_WAIT  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51500    CLOSE_WAIT  
...  
tcp        1      0 111.207.49.219:61821    111.207.49.213:51424    CLOSE_WAIT  
tcp       45      0 111.207.49.219:61821    223.104.38.134:41201    ESTABLISHED 

再看一下tcp连接建立与断开的过程图: 奋斗
CLOSE_WAIT是TCP的一个状态,它在ESTABLISHED(连接建立)基础上,收到对方的FIN且我方已回ACK,进入CLOSE_WAIT状态。但是如果服务端不执行close(),就不能由CLOSE_WAIT迁移至LAST_ACK。说白了就是对方已关闭我方尚未关闭。
如果有长时间和大量的TCP处于CLOSE_WAIT状态时,原因是连接未正确关闭。 解决如下:

#define NUM_TCP_KEEPCNT 5      //关闭一个非活跃连接之前的最大尝试次数
#define NUM_TCP_KEEPIDLE 10    //关闭一个非活跃连接之前的最大重试次数
#define NUM_TCP_KEEPINTVL 120   //前后两次探测之间的时间间隔,单位是秒

int set_keepalive(int fd){
    int num;
    int r;
    int optval;
    socklen_t optlen = sizeof(optval);

    /* set up SO_KEEPALIVE */
    optval = 1;
    if(setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &optval, optlen) < 0){
        mylog(log_fatal,"setsockopt SO_KEEPALIVE fail\n");
        return -1;
    }


    /* set up TCP_KEEPCNT */
    num = NUM_TCP_KEEPCNT;
    if(setsockopt(fd, SOL_TCP, TCP_KEEPCNT, (char*)&num, sizeof(num)) < 0){
        mylog(log_fatal,"setsockopt TCP_KEEPCNT fail\n");
    }



    /* set up TCP_KEEPIDLE */
    num = NUM_TCP_KEEPIDLE;
    if(setsockopt(fd, SOL_TCP, TCP_KEEPIDLE, (char*)&num, sizeof(num)) < 0){
        mylog(log_fatal,"setsockopt TCP_KEEPCNT fail\n");
    }
 /* set up TCP_KEEPINTVL */
    num = NUM_TCP_KEEPINTVL;
    if(setsockopt(fd, SOL_TCP, TCP_KEEPINTVL, (char*)&num, sizeof(num)) < 0){
        mylog(log_fatal,"setsockopt TCP_KEEPINTVL fail\n");
    }

    return 1;
}

或者可以通过修改内核参数解决。keepalibe部分选项如下(一下只是示例,根据实际情况修改):

sysctl -a | grep keepalive
net.ipv4.tcp_keepalive_intvl = 5
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 20

服务端允许连接的数量较少
该问题是上个问题出现时发现的,当连接130个之后,新的连接再也不能建立,内核选项如下:

net.core.somaxconn = 1024– 该内核参数控制传入连接的数量。增加它可以1024让 Momentum 处理更多开放的连接。

还有其他的优化项:

net.ipv4.tcp_tw_reuse– 当从协议角度来看安全时,将处于 TIME_WAIT 状态的套接字重新用于新连接。设置此选项可以1重用打开的连接,从而提高效率。
net.ipv4.tcp_tw_recycle– 此内核参数可实现 TIME_WAIT 套接字的快速回收。设置1允许重用套接字而无需正常等待时间。

net.core.somaxconn介绍
对于一个TCP链接,Server与Client需要通过三次握手来建立网络链接,当三次握手成功之后,我们就可以看到端口状态由LISTEN转为ESTABLISHED,接着这条链路上就可以开始传送数据了。 net.core.somaxconn是Linux中的一个内核(kernel)参数,表示socket监听(listen)的backlog上限。
什么是backlog?backlog就是socket的监听队列,当一个请求(request)尚未被处理或者建立时,它就会进入backlog。
而socket server可以一次性处理backlog中的所有请求,处理后的请求不再位于监听队列中。
当Server处理请求较慢时,导致监听队列被填满后,新来的请求就会被拒绝。