用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关联函数处理
pub async fn udp_execute_assoc(
mut stream: TcpStream,
proxy_addr: SocketAddr,
bind_ip: IpAddr,
) -> ProxyResult<()> {
}
  • 此刻我们对UDP进行监听,得到相应的端口,然后再将传入的bind_ip和端口返回给tcp的客户端,让客户端建立udp与当前的端口绑定
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端口
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多个接收
// 使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()收到消息则表示需要关闭此关联。以下是代理接收请求
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?;
}
}
  • 代理方收到远程的消息 添加头发送到客户端
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
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)

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

wmproxy --user aaa --pass bbb -b 0.0.0.0 --udp 127.0.0.1

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

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功能已实现。

用Rust手把手编写一个Proxy(代理), UDP绑定篇的更多相关文章

  1. go: 如何编写一个正确的udp服务端

    udp的服务端有一个大坑,即如果收包不及时,在系统缓冲写满后,将大量丢包. 在网上通常的示例中,一般在for循环中执行操作逻辑.这在生产环境将是一个隐患.是的,俺就翻车了. go强大简易的并发能力可以 ...

  2. 【实验 1-2】编写一个简单的 UDP 服务器和 UDPP 客户端程序。程序均为控制台程序窗口。

    1.服务器 #include<winsock2.h> //包含头文件#include<stdio.h>#include<windows.h>#pragma comm ...

  3. 新的知识点来了-ES6 Proxy代理 和 去银行存款有什么关系?

    ES给开发者提供了一个新特性:Proxy,就是代理的意思.也就是我们这一节要介绍的知识点. 以前,ATM还没有那么流行的时候(暴露年纪),我们去银行存款或者取款的时候,需要在柜台前排队,等柜台工作人员 ...

  4. Java实战_手把手编写记事本

    Java运用SWT插件编写桌面记事本应用程序 可实现windows系统桌面记事本基本功能.傻瓜式教学,一步一步手把手操作.小白也可自己编写出完整的应用程序. 须要工具:Eclipse(带SWT插件) ...

  5. 使用 TUN 设备实现一个简单的 UDP 代理隧道

    若要实现在 Linux 下的代理程序,方法有很多,比如看着 RFC 1928 来实现一个 socks5 代理并自行设置程序经过 socks5 代理等方式,下文是使用 Linux 提供的 tun/tap ...

  6. 自己写一个java.lang.reflect.Proxy代理的实现

    前言 Java设计模式9:代理模式一文中,讲到了动态代理,动态代理里面用到了一个类就是java.lang.reflect.Proxy,这个类是根据代理内容为传入的接口生成代理用的.本文就自己写一个Pr ...

  7. 手把手教你编写一个具有基本功能的shell(已开源)

    刚接触Linux时,对shell总有种神秘感:在对shell的工作原理有所了解之后,便尝试着动手写一个shell.下面是一个从最简单的情况开始,一步步完成一个模拟的shell(我命名之为wshell) ...

  8. 手把手教你编写一个简单的PHP模块形态的后门

    看到Freebuf 小编发表的用这个隐藏于PHP模块中的rootkit,就能持久接管服务器文章,很感兴趣,苦无作者没留下PoC,自己研究一番,有了此文 0×00. 引言 PHP是一个非常流行的web ...

  9. Hexo+NexT(六):手把手教你编写一个Hexo过滤器插件

    Hexo+NexT介绍到这里,我认为已经可以很好地完成任务了.它所提供的一些基础功能及配置,都已经进行了讲解.你已经可以随心所欲地配置一个自己的博客环境,然后享受码字的乐趣. 把博客托管到Github ...

  10. 从头开始编写一个Orchard网上商店模块(6) - 创建购物车服务和控制器

    原文地址: http://skywalkersoftwaredevelopment.net/blog/writing-an-orchard-webshop-module-from-scratch-pa ...

随机推荐

  1. 【论文阅读】Uformer:A General U-Shaped Transformer for Image Restoration

    前言 博客主页:睡晚不猿序程 首发时间:2023.6.8 最近更新时间:2023.6.8 本文由 睡晚不猿序程 原创 作者是蒻蒟本蒟,如果文章里有任何错误或者表述不清,请 tt 我,万分感谢!orz ...

  2. 一次性掌握innodb引擎如何解决幻读和不可重复读

    了解mysql的都知道,在mysql的RR(可重复)隔离级别下解决了幻读和不可重复.你知道RR下是怎么解决的吗,很多人会回答是通过MVCC和next-key解决的,具体是怎么解决的,今天来重点分析下. ...

  3. Windows/DOS与Unix文件格式之间的相互转换(/r/n问题)

    PS:今天遇到一个文件转换问题,现在将网上搜索到资料贴出来.. 第一个资料 Windows/DOS与Unix文件格式是不同的,问题一般就是出在/r/n问题上. 回车(CR)和换行(LF)符都是用来表示 ...

  4. httpx的两个坑(httpx.ReadTimeout; SSL: CERTIFICATE_VERIFY_FAILED)

    关于python异步网络请求库httpx的两个坑 其一:httpx.ReadTimeout 实测发现,网络不稳定的情况下,极其容易出现该错误. 相对于requests库, httpx库是有默认的超时时 ...

  5. Hugging News #0710: 体验 MusicGen、Diffusers 库发布一周年、我们的内容政策更新

    每一周,我们的同事都会向社区的成员们发布一些关于 Hugging Face 相关的更新,包括我们的产品和平台更新.社区活动.学习资源和内容更新.开源库和模型更新等,我们将其称之为「Hugging Ne ...

  6. 即构 SDK 6月迭代:新增拉流画面镜像等功能,为开发者提供更大便利

    即构SDK6月新版本已上线,本月SDK迭代主要新增了拉流画面镜像功能,媒体播放器新增支持缓存相关的设置,新增支持设置对焦模式和曝光模式等功能,多个功能模块的灵活设置,让开发者能更便利的自定义选择,为用 ...

  7. 行行AI人才直播第10期:CTC智仝咨询联合创始人王发鑫《AI时代职场进阶之路——资深猎头的职场洞见》

    当今AI技术正在快速渗透各个行业,从去年热门的AIGC到今年爆火的ChatGPT,人工智能高速发展让人惊叹的同时,也让"算法取代人类"."AI或带来失业潮"等老 ...

  8. redis 使用 nologin 用户启动

    添加不可登录的redis用户 sudo useradd -M -s /sbin/nologin redis 为redis新建目录,并设置属性 mkdir -p /data/redis &&am ...

  9. python连接数据库及查询包含中文错误解决方法

    使用MySQLdb库来连接数据库 import MySQLdb conn = MySQLdb.connect(host='127.0.0.1', user='root', passwd='', por ...

  10. 【算法】单调栈 & 单调队列学习笔记

    1. 单调栈简介 1.1 前言 今天是 2023/1/15,一中寒假集训阶段性的结束了.集训的学习笔记可以在本人 blogs 的[算法]标签栏中找. 马上就要过年了,提前祝大家新年快乐! 1.2 什么 ...