用Rust手把手编写一个Proxy(代理), UDP绑定篇

用Rust手把手编写一个Proxy(代理), UDP绑定篇

项目 ++wmproxy++

gite: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

了解UDP

特点

UDP是基于IP的简单协议,不可靠的协议。

UDP的优点:简单,轻量化。

UDP的缺点:没有流控制,没有应答确认机制,不能解决丢包、重发、错序问题。

这里需要注意一点,并不是所有使用UDP协议的应用层都是不可靠的,应用程序可以自己实现可靠的数据传输,通过增加确认和重传机制,所以使用UDP 协议最大的特点就是速度快。如HTTP3就是用UDP标准下实现的上层协议。

适用场景

UDP协议一般作为重速度,但可以接受轻微损失的如流媒体应用、语音交流、视频会议所使用的传输层协议,还有许多基于互联网的电话服务使用的VOIP也是基于UDP运行的,实时视频和音频流协议旨在处理偶尔丢失的数据包,因此,如果重新传输丢失的数据包,则只会发生质量略有下降,而不是出现较大的延迟。

最常用到的为DNS协议,希望能以更小的包(包头tcp20字节,udp8字节),更快速的解析得到地址。

socks5 udp协议

协议说明

UDP 关联请求用于在UDP中继进程内建立关联以处理UDP数据报。DST.ADDR和DST.PORT字段包含客户端期望用于发送UDP数据报的地址和端口。服务器可以使用此信息来限制对关联的访问。如果客户端在UDP 关联请求时没有掌握此信息,客户端必须使用端口号和地址都为零的地址。

UDP关联会在随着的TCP连接终止时终止。

在UDP 关联请求的回复中,BND.PORT和BND.ADDR字段指示客户端必须发送UDP请求消息以进行中继的端口号/地址。

协议详情

基于UDP的客户端必须将其数据报发送到UDP端口,该端口在UDP关联请求的回复中的BND.PORT指示。如果所选的认证方法提供了封装用于身份验证、完整性、和/或机密性,则数据报必须使用适当的封装。每个UDP数据报都带有UDP请求头:

  +----+------+------+----------+----------+----------+
  |RSV | FRAG | ATYP | DST.ADDR | DST.PORT |   DATA   |
  +----+------+------+----------+----------+----------+
  | 2  |  1   |  1   | Variable |    2     | Variable |
  +----+------+------+----------+----------+----------+
  
  RSV: 保留2字节
  FRAG:当前分片, 请求时为0
  ATYP:地址类型,0x01为ipv4,0x03为域名地址,0x04为ipv6
  DST.ADDR:目标地址,根据类型读取相应字节
  DST.PORT: 目标端口号,2字节
  DATA:发送数据

当一个UDP中继服务器决定中继UDP数据报时,它会默默地这样做,不需要向请求的客户端发送任何通知。同样,它会丢弃它不能或不愿意中继的数据报。当一个UDP中继服务器从远程主机接收到回复数据报时,它必须使用上述UDP请求头和任何与认证方法相关的封装对数据报进行封装。

UDP中继服务器必须从SOCKS服务器获取将发送数据报到BND.PORT的客户端的预期IP地址,该地址在UDP关联请求的回复中给出。它必须丢弃来自除为特定关联记录的IP地址以外的任何源IP地址的所有数据报。

FRAG字段指示此数据报是否是多个片段之一。如果实现,高位比特指示片段序列的结束,而值为X'00'表示此数据报是独立的。值在1和127之间的值表示片段在片段序列中的位置。每个接收器都有一个重新组装队列和与这些片段关联的重新组装定时器。每当重新组装定时器到期或接收到携带FRAG字段值小于为该片段序列处理的最高FRAG值的新数据报时,必须重新初始化重新组装队列并丢弃关联的片段。重新组装定时器必须不小于5秒。建议应用程序尽可能避免分片。

分片实施是可选项;不支持分片的实现必须丢弃任何FRAG字段值不是X'00'的数据报。

协议流程图

UDP协议实现的是本地和远程地址中间构建出一条UDP链路,常用的如DNS解析。

对内的UDP端口接收到信息先读取远程地址的信息再做相应的转发
对外的UDP端口接收到远程的信息先加上相应的头信息再转发给客户端。

客户端的TCP长链信息必须保持活跃,一旦TCP连接关掉则通知UDP断掉连接。

代码实现

在我们详细了解了协议的内容之后,就可以开始写代码

  • 在我们收到UDP的command之后,把数据交由udp关联函数处理
rust 复制代码
pub async fn udp_execute_assoc(
    mut stream: TcpStream,
    proxy_addr: SocketAddr,
    bind_ip: IpAddr,
) -> ProxyResult<()> {
}
  • 此刻我们对UDP进行监听,得到相应的端口,然后再将传入的bind_ip和端口返回给tcp的客户端,让客户端建立udp与当前的端口绑定
rust 复制代码
let peer_sock = UdpSocket::bind("0.0.0.0:0").await?;
let port = peer_sock.local_addr()?.port();
ProxySocks5::tcp_write_reply(&mut stream, true, SocketAddr::new(bind_ip, port)).await?;
  • 接下来开始进行转发逻辑,用异步函数,主要用三个对象,tcp的连接,对内的udp端口,对外的udp端口
rust 复制代码
async fn udp_transfer(stream: TcpStream, inbound: UdpSocket) -> ProxyResult<()> {
    let outbound = UdpSocket::bind("0.0.0.0:0").await?;
}
  • 因为在tcp连接被断开的时间,我们要通知udp关联结束,那么我们要监听tcp是否被断开,被断开后通知udp结束监听。在这里采用了tokio::sync::broadcast,可以一个Sender多个接收
rust 复制代码
// 使tcp断开的时候通知udp结束关联,结束处理函数
let (sender, _) = channel::<()>(1);

async fn upd_handle_tcp_block(mut stream: TcpStream, mut receiver: Receiver<()>, sender: Sender<()>) -> ProxyResult<()> {
    let mut buf = [0u8; 100];
    loop {
        let n = tokio::select! {
            r = stream.read(&mut buf) => {
                r?
            },
            _ = receiver.recv() => {
                return Ok(());
            }
        };
        if n == 0 {
            let _ = sender.send(());
            return Ok(());
        }
    }
}
  • 因为tokio::try_join!是发生错误或者所有的均正确返回才结束,我们需要在发生错误的时候通知其它的异步停止,我们此时用的是tokio::select!都同时监听receiver.recv()收到消息则表示需要关闭此关联。以下是代理接收请求
rust 复制代码
async fn udp_handle_request(
    inbound: &UdpSocket,
    outbound: &UdpSocket,
    mut receiver: Receiver<()>,
) -> ProxyResult<()> {
    let mut buf = BinaryMut::with_capacity(0x10000);
    loop {
        //...
        // 代理对内的端口只会跟客户端的通讯, 所以建立connect
        inbound.connect(client_addr).await?;

        let (flag, addr) = Self::udp_parse_request(&mut buf).await?;
        if flag != 0 {
            return Ok(());
        }
        outbound.send_to(buf.chunk(), addr).await?;
    }
}
  • 代理方收到远程的消息 添加头发送到客户端
rust 复制代码
async fn udp_handle_response(
    inbound: &UdpSocket,
    outbound: &UdpSocket,
    mut receiver: Receiver<()>,
) -> ProxyResult<()> {
    let mut buf = BinaryMut::with_capacity(0x10000);
    loop {

        //...
        let mut buffer = BinaryMut::with_capacity(100);
        buffer.put_slice(&[0, 0, 0]);
        ProxySocks5::encode_socket_addr(&mut buffer, &client_addr)?;
        buffer.put_slice(buf.chunk());

        // 因为已经建立了绑定, 所以直接发送
        inbound.send(buffer.chunk()).await?;
    }
}

至此客户端与服务端的UDP通讯已完成。

如何验证

接下来推荐一个工具brook他提供了命令行,可以完美的测试是否正确实现了SOCKS5的UDP功能。

testsocks5
doc 复制代码
Test UDP and TCP of socks5 server

--dns="": DNS server for connecting (default: 8.8.8.8:53)

--domain="": Domain for query (default: http3.ooo)

--password, -p="": Socks5 password

--socks5, -s="": Like: 127.0.0.1:1080

--username, -u="": Socks5 username

-a="": The A record of domain (default: 137.184.237.95)

所以我们此时开启服务器监听

bash 复制代码
wmproxy --user aaa --pass bbb -b 0.0.0.0 --udp 127.0.0.1

此时我们开启了一个代理,用户是aaa密码是bbb,udp是绑定127.0.0.1的一个代理,我们运行以下命令进行测试

bash 复制代码
brook testsocks5 -s 192.168.179.133:8090 --username aaa --password bbb

我们可以看下以下返回则表示成功

Testing TCP: query http3.ooo A on 8.8.8.8:53
TCP: OK

Testing UDP: query http3.ooo A on 8.8.8.8:53
2023/09/19 14:13:56 Sent Request: 0x5 0x3 0x0 0x1 []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0} []byte{0x0, 0x0}
2023/09/19 14:13:58 Got Reply: 0x5 0x0 0x0 0x1 []byte{0x7f, 0x0, 0x0, 0x1} []byte{0xd8, 0x3}
2023/09/19 14:13:58 Sent Datagram. []byte{0xba, 0xe4, 0x1, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x68, 0x74, 0x74, 0x70, 0x33, 0x3, 0x6f, 0x6f, 0x6f, 0x0, 0x0, 0x1, 0x0, 0x1}
2023/09/19 14:13:58 Got Datagram. data: []byte{0x0, 0x0} 0x0 0x1 []byte{0x8, 0x8, 0x8, 0x8} []byte{0x0, 0x35} []byte{0xba, 0xe4, 0x81, 0x80, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x5, 0x68, 0x74, 0x74, 0x70, 0x33, 0x3, 0x6f, 0x6f, 0x6f, 0x0, 0x0, 0x1, 0x0, 0x1, 0xc0, 0xc, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3c, 0x0, 0x4, 0x89, 0xb8, 0xed, 0x5f} datagram address: "8.8.8.8:53"
UDP: OK

他会进行TCP测试和UDP测试,如果看到UDP: OK则表示测试通过。

至此UDP功能已实现。