6. 用Rust手把手编写一个wmproxy(代理,内网穿透等), 通讯协议源码解读篇
用Rust手把手编写一个wmproxy(代理,内网穿透等), 通讯协议源码解读篇
项目 ++wmproxy++
gite: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
事件模型的选取
- OS线程, 简单的一个IO对应一个系统级别的线程,通常单进程创建的线程数是有限的,在线程与线程间同步数据会相当困难,线程间的调度争用会相当损耗效率,不适合IO密集的场景。
- 事件驱动(Event driven), 事件驱动基本上是最早的高并发的IO密集型的编程模式了,如C++的libevent,RUST的MIO,通过监听IO的可读可写从而进行编程设计,缺点通常跟回调( Callback )一起使用,如果使用不好,回调层级过多会有回调地狱的风险。
- 协程(Coroutines) 可能是目前比较火的并发模型,火遍全球的Go语言的协程设计就非常优秀。协程跟线程类似,无需改变编程模型,同时它也跟async类似,可以支持大量的任务并发运行。
- actor模型 是erlang的杀手锏之一,它将所有并发计算分割成一个一个单元,这些单元被称为actor,单元之间通过消息传递的方式进行通信和数据传递,跟分布式系统的设计理念非常相像。由于actor模型跟现实很贴近,因此它相对来说更容易实现,但是一旦遇到流控制、失败重试等场景时,就会变得不太好用
- async/await, 该模型为异步编辑模型,async模型的问题就是内部实现机制过于复杂,对于用户来说,理解和使用起来也没有线程和协程简单。主要是等待完成状态await,就比如读socket数据,等待系统将数据送达再继续触发读操作的执行,从而答到无损耗的运行。
这里我们选择的是
async/await的模式
Rust中的async
- Future 在 Rust 中是惰性的,只有在被轮询(poll)时才会运行, 因此丢弃一个future会阻止它未来再被运行, 你可以将Future理解为一个在未来某个时间点被调度执行的任务。在Rust中调用异步函数没有用await会被编辑器警告,因为这不符合预期。
- Async 在 Rust 中使用开销是零, 意味着只有你能看到的代码(自己的代码)才有性能损耗,你看不到的(async内部实现)都没有性能损耗,例如,你可以无需分配任何堆内存、也无需任何动态分发来使用async,这对于热点路径的性能有非常大的好处,正是得益于此,Rust 的异步编程性能才会这么高。
- Rust 异步运行时,Rust社区生态中已经提供了非常优异的运行时实现例如tokio,官方版本的async目前的生态相对tokio会差许多
- 运行时同时支持单线程和多线程
流代码的封装
跟数据通讯相关的代码均放在
streams目录下面。
- center_client.rs中的- CenterClient表示中心客户端,提供主动连接服务端的能力并可选择为加密(- TLS)或者普通模式,并且将该客户端收发的消息转给服务端
- center_server.rs中的- CenterServer表示中心服务端,接受中心客户端的连接,并且将信息处理或者转发
- trans_stream.rs中的- TransStream表示转发流量端,提供与中心端绑定的读出写入功能,在代理服务器中客户端接收的连接因为无需处理任何数据,直接绑定为- TransStream将数据完整的转发给服务端
- virtual_stream.rs中的- VirtualStream表示虚拟端,虚拟出一个流连接,并实现AsyncRead及AsyncRead,可以和流一样正常操作,在代理服务器中服务端接收到新连接,把他虚拟成一个- VirtualStream,就可以直接和他连接的服务器上做双向绑定。
几种流式在代码中的转化
HTTP代理
下面展示的是http代理,通过加密TLS中的转化
A[TcpStream请求到代理]<-->|建立连接/明文|B[代理转化成TransStream]
B<-->|转发到/内部|C[中心客户端]
C<-->|建立加密连接/加密|D[TlsStream< TcpStream>绑定中心服务端]
D<-->|收到Create/内部|E[虚拟出VirtualStream]
E<-->|解析到host并连接/明文|F[TcpStream连接到http服务器]
上述过程实现了程序中实现了http的代理转发
HTTP内网穿透
以下是http内网穿透在代理中的转化
A[服务端绑定http对外端口]<-->|接收连接/明文|B[外部的TcpStream]
B<-->|转发到/内部|C[中心服务端并绑定TransStream]
C<-->|通过客户的加密连接推送/加密|D[TlsStream< TcpStream>绑定中心客户端]
D<-->|收到Create/内部|E[虚拟出VirtualStream]
E<-->|解析对应的连接信息/明文|F[TcpStream连接到内网的http服务器]
上述过程可以主动把公网的请求连接转发到内网,由内网提供完服务后再转发到公网的请求,从而实现内网穿透。
流代码的介绍
CenterClient中心客端
下面是代码类的定义
/// 中心客户端
/// 负责与服务端建立连接,断开后自动再重连
pub struct CenterClient {
    /// tls的客户端连接信息
    tls_client: Option<Arc<rustls::ClientConfig>>,
    /// tls的客户端连接域名
    domain: Option<String>,
    /// 连接中心服务器的地址
    server_addr: SocketAddr,
    /// 内网映射的相关消息
    mappings: Vec<MappingConfig>,
    /// 存在普通连接和加密连接,此处不为None则表示普通连接
    stream: Option<TcpStream>,
    /// 存在普通连接和加密连接,此处不为None则表示加密连接
    tls_stream: Option<TlsStream<TcpStream>>,
    /// 绑定的下一个sock_map映射
    next_id: u32,
    /// 发送Create,并将绑定的Sender发到做绑定
    sender_work: Sender<(ProtCreate, Sender<ProtFrame>)>,
    /// 接收的Sender绑定,开始服务时这值move到工作协程中,所以不能二次调用服务
    receiver_work: Option<Receiver<(ProtCreate, Sender<ProtFrame>)>>,
    /// 发送协议数据,接收到服务端的流数据,转发给相应的Stream
    sender: Sender<ProtFrame>,
    /// 接收协议数据,并转发到服务端。
    receiver: Option<Receiver<ProtFrame>>,
}
主要的逻辑流程,循环监听数据流的到达,同时等待多个异步的到达,这里用的是
tokio::select!宏
loop {
    let _ = tokio::select! {
        // 严格的顺序流
        biased;
        // 新的流建立,这里接收Create并进行绑定
        r = receiver_work.recv() => {
            if let Some((create, sender)) = r {
                map.insert(create.sock_map(), sender);
                let _ = create.encode(&mut write_buf);
            }
        }
        // 数据的接收,并将数据写入给远程端
        r = receiver.recv() => {
            if let Some(p) = r {
                let _ = p.encode(&mut write_buf);
            }
        }
        // 数据的等待读取,一旦流可读则触发,读到0则关闭主动关闭所有连接
        r = reader.read(&mut vec) => {
            match r {
                Ok(0)=>{
                    is_closed=true;
                    break;
                }
                Ok(n) => {
                    read_buf.put_slice(&vec[..n]);
                }
                Err(_err) => {
                    is_closed = true;
                    break;
                },
            }
        }
        // 一旦有写数据,则尝试写入数据,写入成功后扣除相应的数据
        r = writer.write(write_buf.chunk()), if write_buf.has_remaining() => {
            match r {
                Ok(n) => {
                    write_buf.advance(n);
                    if !write_buf.has_remaining() {
                        write_buf.clear();
                    }
                }
                Err(e) => {
                    println!("center_client errrrr = {:?}", e);
                },
            }
        }
    };
    loop {
        // 将读出来的数据全部解析成ProtFrame并进行相应的处理,如果是0则是自身消息,其它进行转发
        match Helper::decode_frame(&mut read_buf)? {
            Some(p) => {
                match p {
                    ProtFrame::Create(p) => {
                    }
                    ProtFrame::Close(_) | ProtFrame::Data(_) => {
                    },
                }
            }
            None => {
                break;
            }
        }
    }
}
CenterServer中心服务端
下面是代码类的定义
/// 中心服务端
/// 接受中心客户端的连接,并且将信息处理或者转发
pub struct CenterServer {
    /// 代理的详情信息,如用户密码这类
    option: ProxyOption,
    /// 发送协议数据,接收到服务端的流数据,转发给相应的Stream
    sender: Sender<ProtFrame>,
    /// 接收协议数据,并转发到服务端。
    receiver: Option<Receiver<ProtFrame>>,
    /// 发送Create,并将绑定的Sender发到做绑定
    sender_work: Sender<(ProtCreate, Sender<ProtFrame>)>,
    /// 接收的Sender绑定,开始服务时这值move到工作协程中,所以不能二次调用服务
    receiver_work: Option<Receiver<(ProtCreate, Sender<ProtFrame>)>>,
    /// 绑定的下一个sock_map映射,为双数
    next_id: u32,
}
主要的逻辑流程,循环监听数据流的到达,同时等待多个异步的到达,这里用的是
tokio::select!宏,select处理方法与Client相同,均处理相同逻辑,不同的是接收数据包后数据端是处理的proxy的请求,而Client处理的是内网穿透的逻辑
loop {
    // 将读出来的数据全部解析成ProtFrame并进行相应的处理,如果是0则是自身消息,其它进行转发
    match Helper::decode_frame(&mut read_buf)? {
        Some(p) => {
            match p {
                ProtFrame::Create(p) => {
                    tokio::spawn(async move {
                        let _ = Proxy::deal_proxy(stream, flag, username, password, udp_bind).await;
                    });
                }
                ProtFrame::Close(_) | ProtFrame::Data(_) => {
                },
            }
        }
        None => {
            break;
        }
    }
}
TransStream转发流量端
下面是代码类的定义
/// 转发流量端
/// 提供与中心端绑定的读出写入功能
pub struct TransStream<T>
where
    T: AsyncRead + AsyncWrite + Unpin,
{
    // 流有相应的AsyncRead + AsyncWrite + Unpin均可
    stream: T,
    // sock绑定的句柄
    id: u32,
    // 读取的数据缓存,将转发成ProtFrame
    read: BinaryMut,
    // 写的数据缓存,直接写入到stream下,从ProtFrame转化而来
    write: BinaryMut,
    // 收到数据通过sender发送给中心端
    in_sender: Sender<ProtFrame>,
    // 收到中心端的写入请求,转成write
    out_receiver: Receiver<ProtFrame>,
}
主要的逻辑流程,循环监听数据流的到达,同时等待多个异步的到达,这里用的是
tokio::select!宏,监听的对象有stream可读,可写,sender的写发送及receiver的可接收
loop {
    // 有剩余数据,优先转化成Prot,因为数据可能从外部直接带入
    if self.read.has_remaining() {
        link.push_back(ProtFrame::new_data(self.id, self.read.copy_to_binary()));
        self.read.clear();
    }
    tokio::select! {
        n = reader.read(&mut buf) => {
            let n = n?;
            if n == 0 {
                return Ok(())
            } else {
                self.read.put_slice(&buf[..n]);
            }
        },
        r = writer.write(self.write.chunk()), if self.write.has_remaining() => {
            match r {
                Ok(n) => {
                    self.write.advance(n);
                    if !self.write.has_remaining() {
                        self.write.clear();
                    }
                }
                Err(_) => todo!(),
            }
        }
        r = self.out_receiver.recv() => {
            if let Some(v) = r {
                if v.is_close() || v.is_create() {
                    return Ok(())
                } else if v.is_data() {
                    match v {
                        ProtFrame::Data(d) => {
                            self.write.put_slice(&d.data().chunk());
                        }
                        _ => unreachable!(),
                    }
                }
            } else {
                return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid frame"))
            }
        }
        p = self.in_sender.reserve(), if link.len() > 0 => {
            match p {
                Err(_)=>{
                    return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid frame"))
                }
                Ok(p) => {
                    p.send(link.pop_front().unwrap())
                },
            }
        }
    }
VirtualStream虚拟端
下面是代码类的定义,我们并未有真实的socket,通过虚拟出的端方便后续的操作
/// 虚拟端
/// 虚拟出一个流连接,并实现AsyncRead及AsyncRead,可以和流一样正常操作
pub struct VirtualStream
{
    // sock绑定的句柄
    id: u32,
    // 收到数据通过sender发送给中心端
    sender: PollSender<ProtFrame>,
    // 收到中心端的写入请求,转成write
    receiver: Receiver<ProtFrame>,
    // 读取的数据缓存,将转发成ProtFrame
    read: BinaryMut,
    // 写的数据缓存,直接写入到stream下,从ProtFrame转化而来
    write: BinaryMut,
}
虚拟的流主要通过实现AsyncRead及AsyncWrite
impl AsyncRead for VirtualStream
{
    // 有读取出数据,则返回数据,返回数据0的Ready状态则表示已关闭
    fn poll_read(
        mut self: std::pin::Pin<&mut Self>,
        cx: &mut [std](https://note.youdao.com/)[link](https://note.youdao.com/)::task::Context<'_>,
        buf: &mut tokio::io::ReadBuf<'_>,
    ) -> std::task::Poll<std::io::Result<()>> {
        loop {
            match self.receiver.poll_recv(cx) {
                Poll::Ready(value) => {
                    if let Some(v) = value {
                        if v.is_close() || v.is_create() {
                            return Poll::Ready(Ok(()))
                        } else if v.is_data() {
                            match v {
                                ProtFrame::Data(d) => {
                                    self.read.put_slice(&d.data().chunk());
                                }
                                _ => unreachable!(),
                            }
                        }
                    } else {
                        return Poll::Ready(Ok(()))
                    }
                },
                Poll::Pending => {
                    if !self.read.has_remaining() {
                        return Poll::Pending;
                    }
                },
            }
            if self.read.has_remaining() {
                let copy = std::cmp::min(self.read.remaining(), buf.remaining());
                buf.put_slice(&self.read.chunk()[..copy]);
                self.read.advance(copy);
                return Poll::Ready(Ok(()));
            }
        }
    }
}
impl AsyncWrite for VirtualStream
{
    fn poll_write(
        mut self: Pin<&mut Self>,
        cx: &mut std::task::Context<'_>,
        buf: &[u8],
    ) -> std::task::Poll<Result<usize, std::io::Error>> {
        self.write.put_slice(buf);
        if let Err(_) = ready!(self.sender.poll_reserve(cx)) {
            return Poll::Pending;
        }
        let binary = Binary::from(self.write.chunk().to_vec());
        let id = self.id;
        if let Ok(_) = self.sender.send_item(ProtFrame::Data(ProtData::new(id, binary))) {
            self.write.clear();
        }
        Poll::Ready(Ok(buf.len()))
    }
}
至此基本几个大类已设置完毕,接下来仅需简单的拓展就能实现内网穿透功能。
6. 用Rust手把手编写一个wmproxy(代理,内网穿透等), 通讯协议源码解读篇的更多相关文章
- 借助FRP反向代理实现内网穿透
		一.frp 是什么? frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP.UDP.HTTP.HTTPS 等多种协议.可以将内网服务以安全.便捷的方式通过具有公网 IP 节点的中转暴露到公 ... 
- 分享一个内网穿透工具frp
		首先简单介绍一下内网穿透: 内网穿透:通过公网,访问局域网里的IP地址与端口,这需要将局域网里的电脑端口映射到公网的端口上:这就需要用到反向代理,即在公网服务器上必须运行一个服务程序,然后在局域网中需 ... 
- Alamofire源码解读系列(六)之Task代理(TaskDelegate)
		本篇介绍Task代理(TaskDelegate.swift) 前言 我相信可能有80%的同学使用AFNetworking或者Alamofire处理网络事件,并且这两个框架都提供了丰富的功能,我也相信很 ... 
- 【代理】内网穿透工具 frp&frps
		frp 是一个高性能的反向代理应用,可以帮助您轻松地进行内网穿透,对外网提供服务,支持 tcp, http, https 等协议类型,并且 web 服务支持根据域名进行路由转发. ### frp 的作 ... 
- frp实现基于反向代理的内网穿透
		个人博客主页: xzajyjs.cn frp是什么 简单地说,frp就是一个反向代理软件,它体积轻量但功能很强大,可以使处于内网或防火墙后的设备对外界提供服务,它支持HTTP.TCP.UDP等众多协议 ... 
- 【新晋开源项目】内网穿透神器[中微子代理] 加入 Dromara 开源社区
		1.关于作者 dromara开源组织成员,dromara/neutrino-proxy项目作者 名称:傲世孤尘.雨韵诗泽 名言: 扎根土壤,心向太阳.积蓄能量,绽放微光. 拘浊酒邀明月,借赤日暖苍穹. ... 
- 如何判断一个Http Message的结束——python源码解读
		HTTP/1.1 默认的连接方式是长连接,不能通过简单的TCP连接关闭判断HttpMessage的结束. 以下是几种判断HttpMessage结束的方式: 1. HTTP协议约定status ... 
- 代理内网上网-iptables
		代理内网上网-iptables 1.1 环境说明 主机A:(能上网) ip:内172.16.1.7/24 外10.0.0.7/24 系统CentOS 6.9 主机B:(不能上网) ip:内172.16 ... 
- koa2源码解读及实现一个简单的koa2框架
		阅读目录 一:封装node http server. 创建koa类构造函数. 二:构造request.response.及 context 对象. 三:中间件机制的实现. 四:错误捕获和错误处理. k ... 
- java 动态代理深度学习(Proxy,InvocationHandler),含$Proxy0源码
		java 动态代理深度学习, 一.相关类及其方法: java.lang.reflect.Proxy,Proxy 提供用于创建动态代理类和实例的静态方法.newProxyInstance()返回一个指定 ... 
随机推荐
- 【Python入门教程】Python常用表格函数&操作(xlrd、xlwt、openpyxl、xlwings)
			 在我们使用Python时,避免不了与Excel打交道.同样Python的三方库和代码的简洁性也为我们处理大数据提供了便利.今天给大家介绍一下常用的处理表格的函数,同时还有一些常用的 ... 
- MySQL读取的记录和我想象的不一致
			摘要:并发的事务在运行过程中会出现一些可能引发一致性问题的现象,本篇将详细分析一下. 本文分享自华为云社区<MySQL读取的记录和我想象的不一致--事物隔离级别和MVCC>,作者:砖业洋_ ... 
- rust cargo build一直出现 Blocking waiting for file lock on package cache
			如果确定没有多个程序占用,可以删除rm -rf ~/.cargo/.package-cache,然后再执行 
- Custom directive is missing corresponding SSR transform and will be ignored
			背景 最近在给业务组件库集成指令库,将各个项目中常用的指令如一键复制.元素和弹窗拖拽等封装到一起,进行统一发版维护. 业务组件库项目架构采用的是pnpm+vite+vue3+vitepress,其中v ... 
- To ChatGPT:让你更加随意地使用所有ChatGPT应用
			现在其实已经有很多在线的llm服务了,当然也存在许多开源部署方案,但是不知道大家有没有发现一个问题,目前基于ChatGPT开发的应用,都是使用的OpenAI的接口.换句话说,如果没有OpenAI账号, ... 
- 密码学概念科普(加密算法、数字签名、散列函数、HMAC)
			密码散列函数 密码散列函数 (Cryptographic hash function),是一个单向函数,输入消息,输出摘要.主要特点是: 只能根据消息计算摘要,很难根据摘要反推消息 改变消息,摘要一定 ... 
- 基于C# 开发的SOL SERVER 操作数据库类(SQLHelp)
			说明:以下是我近两年年来开发中最常用的C#操作sql server数据库访问类,对初学者非常有用,容易扩展,支持多库操作,多研究研究,有什么问题欢迎留言 当前环境为 C# .NET CORE 3.0 ... 
- influxdb 保留策略
			转载请注明出处: InfluxDB 中的保留策略用于定义时间序列数据在数据库中的保留期限.保留策略决定了数据在 InfluxDB 中的存储持续时间和精度.以下是 InfluxDB 的保留策略类型以及如 ... 
- Win32编程
			WIN32 malloc函数的底层实现是Win32API 字符编码 原始的ASCII编码最多能表示127个符号 0-7F(十六进制) 缺点:表示的符号太少了 ASCII编码的扩展:GB2312或GB2 ... 
- DataGridView 控件分页
			在使用Winform开发桌面应用时,工具箱预先提供了丰富的基础控件,利用这些基础控件可以开展各类项目的开发.但是或多或少都会出现既有控件无法满足功能需求的情况,或者在开发类似项目时,我们希望将具有相同 ... 
