用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关联函数处理
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绑定篇的更多相关文章
- go: 如何编写一个正确的udp服务端
udp的服务端有一个大坑,即如果收包不及时,在系统缓冲写满后,将大量丢包. 在网上通常的示例中,一般在for循环中执行操作逻辑.这在生产环境将是一个隐患.是的,俺就翻车了. go强大简易的并发能力可以 ...
- 【实验 1-2】编写一个简单的 UDP 服务器和 UDPP 客户端程序。程序均为控制台程序窗口。
1.服务器 #include<winsock2.h> //包含头文件#include<stdio.h>#include<windows.h>#pragma comm ...
- 新的知识点来了-ES6 Proxy代理 和 去银行存款有什么关系?
ES给开发者提供了一个新特性:Proxy,就是代理的意思.也就是我们这一节要介绍的知识点. 以前,ATM还没有那么流行的时候(暴露年纪),我们去银行存款或者取款的时候,需要在柜台前排队,等柜台工作人员 ...
- Java实战_手把手编写记事本
Java运用SWT插件编写桌面记事本应用程序 可实现windows系统桌面记事本基本功能.傻瓜式教学,一步一步手把手操作.小白也可自己编写出完整的应用程序. 须要工具:Eclipse(带SWT插件) ...
- 使用 TUN 设备实现一个简单的 UDP 代理隧道
若要实现在 Linux 下的代理程序,方法有很多,比如看着 RFC 1928 来实现一个 socks5 代理并自行设置程序经过 socks5 代理等方式,下文是使用 Linux 提供的 tun/tap ...
- 自己写一个java.lang.reflect.Proxy代理的实现
前言 Java设计模式9:代理模式一文中,讲到了动态代理,动态代理里面用到了一个类就是java.lang.reflect.Proxy,这个类是根据代理内容为传入的接口生成代理用的.本文就自己写一个Pr ...
- 手把手教你编写一个具有基本功能的shell(已开源)
刚接触Linux时,对shell总有种神秘感:在对shell的工作原理有所了解之后,便尝试着动手写一个shell.下面是一个从最简单的情况开始,一步步完成一个模拟的shell(我命名之为wshell) ...
- 手把手教你编写一个简单的PHP模块形态的后门
看到Freebuf 小编发表的用这个隐藏于PHP模块中的rootkit,就能持久接管服务器文章,很感兴趣,苦无作者没留下PoC,自己研究一番,有了此文 0×00. 引言 PHP是一个非常流行的web ...
- Hexo+NexT(六):手把手教你编写一个Hexo过滤器插件
Hexo+NexT介绍到这里,我认为已经可以很好地完成任务了.它所提供的一些基础功能及配置,都已经进行了讲解.你已经可以随心所欲地配置一个自己的博客环境,然后享受码字的乐趣. 把博客托管到Github ...
- 从头开始编写一个Orchard网上商店模块(6) - 创建购物车服务和控制器
原文地址: http://skywalkersoftwaredevelopment.net/blog/writing-an-orchard-webshop-module-from-scratch-pa ...
随机推荐
- CentOS Linux 7 安全基线设置
作为一个生信人,不管是日常的数据分析还是其他工具应用的开发,服务器的安全始终是一个无法避免的话题.尤其是当我们拿到一台新的服务器,我们需要怎样才能确保它是安全可靠,并最小限度降低它被攻击的可能性? 下 ...
- 在DevExpress的GridView的列中,使用RepositoryItemSearchLookUpEdit控件实现产品列表信息的展示和选择
有时候,我们为了方便,我们往往使用扩展函数的代码方式创建很多GridView的操作功能,如在随笔<在DevExpress中使用BandedGridView表格实现多行表头的处理>中介绍过多 ...
- KL变换
covariance 指两个变量的相关性:cov(x, y) =E(x y) - E(x) E(y) cov(x, y) < 0 负相关 cov(x, y) = 0 无关 cov(x, y) & ...
- Pytorch-如何在模型中引入可学习参数
错误实例: def init(self): self.w1 = torch.nn.Parameter(torch.FloatTensor(1),requires_grad=True).cuda() s ...
- 曲线艺术编程 coding curves 第七章 抛物线(Parabolas)
抛物线 Parabolas 原作:Keith Peters https://www.bit-101.com/blog/2022/11/coding-curves/ 译者:池中物王二狗(sheldon) ...
- 区块链应用:椭圆曲线数字签名算法ECDSA
1 椭圆曲线密码学 椭圆曲线密码学(Elliptic Curve Cryptography,缩写ECC),是基于椭圆曲线数学理论实现的一种非对称加密算法.椭圆曲线在密码学中的使用是在1985年有Nea ...
- 除了参数,ref关键字还可以用在什么地方?
<老生常谈:值类型 V.S. 引用类型>中花了很大的篇幅介绍ref参数针对值类型和引用类型变量的传递.在C#中,除了方法的ref参数,我们还有很多使用ref关键字传递引用/地址的场景,本篇 ...
- 深入JS——理解闭包可以看作是某种意义上的重生
JS中有一个非常重要但又难以完全掌握的概念,那就是闭包.很多JS程序员自以为已经掌握了闭包,但实质上是一知半解,就像"JS中万物皆为对象"这个常见的错误说法一样,很多前端开发者到现 ...
- Redis的五大数据类型及其使用场景
前言 redis是一个非常快速的非关系数据库解决方案.其简单的键值数据模型使 Redis 能够处理大型数据集,同时保持令人印象深刻的读写速度和可用性.redis提供了五种数据类型,分别是是: ...
- SQL Server 配置允许远程连接
前言 需要别人远程你的数据库,首先需要的是在一个局域网内,或者连接的是同一个路由器,接下来就是具体步骤: 1.首先是要检查SQLServer数据库服务器中是否允许远程链接.其具体操作为: 1. 打开数 ...