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

项目 ++wmproxy++

gite: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

内网、公网

内网:也叫做局域网,通常指单一的网络环境。例如你家里的路由器网络、网吧、公司网络、学校网络。网络大小不定,内网中的主机可以互联互通,但是越出这个局域网访问,就无法访问该网络中的主机。

公网:就是互联网,其实也可以看做一个扩大版的内网,比如叫城际网,省域网,国网。有单独的公网IP,任何其它地址可以访问网络的可以直接访问该IP,从而实现服务。

为什么要内网穿透

内网限制

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

公网优缺点

  1. 服务器贵,带宽贵
  2. IP固定,所有端口均可开放
  3. 带宽稳定,基本上所有高防机房或者云厂商都能提供稳定的带宽

内网穿透的场景

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

描述:线上项目有问题或者有某些新功能,必须进行Debug进行调试和测试。

特点:本地调试、网速要求低、需要HTTP或者HTTPS协议。

需求:必须本地,必须HTTP[S]网址。

场景2:公司或者家里的本地存储或者公司内部系统

描述:如外出进行工作,或者本地有大量的私有数据(敏感不适合上云),但是自己必须得进行访问,如git服务或者照片服务等

特点:需要远程能随时随地的访问,访问内容不确定,但是需要能提供

需求:要相对比较稳定的线路,但是带宽相对要求较低

场景3:私有服务器和小伙伴开黑

描述:把自己的电脑做服务器,有时候云上的主机配置相对较高点的一个月费用极高,所以需要本地做私有服务器,或者把自己当做一台训练机

特点:对稳定性要求不用太高的,可以提供相应的服务

TCP内网穿透的原理

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

  1. 内网服务器
  2. 公网服务器,有公网IP

此时网络如下,如此外部用户就能访问到内网服务器的数据,此时内网穿透客户端及服务端是保持长连接以方便进行推送,本质上是长链接在转发数据而实现穿透功能

flowchart TD
C[内网服务器]<-->|由穿透客户端连接到内网服务器|A
A[内网穿透客户端wmproxy]<-->|建立连接/保持连接|B[内网穿透服务端wmproxy]
B<-->|访问建立连接|D[外网用户]

Rust实现内网穿透

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

客户端相关

客户端配置client.yaml

# 连接服务端地址
server: 127.0.0.1:8091
# 连接服务端是否加密
ts: true # 内网映射配置的数组
mappings:
#将localhost的域名转发到本地的127.0.0.1:8080
- name: web
mode: http
local_addr: 127.0.0.1:8080
domain: localhost
#将tcp的流量无条件转到127.0.0.1:8080
- name: tcp
mode: tcp
local_addr: 127.0.0.1:8080
domain:

启动客户端

wmproxy -c config/client.yaml

服务端相关

服务端配置server.yaml

#绑定的ip地址
bind_addr: 127.0.0.1:8091
#代理支持的功能,1为http,2为https,4为socks5
flag: 7
#内网映射http绑定地址
map_http_bind: 127.0.0.1:8001
#内网映射tcp绑定地址
map_tcp_bind: 127.0.0.1:8002
#内网映射https绑定地址
map_https_bind: 127.0.0.1:8003
#内网映射的公钥证书,为空则是默认证书
map_cert:
#内网映射的私钥证书,为空则是默认证书
map_key:
#接收客户端是为是加密客户端
tc: true
#当前服务模式,server为服务端,client为客户端
mode: server

启动服务端

wmproxy -c config/server.yaml

测试实现

在本地的8080端口上启动了一个简单的http文件服务器

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:8002http://localhost:8002都可以得到一样的结果

此时tcp转发成功

源码实现

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

监听

在程序启动的时候看我们是否配置了相应的http/https/tcp的内网穿透转发,如果有我们对相应的端口做监听,此时如果我们是https转发,要配置相应的证书,将会对TcpStream升级为TlsStream<TcpStream>

let http_listener = if let Some(ls) = &self.option.map_http_bind {
Some(TcpListener::bind(ls).await?)
} else {
None
};
let mut https_listener = if let Some(ls) = &self.option.map_https_bind {
Some(TcpListener::bind(ls).await?)
} else {
None
}; let map_accept = if https_listener.is_some() {
let map_accept = self.option.get_map_tls_accept().await.ok();
if map_accept.is_none() {
let _ = https_listener.take();
}
map_accept
} else {
None
};
let tcp_listener = if let Some(ls) = &self.option.map_tcp_bind {
Some(TcpListener::bind(ls).await?)
} else {
None
};

转发相关代码,主要在两个类里,分别为trans/http.rstrans/tcp.rs

http里面需要预处理相关的头文件消息,

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

http转发源码

以下为部分代码,后续将进行比较正规的HTTP服务,以适应HTTP2

pub async fn process<T>(self, mut inbound: T) -> Result<(), ProxyError<T>>
where
T: AsyncRead + AsyncWrite + Unpin,
{
let mut request;
let host_name;
let mut buffer = BinaryMut::new();
loop {
// 省略读信息
request = webparse::Request::new();
// 通过该方法解析标头是否合法, 若是partial(部分)则继续读数据
// 若解析失败, 则表示非http协议能处理, 则抛出错误
// 此处clone为浅拷贝,不确定是否一定能解析成功,不能影响偏移
match request.parse_buffer(&mut buffer.clone()) {
Ok(_) => match request.get_host() {
Some(host) => {
host_name = host;
break;
}
None => {
if !request.is_partial() {
Self::err_server_status(inbound, 503).await?;
return Err(ProxyError::UnknownHost);
}
}
},
// 数据不完整,还未解析完,等待传输
Err(WebError::Http(HttpError::Partial)) => {
continue;
}
Err(e) => {
Self::err_server_status(inbound, 503).await?;
return Err(ProxyError::from(e));
}
}
} // 取得相关的host数据,对内网的映射端做匹配,如果未匹配到返回错误,表示不支持
{
let mut is_find = false;
let read = self.mappings.read().await;
for v in &*read {
if v.domain == host_name {
is_find = true;
}
}
if !is_find {
Self::not_match_err_status(inbound, "no found".to_string()).await?;
return Ok(());
}
} // 有新的内网映射消息到达,通知客户端建立对内网指向的连接进行双向绑定,后续做正规的http服务以支持拓展
let create = ProtCreate::new(self.sock_map, Some(host_name));
let (stream_sender, stream_receiver) = channel::<ProtFrame>(10);
let _ = self.sender_work.send((create, stream_sender)).await; // 创建传输端进行绑定
let mut trans = TransStream::new(inbound, self.sock_map, self.sender, stream_receiver);
trans.reader_mut().put_slice(buffer.chunk());
trans.copy_wait().await?;
// let _ = copy_bidirectional(&mut inbound, &mut outbound).await?;
Ok(())
}

tcp转发源码

tcp处理相对比较简单,因为我们无法确定协议里是哪个类型的源码,所以对我们来说,就是单纯的把接收的数据完全转发到新的端口里。以下是部分源码

pub async fn process<T>(self, inbound: T) -> Result<(), ProxyError<T>>
where
T: AsyncRead + AsyncWrite + Unpin,
{
// 寻找是否有匹配的tcp转发协议,如果有,则进行转发,如果没有则丢弃数据
{
let mut is_find = false;
let read = self.mappings.read().await; for v in &*read {
if v.mode == "tcp" {
is_find = true;
}
}
if !is_find {
log::warn!("not found tcp client trans");
return Ok(());
}
} // 通知客户端数据进行连接的建立,客户端的tcp配置只能存在有且只有一个,要不然无法确定转发源
let create = ProtCreate::new(self.sock_map, None);
let (stream_sender, stream_receiver) = channel::<ProtFrame>(10);
let _ = self.sender_work.send((create, stream_sender)).await; let trans = TransStream::new(inbound, self.sock_map, self.sender, stream_receiver);
trans.copy_wait().await?;
Ok(())
}

到此部分细节已基本调通,后续将优化http的处理相关,以方便支持http的头信息重写和tcp的错误信息将写入正确的日志,以方便进行定位。

7. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP及TCP内网穿透原理及运行篇的更多相关文章

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

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

  2. JAVA WEB快速入门之从编写一个基于SpringBoot+Mybatis快速创建的REST API项目了解SpringBoot、SpringMVC REST API、Mybatis等相关知识

    JAVA WEB快速入门系列之前的相关文章如下:(文章全部本人[梦在旅途原创],文中内容可能部份图片.代码参照网上资源) 第一篇:JAVA WEB快速入门之环境搭建 第二篇:JAVA WEB快速入门之 ...

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

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

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

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

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

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

  6. 编写一个make

    一.简介 How to make a "make"?在进行实现前,应该先对make有一个最基本的了解.这里稍作简介:当一个程序的源文件较少时,对其进行修改并重新生成可执行文件并不复 ...

  7. PHP: 手把手编写自己的 MVC 框架实例教程

    1 什么是MVC MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model).视图(View)和控制器(Controller ...

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

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

  9. 分享一个获取代理ip的python函数

    分享一个获取代理ip的python函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #coding:utf-8 from bs4 import Beaut ...

  10. 手把手制作一个简单的IDEA插件(环境搭建Demo篇)

    新建IDEA插件File --> new --> Project--> Intellij PlatForm Plugin-->Next-->填好项目名OK 编写插件新建工 ...

随机推荐

  1. Go语言中的init函数: 特点、用途和注意事项

    1. 引言 在Go语言中,init()函数是一种特殊的函数,用于在程序启动时自动执行一次.它的存在为我们提供了一种机制,可以在程序启动时进行一些必要的初始化操作,为程序的正常运行做好准备. 在这篇文章 ...

  2. 简单了解一下国产GPU

    英伟达都一万亿市值了,国产GPU现在发展的怎么样了?万字长文,有兴趣的进来简单了解一下. 最近,与GPU有关的几个科技新闻:一是英伟达NVIDIA市值超过一万亿美元,成为全球第一家市值过万亿的芯片公司 ...

  3. CF1034D Intervals of Intervals

    简要题意 给定 \(n\) 个区间组成的序列,定义它的一个连续段的价值为这个段内所有区间的并覆盖的长度.求价值前 \(k\) 大的段的价值和. 数据范围:\(1\le n\le 3\times 10^ ...

  4. 4. SpringMVC获取请求参数

    1. 通过 ServletAPI 获取 ‍ 将 HttpServletRequest 作为控制器方法的形参 , 此时 HttpServletRequest 类型的参数表示封装了当前请求的请求报文的对象 ...

  5. Dlang 与 C 语言交互(二)

    Dlang 与 C 语言交互(二) 随着需求不断增加,发现好像需要更多的东西了.在官网上找不到资料,四处拼凑才有了本文的分享. 上一文(DLang 与 C 语言交互(一) - jeefy - 博客园) ...

  6. java.lang.IndexOutOfBoundsException

    原因:一个ArrayList数组中没有元素,而你想获取第一个元素,运行是就会报此类型的错误 解决方案:用 array[] 的  .length 查看 数组的长度

  7. 【笔试实战】LeetCode题单刷题-编程基础 0 到 1【三】

    682. 棒球比赛 题目链接 682. 棒球比赛 题目描述 你现在是一场采用特殊赛制棒球比赛的记录员.这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分. 比赛开始时,记录是空白的.你 ...

  8. 【转载】老男孩读PCIe

    目录 老男孩读PCIe之一:从PCIe速度说起 老男孩读PCIe之二:PCIe拓扑结构 老男孩读PCIe之三:PCIe分层结构 老男孩读PCIe之四:TLP类型 老男孩读PCIe之五:TLP结构 老男 ...

  9. 利用python的PyPDF2和PyMuPDF库玩转PDF的提取、合并、旋转、缩放、加密

    一.安装PyPDF2和PyMuPDF库 pip install PyPDF2 pip install pymupdf # fitz是pymupdf的子模块 二.工具类代码 from PyPDF2 im ...

  10. DDD架构为什么应该首选六边形架构?

    一.传统分层架构 分层架构的一个重要原则是:每层只能与位于其下方的层发生耦合. 分层架构分两种:一种是严格分层架构,规定某层只能与直接位于其下方的层发生耦合:另一种是松散分层架构,允许任意上方层与任意 ...