36. 乾貨系列從零用Rust編寫負載均衡及代理,內網穿透中內網代理的實現

来源:https://www.cnblogs.com/wmproxy/archive/2023/12/22/wmproxy36.html
-Advertisement-
Play Games

內網代理可以實現不想暴露太多信息給外部,但是又能提供內部的完整信息支持,相當於建立了一條可用的HTTP通道。可以在有這方面需求的人優化網路結構。 ...


wmproxy

wmproxy已用Rust實現http/https代理, socks5代理, 反向代理, 靜態文件伺服器,四層TCP/UDP轉發,七層負載均衡,內網穿透,後續將實現websocket代理等,會將實現過程分享出來,感興趣的可以一起造個輪子

項目地址

國內: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

項目設計目標

  • HTTP轉發
  • HTTPS轉發(證書在伺服器,內網為HTTP)
  • TCP轉發(純粹的TCP轉發,保持原樣的協議)
  • PROXY轉發(服務端接收數據,內網的客戶端當成PROXY客戶端,相當於逆向訪問內網伺服器,[新增])

實現方案

服務端提供客戶端的連接埠,可加密Tls,可雙向加密mTls,可賬號密碼認證,客戶端連接服務端的埠等待數據的處理。主要有兩個類服務端CenterServer客戶端CenterClient

一些細節可以參考第5篇,第6篇,第10篇,第12篇,有相關的內網穿透的細節。

內網代理的實現

  1. 首先添加一種模式
#[serde_as]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MappingConfig {
    /// 其它欄位....
    // 添加模塊proxy
    pub mode: String,
}
  1. 添加內網代理監聽埠
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxyConfig {
    /// 其它欄位....
    pub(crate) map_http_bind: Option<SocketAddr>,
    pub(crate) map_https_bind: Option<SocketAddr>,
    pub(crate) map_tcp_bind: Option<SocketAddr>,
    // 新加代理介面監聽欄位
    pub(crate) map_proxy_bind: Option<SocketAddr>,
    -
}

目前埠做唯一綁定,後續可根據配置動態響應相應的數據。

  1. 做映射

由於代理和tcp類似,服務端均不做任務處理,只需將數據完全轉發給客戶端處理即可

pub async fn server_new_prxoy(&mut self, stream: TcpStream) -> ProxyResult<()> {
    let trans = TransTcp::new(
        self.sender(),
        self.sender_work(),
        self.calc_next_id(),
        self.mappings.clone(),
    );
    tokio::spawn(async move {
        if let Err(e) = trans.process(stream, "proxy").await {
            log::warn!("內網穿透:轉發Proxy轉發時發生錯誤:{:?}", e);
        }
    });
    return Ok(());
}
  1. 客戶端處理
    客戶端將映射流轉化成VirtualStream,把它當成一個虛擬流,然後邏輯均用代理的來處理
let (virtual_sender, virtual_receiver) = channel::<ProtFrame>(10);
map.insert(p.sock_map(), virtual_sender);

if mapping.as_ref().unwrap().is_proxy() {
    let stream = VirtualStream::new(
        p.sock_map(),
        sender.clone(),
        virtual_receiver,
    );

    let (flag, username, password, udp_bind) = (
        option.flag,
        option.username.clone(),
        option.password.clone(),
        option.udp_bind.clone(),
    );
    tokio::spawn(async move {
        // 處理代理的能力
        let _ = WMCore::deal_proxy(
            stream, flag, username, password, udp_bind,
        )
        .await;
    });
}

VirtualStream是一個虛擬出一個流連接,並實現AsyncRead及AsyncRead,可以和流一樣正常操作,這也是Trait而不是繼承的好處之一,定義就可以比較簡單:

pub struct VirtualStream
{
    // sock綁定的句柄
    id: u32,
    // 收到數據通過sender發送給中心端
    sender: PollSender<ProtFrame>,
    // 收到中心端的寫入請求,轉成write
    receiver: Receiver<ProtFrame>,
    // 讀取的數據緩存,將轉發成ProtFrame
    read: BinaryMut,
    // 寫的數據緩存,直接寫入到stream下,從ProtFrame轉化而來
    write: BinaryMut,
}
  1. 設計ProxyServer

統一的代理服務類,剝離相關代碼,使代碼更清晰

/// 代理伺服器類, 提供代理服務
pub struct ProxyServer {
    flag: Flag,
    username: Option<String>,
    password: Option<String>,
    udp_bind: Option<IpAddr>,
    headers: Vec<ConfigHeader>,
}
  1. 代理HTTP頭信息的重寫
    HTTP中添加相關代碼以支持頭信息重寫
impl Operate {
    fn deal_request(&self, req: &mut RecvRequest) -> ProtResult<()> {
        if let Some(headers) = &self.headers {
            // 覆寫Request的頭文件信息
            Helper::rewrite_request(req, headers);
        }
        Ok(())
    }
    
    fn deal_response(&self, res: &mut RecvResponse) -> ProtResult<()> {
        if let Some(headers) = &self.headers {
            // 覆寫Request的頭文件信息
            Helper::rewrite_response(res, headers);
        }
        Ok(())
    }
}

內網代理流程圖:

flowchart TD A[外部客戶端] -->|以代理方式訪問|B B[服務端監聽Proxy] <-->|數據轉發| C[中心服務端CenterServer] C <-->|協議傳輸|D[中心客戶端CenterClient] D <-->|虛擬數據流|E[虛擬客戶端] E <-->|處理數據|F[內網代理服務,可完全訪問內網]

這樣子我們就以代理的方式擁有了所有的內網HTTP相關服務的訪問許可權。可以簡化我們網路的結構。

自動化測試

內網穿透的自動化測試在 tests/mapping
將自動構建內網客戶端服務,外網服務端服務做測試,以下部分代碼節選:

#[tokio::test]
async fn run_test() {
    let local_server_addr = run_server().await.unwrap();
    let addr = "127.0.0.1:0".parse().unwrap();
    let proxy = ProxyConfig::builder()
        .bind_addr(addr)
        .map_http_bind(Some(addr))
        .map_https_bind(Some(addr))
        .map_tcp_bind(Some(addr))
        .map_proxy_bind(Some(addr))
        .center(true)
        .mode("server".to_string())
        .into_value()
        .unwrap();

    let (server_addr, http_addr, https_addr, tcp_addr, proxy_addr, _sender) =
        run_mapping_server(proxy).await.unwrap();
    let mut mapping = MappingConfig::new(
        "test".to_string(),
        "http".to_string(),
        "soft.wm-proxy.com".to_string(),
        vec![],
    );
    mapping.local_addr = Some(local_server_addr);

    let mut mapping_tcp = MappingConfig::new(
        "tcp".to_string(),
        "tcp".to_string(),
        "soft.wm-proxy.com".to_string(),
        vec![],
    );
    mapping_tcp.local_addr = Some(local_server_addr);

    let mut mapping_proxy = MappingConfig::new(
        "proxy".to_string(),
        "proxy".to_string(),
        "soft.wm-proxy.com1".to_string(),
        vec![
            ConfigHeader::new(wmproxy::HeaderOper::Add, false, "from_proxy".to_string(), "mapping".to_string())
        ],
    );
    mapping_proxy.local_addr = Some(local_server_addr);

    let proxy = ProxyConfig::builder()
        .bind_addr(addr)
        .server(Some(server_addr))
        .center(true)
        .mode("client".to_string())
        .mapping(mapping)
        .mapping(mapping_tcp)
        .mapping(mapping_proxy)
        .into_value()
        .unwrap();
    let _client_sender = run_mapping_client(proxy).await.unwrap();

    fn do_build_req(url: &str, method: &str, body: &Vec<u8>) -> Request<Body> {
        let body = BinaryMut::from(body.clone());
        Request::builder()
            .method(method)
            .url(&*url)
            .body(Body::new_binary(body))
            .unwrap()
    }
    
    {
        let url = &*format!("http://{}/", local_server_addr);
        let client = Client::builder()
            // .http2(false)
            .http2_only(true)
            .add_proxy(&*format!("http://{}", proxy_addr.unwrap())).unwrap()
            .connect(&*url)
            .await
            .unwrap();

        let mut res = client
            .send_now(do_build_req(url, "GET", &vec![]))
            .await
            .unwrap();
        let mut result = BinaryMut::new();
        res.body_mut().read_all(&mut result).await;

        // 測試頭信息來確認是否來源於代理
        assert_eq!(res.headers().get_value(&"from_proxy"), &"mapping");
        assert_eq!(result.remaining(), HELLO_WORLD.as_bytes().len());
        assert_eq!(result.as_slice(), HELLO_WORLD.as_bytes());
        assert_eq!(res.version(), Version::Http2);
    }
}

小結

內網代理可以實現不想暴露太多信息給外部,但是又能提供內部的完整信息支持,相當於建立了一條可用的HTTP通道。可以在有這方面需求的人優化網路結構。

點擊 [關註][在看][點贊] 是對作者最大的支持


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

-Advertisement-
Play Games
更多相關文章
  • Qt 是一個跨平臺C++圖形界面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以通過拖拽的方式將不同組件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹`QCharts`二維繪圖組件的常用方法及靈活運用。Qt Charts 提供了一個強大且易於使用的工具集,用於在 ... ...
  • Detours 代碼倉庫: https://github.com/microsoft/Detours x64寫一個任意地址hook要比x86麻煩的多,所以這裡直接封裝框架來用於x64的hook。 Detours是微軟發佈的一個API hook框架,同時支持x86和x64,看文檔說也支持ARM和ARM ...
  • 一款好用又強大的開源社區,採用主流的互聯網技術架構、全新的UI設計、支持一鍵源碼部署,擁有完整的文章&教程發佈/搜索/評論/統計流程等,代碼完全開源,沒有任何二次封裝,是一個非常適合二次開發/實戰的現代化社區項目。 ...
  • Python 介紹 Python 是一種 高級 的、解釋型 的、通用 的編程語言。其設計哲學強調代碼的可讀性,使用顯著的縮進。Python 是 動態類型 和 垃圾收集 的。 基本語法 設置 Python 環境並開始基礎知識。 文章鏈接:Python 安裝與快速入門 變數 變數用於存儲在電腦程式中引 ...
  • 平時習慣了./和../作為訪問目錄的路徑,但今天使用golang中fs.ReadDir這個函數的時候發現這個習慣是不正確的。 但是常用的命令並沒有分很清楚.和./ 在這幾個命令中使用.或./都可以到達目錄下 ls cd 錯誤示範 package main import ( "fmt" "io/fs" ...
  • 數據的預處理是數據分析,或者機器學習訓練前的重要步驟。通過數據預處理,可以 提高數據質量,處理數據的缺失值、異常值和重覆值等問題,增加數據的準確性和可靠性 整合不同數據,數據的來源和結構可能多種多樣,分析和訓練前要整合成一個數據集 提高數據性能,對數據的值進行變換,規約等(比如無量綱化),讓演算法更加 ...
  • 在實際的業務開發中,我們經常會碰到VO、BO、PO、DTO等對象屬性之間的賦值,當屬性較多的時候我們使用get,set的方式進行賦值的工作量相對較大,因此很多人會選擇使用spring提供的拷貝工具BeanUtils的copyProperties方法完成對象之間屬性的拷貝。通過這種方式可以很大程度上降... ...
  • 馬哥原創:小紅書詳情採集軟體,自動爬取xhs筆記的詳情數據,欄位含:筆記id,筆記鏈接,筆記標題,筆記內容,筆記類型,發佈時間,修改時間,IP屬地,點贊數,收藏數,評論數,轉發數,用戶昵稱,用戶id,用戶主頁鏈接。 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...