用Rust手把手編寫一個wmproxy(代理,內網穿透等), HTTP及TCP內網穿透原理及運行篇 項目 ++wmproxy++ gite: https://gitee.com/tickbh/wmproxy github: https://github.com/tickbh/wmproxy 內網、公 ...
用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
# 連接服務端地址
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:8002
和http://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.rs
和trans/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的錯誤信息將寫入正確的日誌,以方便進行定位。