7. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP及TCP内网穿透原 ...

宁睿  金牌会员 | 2023-10-13 14:15:42 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 567|帖子 567|积分 1709

用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP及TCP内网穿透原理及运行篇

项目 ++wmproxy++

gite: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
内网、公网

内网:也叫做局域网,通常指单一的网络环境。例如你家里的路由器网络、网吧、公司网络、学校网络。网络大小不定,内网中的主机可以互联互通,但是越出这个局域网访问,就无法访问该网络中的主机。
公网:就是互联网,其实也可以看做一个扩大版的内网,比如叫城际网,省域网,国网。有单独的公网IP,任何其它地址可以访问网络的可以直接访问该IP,从而实现服务。
为什么要内网穿透

内网限制


  • IP不固定,通过家庭网,手机4G/5G访问的出口地址都是动态的,每次连接都会变化
  • 运营商通常会做NAT转化,从而实际上你访问的出口地址其实也是一个内网地址,如通常https://www.baidu.com/s?wd=ip查询地址
  • 常用端口无法使用,如80/443这类标准端口被直接限制不能使用。
公网优缺点


  • 服务器贵,带宽贵
  • IP固定,所有端口均可开放
  • 带宽稳定,基本上所有高防机房或者云厂商都能提供稳定的带宽
内网穿透的场景

场景1:开发人员本地调试接口

描述:线上项目有问题或者有某些新功能,必须进行Debug进行调试和测试。
特点:本地调试、网速要求低、需要HTTP或者HTTPS协议。
需求:必须本地,必须HTTP[S]网址。
场景2:公司或者家里的本地存储或者公司内部系统

描述:如外出进行工作,或者本地有大量的私有数据(敏感不适合上云),但是自己必须得进行访问,如git服务或者照片服务等
特点:需要远程能随时随地的访问,访问内容不确定,但是需要能提供
需求:要相对比较稳定的线路,但是带宽相对要求较低
场景3:私有服务器和小伙伴开黑

描述:把自己的电脑做服务器,有时候云上的主机配置相对较高点的一个月费用极高,所以需要本地做私有服务器,或者把自己当做一台训练机
特点:对稳定性要求不用太高的,可以提供相应的服务
TCP内网穿透的原理

内网IP无法直接被访问,所以此时需求

  • 内网服务器
  • 公网服务器,有公网IP
此时网络如下,如此外部用户就能访问到内网服务器的数据,此时内网穿透客户端及服务端是保持长连接以方便进行推送,本质上是长链接在转发数据而实现穿透功能
flowchart TD    C[内网服务器]|由穿透客户端连接到内网服务器|A    A[内网穿透客户端wmproxy]|建立连接/保持连接|B[内网穿透服务端wmproxy]    B|访问建立连接|D[外网用户]Rust实现内网穿透

wmproxy一款简单易用的内网穿透工具,简单示例如下:
客户端相关

客户端配置client.yaml
  1. # 连接服务端地址
  2. server: 127.0.0.1:8091
  3. # 连接服务端是否加密
  4. ts: true
  5. # 内网映射配置的数组
  6. mappings:
  7.   #将localhost的域名转发到本地的127.0.0.1:8080
  8.   - name: web
  9.     mode: http
  10.     local_addr: 127.0.0.1:8080
  11.     domain: localhost
  12.   #将tcp的流量无条件转到127.0.0.1:8080
  13.   - name: tcp
  14.     mode: tcp
  15.     local_addr: 127.0.0.1:8080
  16.     domain:
复制代码
启动客户端
  1. wmproxy -c config/client.yaml
复制代码
服务端相关

服务端配置server.yaml
  1. #绑定的ip地址
  2. bind_addr: 127.0.0.1:8091
  3. #代理支持的功能,1为http,2为https,4为socks5
  4. flag: 7
  5. #内网映射http绑定地址
  6. map_http_bind: 127.0.0.1:8001
  7. #内网映射tcp绑定地址
  8. map_tcp_bind: 127.0.0.1:8002
  9. #内网映射https绑定地址
  10. map_https_bind: 127.0.0.1:8003
  11. #内网映射的公钥证书,为空则是默认证书
  12. map_cert:
  13. #内网映射的私钥证书,为空则是默认证书
  14. map_key:
  15. #接收客户端是为是加密客户端
  16. tc: true
  17. #当前服务模式,server为服务端,client为客户端
  18. mode: server
复制代码
启动服务端
  1. wmproxy -c config/server.yaml
复制代码
测试实现

在本地的8080端口上启动了一个简单的http文件服务器
  1. http-server .
复制代码
http测试

此时,8001的端口是http内网穿透通过服务端映射到客户端,并指向到8080端口,此时若访问http://127.0.0.1:8001则会显示

http映射是根据域名做映射此时我们的域名是127.0.0.1,所以直接返回404无法访问
此时若访问http://localhost:8001,结果如下

我们就可以判定我们的内网转发成功了。
tcp测试

tcp就是在该端口上的流量无条件转发到另一个端口上,此时我们可以预测tcp映射与域名无关,我们在8002上转发到了8080上,此时我们访问http://127.0.0.1:8002和http://localhost:8002都可以得到一样的结果

此时tcp转发成功
源码实现

因为TLS连接与协议无关,只要把普通的TCP转成TLS,剩下的均和普通连接一样处理即可,那么,此时我们只需要处理TCP和HTTP的请求转发即可。
监听

在程序启动的时候看我们是否配置了相应的http/https/tcp的内网穿透转发,如果有我们对相应的端口做监听,此时如果我们是https转发,要配置相应的证书,将会对TcpStream升级为TlsStream
  1. let http_listener = if let Some(ls) = &self.option.map_http_bind {
  2.     Some(TcpListener::bind(ls).await?)
  3. } else {
  4.     None
  5. };
  6. let mut https_listener = if let Some(ls) = &self.option.map_https_bind {
  7.     Some(TcpListener::bind(ls).await?)
  8. } else {
  9.     None
  10. };
  11. let map_accept = if https_listener.is_some() {
  12.     let map_accept = self.option.get_map_tls_accept().await.ok();
  13.     if map_accept.is_none() {
  14.         let _ = https_listener.take();
  15.     }
  16.     map_accept
  17. } else {
  18.     None
  19. };
  20. let tcp_listener = if let Some(ls) = &self.option.map_tcp_bind {
  21.     Some(TcpListener::bind(ls).await?)
  22. } else {
  23.     None
  24. };
复制代码
转发相关代码,主要在两个类里,分别为trans/http.rs和trans/tcp.rs
在http里面需要预处理相关的头文件消息,


  • X-Forwarded-For添加IP信息,从而使内网可以知道访问的IP来源
  • Host,重写Host信息,让内网端如果配置负载均衡可以正确的定位到位置
  • Server,重写Server信息,让内网可以明确知道这个服务端的类型
http转发源码

以下为部分代码,后续将进行比较正规的HTTP服务,以适应HTTP2
  1. pub async fn process<T>(self, mut inbound: T) -> Result<(), ProxyError<T>>
  2. where
  3.     T: AsyncRead + AsyncWrite + Unpin,
  4. {
  5.     let mut request;
  6.     let host_name;
  7.     let mut buffer = BinaryMut::new();
  8.     loop {
  9.         // 省略读信息
  10.         request = webparse::Request::new();
  11.         // 通过该方法解析标头是否合法, 若是partial(部分)则继续读数据
  12.         // 若解析失败, 则表示非http协议能处理, 则抛出错误
  13.         // 此处clone为浅拷贝,不确定是否一定能解析成功,不能影响偏移
  14.         match request.parse_buffer(&mut buffer.clone()) {
  15.             Ok(_) => match request.get_host() {
  16.                 Some(host) => {
  17.                     host_name = host;
  18.                     break;
  19.                 }
  20.                 None => {
  21.                     if !request.is_partial() {
  22.                         Self::err_server_status(inbound, 503).await?;
  23.                         return Err(ProxyError::UnknownHost);
  24.                     }
  25.                 }
  26.             },
  27.             // 数据不完整,还未解析完,等待传输
  28.             Err(WebError::Http(HttpError::Partial)) => {
  29.                 continue;
  30.             }
  31.             Err(e) => {
  32.                 Self::err_server_status(inbound, 503).await?;
  33.                 return Err(ProxyError::from(e));
  34.             }
  35.         }
  36.     }
  37.     // 取得相关的host数据,对内网的映射端做匹配,如果未匹配到返回错误,表示不支持
  38.     {
  39.         let mut is_find = false;
  40.         let read = self.mappings.read().await;
  41.         for v in &*read {
  42.             if v.domain == host_name {
  43.                 is_find = true;
  44.             }
  45.         }
  46.         if !is_find {
  47.             Self::not_match_err_status(inbound, "no found".to_string()).await?;
  48.             return Ok(());
  49.         }
  50.     }
  51.     // 有新的内网映射消息到达,通知客户端建立对内网指向的连接进行双向绑定,后续做正规的http服务以支持拓展
  52.     let create = ProtCreate::new(self.sock_map, Some(host_name));
  53.     let (stream_sender, stream_receiver) = channel::<ProtFrame>(10);
  54.     let _ = self.sender_work.send((create, stream_sender)).await;
  55.    
  56.     // 创建传输端进行绑定
  57.     let mut trans = TransStream::new(inbound, self.sock_map, self.sender, stream_receiver);
  58.     trans.reader_mut().put_slice(buffer.chunk());
  59.     trans.copy_wait().await?;
  60.     // let _ = copy_bidirectional(&mut inbound, &mut outbound).await?;
  61.     Ok(())
  62. }
复制代码
tcp转发源码

tcp处理相对比较简单,因为我们无法确定协议里是哪个类型的源码,所以对我们来说,就是单纯的把接收的数据完全转发到新的端口里。以下是部分源码
  1. pub async fn process<T>(self, inbound: T) -> Result<(), ProxyError<T>>
  2. where
  3.     T: AsyncRead + AsyncWrite + Unpin,
  4. {
  5.     // 寻找是否有匹配的tcp转发协议,如果有,则进行转发,如果没有则丢弃数据
  6.     {
  7.         let mut is_find = false;
  8.         let read = self.mappings.read().await;
  9.         for v in &*read {
  10.             if v.mode == "tcp" {
  11.                 is_find = true;
  12.             }
  13.         }
  14.         if !is_find {
  15.             log::warn!("not found tcp client trans");
  16.             return Ok(());
  17.         }
  18.     }
  19.     // 通知客户端数据进行连接的建立,客户端的tcp配置只能存在有且只有一个,要不然无法确定转发源
  20.     let create = ProtCreate::new(self.sock_map, None);
  21.     let (stream_sender, stream_receiver) = channel::<ProtFrame>(10);
  22.     let _ = self.sender_work.send((create, stream_sender)).await;
  23.    
  24.     let trans = TransStream::new(inbound, self.sock_map, self.sender, stream_receiver);
  25.     trans.copy_wait().await?;
  26.     Ok(())
  27. }
复制代码
到此部分细节已基本调通,后续将优化http的处理相关,以方便支持http的头信息重写和tcp的错误信息将写入正确的日志,以方便进行定位。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

宁睿

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表