用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
此時網路如下,如此外部使用者就能訪問到內網伺服器的資料,此時內網穿透客戶端及服務端是保持長連線以方便進行推送,本質上是長連結在轉發資料而實現穿透功能
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的錯誤資訊將寫入正確的日誌,以方便進行定位。