用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. Linux 下 PostgreSQL 源码编译安装

    由于微信不允许外部链接,你需要点击文章尾部左下角的 "阅读原文",才能访问文中链接. PostgreSQL 是一个功能强大的开源对象关系数据库管理系统(ORDBMS),它从伯克利写 ...

  2. 深入探索C++对象模型(Inside the C++ object model) -- 摘阅笔记(关于对象 - esp 1)

    Object Lessons 关于对象 在C语言中,"数据"和"处理数据的操作(函数)"是分开声明的,也就是说 ,语言本身并没有支持"数据和函数&qu ...

  3. 【城南 · LlamaIndex 教程】一文看懂LlamaIndex用法,为LLMs学习私有知识

    我是卷了又没卷,薛定谔的卷的AI算法工程师「陈城南」(全网平台同名)~ 担任某大厂的算法工程师,带来最新的前沿AI知识,分享 AI 有趣工具和实用玩法,包括 ChatGPT.AI绘图等,欢迎大家交流~ ...

  4. 人工智能政策@🤗: 回应美国国家电信和信息管理局 (NTIA) 关于人工智能问责制的评论请求

    6 月 12 日,Hugging Face 向美国国家电信和信息管理局 NTIA 提交了一份关于 AI 责任政策的信息请求回应.在我们的回应中,我们强调了文档和透明度规范在推动 AI 责任过程中的作用 ...

  5. IOS开发--UILabel的基本使用

    UILabel是iOS中用于显示静态文本的控件. 它的主要功能是:1. 显示一行或多行文本 UILabel可以用来显示单行或多行文本内容.通过设置numberOfLines属性可以控制文本显示的行数. ...

  6. 使用Ajax进行数据请求

    ​ 一.Ajax开源库有很多选择,大家可以根据需求自己选择 jQuery:jQuery是一个广泛应用的JavaScript库,它提供了简洁而强大的API来处理Ajax请求.通过$.ajax()方法或其 ...

  7. Windows系统使用Nginx部署Vue

    Nginx是什么? Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器 ,同时也提供了IMAP/POP3/SMTP服务.Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的R ...

  8. ObjectInputStream_报错问题

    报错: Exception in thread "main" java.io.StreamCorruptedException: invalid stream header: CE ...

  9. MAUI中Windows的标题栏颜色怎么设置

    如下图所示,MAUI中Windows下的标题栏是灰色的,如何设置颜色,找了很久,在GitHub上的issue中找到了答案, 找到/Platforms/Windows/App.xaml <maui ...

  10. 封装一个可以左右滑动的Blazor组件

    为什么要封装组件 最近写MAUI Blazor的时候,总是苦于对移动端没有什么好的支持,没有一个能左右滑动的tab切换组件. 既然没有,那就自己封装一个. 简单了解轮播图.tab切换的库之后,决定使用 ...