7. 用Rust手把手編寫一個wmproxy(代理,內網穿透等), HTTP及TCP內網穿透原理及執行篇

問蒙服務框架發表於2023-10-04

用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的錯誤資訊將寫入正確的日誌,以方便進行定位。

相關文章