加载中...

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

7. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP及TCP内网穿透原理及运行篇
1.用Rust手把手编写一个Proxy(代理), 动工2.用Rust手把手编写一个Proxy(代理), UDP绑定篇3.5. 用Rust手把手编写一个Proxy(代理), 通讯协议建立, 为内网穿透做准备4.6. 用Rust手把手编写一个wmproxy(代理,内网穿透等), 通讯协议源码解读篇
5.7. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP及TCP内网穿透原理及运行篇
6.8. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP改造篇之HPACK原理7. 用Rust手把手编写一个Proxy(代理), 准备篇, 动手造轮子8.9. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP2改造篇之HPACK示例, 了解http2头信息如何处理9.10. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP内网穿透支持修改头信息10.11. 用Rust手把手编写一个wmproxy(代理,内网穿透等), 实现健康检查11.12. 用Rust手把手编写一个wmproxy(代理,内网穿透等), TLS的双向认证信息及token验证12.13. 从零开始编写一个类nginx工具, HTTP中的压缩gzip,deflate,brotli算法13.14. 从零开始编写一个类nginx工具, HTTP文件服务器的实现过程及参数14.15. 从零开始编写一个类nginx工具, 如果将nginx.conf转成yaml,toml,json会怎么样15.16. 从零开始编写一个类nginx工具, 反向代理upstream源码实现16.17. 从零开始编写一个类nginx工具, Rust中一些功能的实现17.18. 从零开始编写一个类nginx工具, 主动式健康检查源码实现18.19. 从零开始编写一个类nginx工具, 配置数据的热更新原理及实现19.25. 干货系列从零用Rust编写正反向代理,序列化之serde是如何工作的20.27. 干货系列从零用Rust编写正反向代理,Rust中日志库的应用基础准备21.28. 干货系列从零用Rust编写正反向代理,项目日志的源码实现22.30. 干货系列从零用Rust编写正反向代理,HTTP的组装之旅(中间件)23.29. 干货系列从零用Rust编写正反向代理,异步回调(async trait)的使用24.26. 干货系列从零用Rust编写正反向代理,如何发布Rust项目到Docker25.32. 干货系列从零用Rust编写正反向代理,关于堆和栈以及如何解决stack overflow26.33. 干货系列从零用Rust编写正反向代理,关于HTTP客户端代理的源码实现27.34. 干货系列从零用Rust编写负载均衡及代理,异步测试在Rust中的实现28.35. 干货系列从零用Rust编写负载均衡及代理,代理服务器的源码升级改造29.36. 干货系列从零用Rust编写负载均衡及代理,内网穿透中内网代理的实现30.37. 干货系列从零用Rust编写负载均衡及代理,负载均衡中try_files实现31.38. 干货系列从零用Rust编写负载均衡及代理,负载均衡中ip通行与禁止32.39. 干货系列从零用Rust编写负载均衡及代理,正则及格式替换33.40. 干货系列从零用Rust编写负载均衡及代理,websocket的实现34.41. 干货系列从零用Rust编写负载均衡及代理,websocket与tcp的映射,WS与TCP互转35.42 干货系列从零用Rust编写负载均衡及代理,wmproxy中配置tcp转websocket36.43 干货系列从零用Rust编写负载均衡及代理,内网穿透方案完整部署37.44从零开始用Rust编写nginx,命令行参数的设计与解析及说明38.45从零开始用Rust编写nginx,静态文件服务器竟然还有这些细节39.46从零开始用Rust编写nginx,数据还能这么传,多层代理(IP多级代理)搭建40.47从零开始用Rust编写nginx,配对还有这么多要求!负载均衡中的路径匹配41.48从零开始用Rust编写nginx,搭建一个简单又好看官方网站42.49从零开始用Rust编写nginx,我竟然在同一个端口上绑定了多少IP43.50从零开始用Rust编写nginx,原来TLS证书还可以这么申请44.51从零开始用Rust编写nginx,江湖救急,TLS证书快过期了
收起

用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

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

由穿透客户端连接到内网服务器
建立连接/保持连接
访问建立连接
内网服务器
内网穿透客户端wmproxy
内网穿透服务端wmproxy
外网用户

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则会显示
New Image

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

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

tcp测试

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

此时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的错误信息将写入正确的日志,以方便进行定位。