20. 從零用Rust編寫正反向代理,四層反向代理stream(tcp與udp)實現

来源:https://www.cnblogs.com/wmproxy/archive/2023/10/30/wmproxy20.html
-Advertisement-
Play Games

wmproxy wmproxy是由Rust編寫,已實現http/https代理,socks5代理, 反向代理,靜態文件伺服器,內網穿透,配置熱更新等, 後續將實現websocket代理等,同時會將實現過程分享出來, 感興趣的可以一起造個輪子法 項目地址 gite: https://gitee.com ...


wmproxy

wmproxy是由Rust編寫,已實現http/https代理,socks5代理, 反向代理,靜態文件伺服器,內網穿透,配置熱更新等, 後續將實現websocket代理等,同時會將實現過程分享出來, 感興趣的可以一起造個輪子法

項目地址

gite: https://gitee.com/tickbh/wmproxy

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

四層代理

四層代理,也稱為網路層代理,是基於IP地址和埠號的代理方式。它只關心數據包的源IP地址、目的IP地址、源埠號和目的埠號,不關心數據包的具體內容。四層代理主要通過報文中的目標地址和埠,再加上負載均衡設備設置的伺服器選擇方式,決定最終選擇的內部伺服器。

因為四層代理不用處理任何相關的包信息,只需將包數據傳遞給正確的伺服器即可,所以實現相對比較簡單。

以下是OSI七層模型的示意圖,來源於網上

實現方式

雙端建立連接,也就是收到客戶端的連接的時候,同時建立一條通往服務端的連接,然後做雙向綁定即可完成服務。

四層代理還有udp的轉發需求,需要同步將udp的數據進行轉發,udp的處理方式處理會相對複雜一些,因為當前地址只有綁定一份,但是可能來自各種不同的地址,不同的客戶端的(remote_ip, remote_port)我們需要當成一個全新的客戶端。

而且有時候無法主動感知是否已經被斷開了,所以也必須有超時機制,好在超時的時候能及時釋放掉連接,好讓系統及時的socket資源。

TCP實現

tcp找到相應的地址,連接,並雙向綁定即可

pub async fn process<T>(
    data: Arc<Mutex<StreamConfig>>,
    local_addr: SocketAddr,
    mut inbound: T,
    _addr: SocketAddr,
) -> ProxyResult<()>
where
    T: AsyncRead + AsyncWrite + Unpin + std::marker::Send + 'static,
{
    let value = data.lock().await;
    for (_, s) in value.server.iter().enumerate() {
        if s.bind_addr.port() == local_addr.port() {
            let addr = ReverseHelper::get_upstream_addr(&s.upstream, "")?;
            let mut connect = HealthCheck::connect(&addr).await?;
            copy_bidirectional(&mut inbound, &mut connect).await?;
            break;
        }
    }
    Ok(())
}

UDP實現

UDP相對比較複雜,下麵我們先列舉內部的流程圖

flowchart TD A[綁定反向udp埠] B[客戶端] H{是否第一次} I[創建非同步協程] D[非同步協程中] B <-->|根據地址連接發送數據到| A A --> H H -->|是|I I -->|將Receiver傳到以接收數據| D H -->|否,將數據Sender給|D D -->|非同步讀取數據併發送|A

在stream綁定的時候,要區分出TCP還是UDP的,做分別的綁定

/// stream的綁定,按bind_mode區分出udp或者是tcp,返回相應的列表
pub async fn bind(&mut self) -> ProxyResult<(Vec<TcpListener>, Vec<StreamUdp>)> {
    let mut listeners = vec![];
    let mut udp_listeners = vec![];
    let mut bind_port = HashSet::new();
    for value in &self.server.clone() {
        if bind_port.contains(&value.bind_addr.port()) {
            continue;
        }
        bind_port.insert(value.bind_addr.port());
        if value.bind_mode == "udp" {
            let listener = Helper::bind_upd(value.bind_addr).await?;
            udp_listeners.push(StreamUdp::new(listener, value.clone()));
        } else {
            let listener = Helper::bind(value.bind_addr).await?;
            listeners.push(listener);
        }
    }
    Ok((listeners, udp_listeners))
}

我們會對連接做分別的監聽,下麵是udp的獲取是否有新數據:

async fn multi_udp_listen_work(
    listens: &mut Vec<StreamUdp>,
) -> (io::Result<(Vec<u8>, SocketAddr)>, usize) {
    if !listens.is_empty() {
        let (data, index, _) =
            select_all(listens.iter_mut().map(|listener| {
                listener.next().boxed()
            })).await;
        if data.is_none() {
            return (Err(io::Error::new(io::ErrorKind::InvalidInput, "read none data")), index)
        }
        (data.unwrap(), index)
    } else {
        let pend = std::future::pending();
        let () = pend.await;
        unreachable!()
    }
}

此處我們用next,也就是我們實現了 futures_core::Stream介面,用Poll的方式來註冊實現有事件的時候來通知。

在tokio中,在read或者write的時候返回Poll::Pending,將會將socket的可讀可寫註冊到底層,如果一旦系統可讀可寫就會通知該介面,將會重新執行一遍futures_core::Stream

我們將同時可以處理可讀可寫可發送事件,如果介面超時我們將關閉相應的介面。

impl Stream for StreamUdp {
    type Item = io::Result<(Vec<u8>, SocketAddr)>;
    fn poll_next(
        mut self: std::pin::Pin<&mut Self>,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Option<Self::Item>> {
        let _ = self.poll_write(cx)?;
        let _ = self.poll_sender(cx)?;
        self.poll_read(cx)
    }
}

下麵是主要的StreamUdp

/// Udp轉發的處理結構,緩存一些數值以做中轉
pub struct StreamUdp {
    /// 讀的緩衝類,避免每次都釋放
    pub buf: BinaryMut,
    /// 核心的udp綁定埠
    pub socket: UdpSocket,
    pub server: ServerConfig,

    /// 如果接收該數據大小為0,那麼則代表通知數據關閉
    pub receiver: Receiver<(Vec<u8>, SocketAddr)>,
    /// 將發送器傳達給每個子協程
    pub sender: Sender<(Vec<u8>, SocketAddr)>,

    /// 接收的緩存數據,無法保證全部直接進行發送完畢
    pub cache_data: LinkedList<(Vec<u8>, SocketAddr)>,
    /// 發送的緩存數據,無法保證全部直接進行發送完畢
    pub send_cache_data: LinkedList<(Vec<u8>, SocketAddr)>,
    /// 每個地址綁定的對象,包含Sender,最後操作時間,超時時間
    remote_sockets: HashMap<SocketAddr, InnerUdp>,
}

結果測試

我們自己開一個udp服務端,綁定了本地的8089,我們將接收到的數據前面加上from server:併進行返回,代理端我們綁定了84的埠,並將udp數據轉發給8089端:

use tokio::net::UdpSocket;
use std::io;

#[tokio::main]
async fn main() -> io::Result<()> {
    let sock = UdpSocket::bind("0.0.0.0:8089").await?;
    let mut buf = [0; 1024];
    loop {
        let (len, addr) = sock.recv_from(&mut buf).await?;
        let mut vec = "from server: ".as_bytes().to_vec();
        vec.extend(&buf[..len]);
        let _ = sock.send_to(&vec, addr).await?;
    }
}

客戶端我們用nc運行:

可以看出兩個客戶端互相獨立,彼此返回的數據均符合預期,正常的接收及返回。

TCP我們綁定了83埠並轉發到HTTP的本地埠8080,我們用curl進行測試,符合預期,如圖:

結語

至此四層的反向代理TCP/UDP均已完成,也符合預期。

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


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

-Advertisement-
Play Games
更多相關文章
  • 不論是在團隊寫作還是在個人工作中,PDF 文檔往往會經過多次修訂和更新。掌握 PDF 文檔內容的變化對於管理文檔有極大的幫助。通過對比 PDF 文檔,用戶可以快速找出文檔增加、刪除和修改的內容,更好地瞭解文檔的演變過程,輕鬆地管理文檔。本文將介紹如何在 Java 程式中通過代碼快速比較兩個 PDF ...
  • 作者:Escape 來源:https://www.escapelife.site/posts/38c81b25.html 服務日誌收集方案:Filebeat + Graylog! 當我們公司內部部署很多服務以及測試、正式環境的時候,查看日誌就變成了一個非常剛需的需求了。是多個環境的日誌統一收集,然後 ...
  • 一、SpringCloud 簡介 Spring Cloud 是一系列框架的有序集合如服務發現註冊、配置中心、消息匯流排、負載均衡、熔斷器、數據監控等。 SpringCloud 將多個服務框架組合起來,通過Spring Boot進行再封裝,屏蔽掉了複雜的配置和實現原理,最終給開發者提供了一套簡單易懂、易 ...
  • 哈嘍兄弟們,今天來實現一下建築市場公共服務平臺的數據採集,順便實現一下網站的JS解密。 話不多說,我們直接開始今天的內容。 首先我們需要準備這些 環境使用 Python 3.8 Pycharm 模塊使用 requests --> pip install requests execjs --> pip ...
  • 我們在對keycloak框架中的核心項目keycloak-services進行二次開發過程中,發現了一個問題,當時有這種需求,在keycloak-services中需要使用infinispan緩存,我們直接添加infinispan-core引用之後,在啟動keycloak進出錯了,提示我們沒有找到i ...
  • AES演算法是一種對稱加密演算法,全稱為高級加密標準(Advanced Encryption Standard)。它是一種分組密碼,以`128`比特為一個分組進行加密,其密鑰長度可以是`128`比特、`192`比特或`256`比特,因此可以提供不同等級的安全性。該演算法採用了替代、置換和混淆等技術,以及多... ...
  • 本文將從啟動類開始詳細分析zookeeper的啟動流程: 載入配置的過程 集群啟動過程 單機版啟動過程 啟動類 org.apache.zookeeper.server.quorum.QuorumPeerMain類。 用於啟動zookeeper服務,第一個參數用來指定配置文件,配置文件properti ...
  • 從配置文件中獲取屬性應該是SpringBoot開發中最為常用的功能之一,但就是這麼常用的功能,仍然有很多開發者抓狂~今天帶大家簡單回顧一下這六種的使用方式: ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...