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

来源:https://www.cnblogs.com/luojiawaf/archive/2023/10/04/17742544.html
-Advertisement-
Play Games

用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,從而實現服務。

為什麼要內網穿透

內網限制

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文記錄了穩定性摸排過程中的一些思考和沉澱。 前言 在之前寫了篇文章《上線十年,81萬行Java代碼的老系統如何重構》,在文章後有同學留言問“這麼複雜的改動,質量是如何應對的”,是一個特別好的問題,當時只是從現有的一些監控、測試、卡口手段上進行了回答。但在回答過程當中就在思考一個問題,交接過來的老代 ...
  • 目錄: 1.前言 2.設計與分析 3.BUG與修正 4.缺陷與改進 5.總結 一、前言 題目集1的題目都是比較基礎的題,不需要用到其他的類,而且所有的題代碼行數都不超過40行。知識點集中在分支選擇和浮點數的處理上,題量適中,難度也較小。比較適合初學Java的學生來熟悉java,瞭解java與之前所學 ...
  • 隨著移動互聯網的發展,手機號碼已經成為我們日常生活中不可或缺的一部分。然而,在我們使用手機號碼時,我們經常需要瞭解某個手機號碼的歸屬地,以便更好的進行溝通和交流。那麼如何快速定位手機號碼的歸屬地呢?本文將基於數據源下載,用代碼的方式來實現這一目標。 一、數據源下載 在實現手機號碼定位功能之前,我們需 ...
  • 示例,將新列表中的所有值設置為 'hello': newlist = ['hello' for x in fruits] 表達式還可以包含條件,不像篩選器那樣,而是作為操縱結果的一種方式: 示例,返回 "orange" 而不是 "banana": newlist = [x if x != "bana ...
  • 【中秋國慶不斷更】OpenHarmony組件內狀態變數使用:@State裝飾器 @State裝飾的變數,或稱為狀態變數,一旦變數擁有了狀態屬性,就和自定義組件的渲染綁定起來。當狀態改變時,UI會發生對應的渲染改變。 在狀態變數相關裝飾器中,@State是最基礎的,使變數擁有狀態屬性的裝飾器,它也是大 ...
  • 【中秋國慶不斷更】HarmonyOS對通知類消息的管理與發佈通知(下) 一、發佈進度條類型通知 進度條通知也是常見的通知類型,主要應用於文件下載、事務處理進度顯示。HarmonyOS提供了進度條模板,發佈通知應用設置好進度條模板的屬性值,如模板名、模板數據,通過通知子系統發送到通知欄顯示。 目前系統 ...
  • Python中的變數 變數的定義 程式中,數據都臨時存儲在記憶體中。每一個被存儲在記憶體的數據都有一個記憶體地址。其中特定的數據被我們所使用,因此我們為那些記憶體地址定義了名稱。這一名稱被稱作 標識符,又稱變數名。而與變數名對應記憶體地址中的數據被稱為變數值。 總結:變數為記憶體中特定的數據。它的記憶體地址的名稱 ...
  • 在這,您將學習瞭解 Spring Boot Starter Parent, 它是 Spring Boot 提供的父級 Pom 文件,旨在提供自動版本依賴管理,幫助我們輕鬆快速地進行 Spring Boot 開發。 什麼是 Spring Boot Starter Parent ? 通過 Spring ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...