35. 乾貨系列從零用Rust編寫負載均衡及代理,代理伺服器的源碼升級改造

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

代理在電腦網路很常見,比如伺服器群組內部通常只會開一個口進行對外訪問,就可以通過內網代理來進行處理,從而更好的保護內網伺服器。代理讓我們網路更安全,但是警惕非正規的代理可能會竊取您的數據。請用HTTPS內容訪問更安全。 ...


wmproxy

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

項目地址

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

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

項目設計目標

在同一個埠上同時支持HTTP/HTTPS/SOCKS5代理,即假設監聽8090埠,那麼可以設置如下:

curl --proxy socks5://127.0.0.1:8090 http://www.baidu.com

curl --proxy http://127.0.0.1:8090 http://www.baidu.com

curl --proxy http://127.0.0.1:8090 https://www.baidu.com

以上方案需要都可以相容打通,才算成功。

初始方案

不做HTTP伺服器,僅簡單的解析數據流,然後進行數據轉發

pub async fn process<T>(
    username: &Option<String>,
    password: &Option<String>,
    mut inbound: T,
) -> Result<(), ProxyError<T>>
where
    T: AsyncRead + AsyncWrite + Unpin,
{
    let mut outbound;
    let mut request;
    let mut buffer = BinaryMut::new();
    loop {
        let size = {
            let mut buf = ReadBuf::uninit(buffer.chunk_mut());
            inbound.read_buf(&mut buf).await?;
            buf.filled().len()
        };

        if size == 0 {
            return Err(ProxyError::Extension("empty"));
        }
        unsafe {
            buffer.advance_mut(size);
        }
        request = webparse::Request::new();
        // 通過該方法解析標頭是否合法, 若是partial(部分)則繼續讀數據
        // 若解析失敗, 則表示非http協議能處理, 則拋出錯誤
        // 此處clone為淺拷貝,不確定是否一定能解析成功,不能影響偏移
        match request.parse_buffer(&mut buffer.clone()) {
            Ok(_) => match request.get_connect_url() {
                Some(host) => {
                    match HealthCheck::connect(&host).await {
                        Ok(v) => outbound = v,
                        Err(e) => {
                            Self::err_server_status(inbound, 503).await?;
                            return Err(ProxyError::from(e));
                        }
                    }
                    break;
                }
                None => {
                    if !request.is_partial() {
                        Self::err_server_status(inbound, 503).await?;
                        return Err(ProxyError::UnknownHost);
                    }
                }
            },
            Err(WebError::Http(HttpError::Partial)) => {
                continue;
            }
            Err(_) => {
                return Err(ProxyError::Continue((Some(buffer), inbound)));
            }
        }
    }

    match request.method() {
        &Method::Connect => {
            log::trace!(
                "https connect {:?}",
                String::from_utf8_lossy(buffer.chunk())
            );
            inbound.write_all(b"HTTP/1.1 200 OK\r\n\r\n").await?;
        }
        _ => {
            outbound.write_all(buffer.chunk()).await?;
        }
    }
    let _ = copy_bidirectional(&mut inbound, &mut outbound).await?;
    Ok(())
}

此方案僅做淺解析,處理相當高效,但遇到如下問題:

  • HTTP/HTTPS代理伺服器需要驗證密碼
  • HTTP服務存在不同的協議,此方法只相容HTTP/1.1,無法相容明確的HTTP/2協議
  • 請求的協議頭有些得做修改,此方法無法修改

改造方案

  • 引入HTTP伺服器介入
  • 但是因為需要相容不同協議,只有等確定協議後才能引入協議,需要預讀數據,進行協議判定。
  • HTTPS代理協議只處理一組Connect協議,之後需要解除http協議進行雙向綁定。

完整源碼

  1. 預讀數據
  • Socks5:第一個位元組為0X05,非ascii字元,其它協議不會影響
  • Https: https代理必鬚髮送Connect方法,所以必須以CONNECT或者connect開頭,且查詢其它HTTP方法沒有以C開頭的,這裡僅判斷第一個字元為C或者c,該協議僅處理一條http請求不參與後續TLS握手協議等保證數據安全
  • 其它開頭的均被認為http代理
let mut buffer = BinaryMut::with_capacity(24);
let size = {
    let mut buf = ReadBuf::uninit(buffer.chunk_mut());
    inbound.read_buf(&mut buf).await?;
    buf.filled().len()
};

if size == 0 {
    return Err(ProxyError::Extension("empty"));
}
unsafe {
    buffer.advance_mut(size);
}
// socks5 協議, 直接返回, 交給socks5層處理
if buffer.as_slice()[0] == 5 {
    return Err(ProxyError::Continue((Some(buffer), inbound)));
}

let mut max_req_num = usize::MAX;
// https 協議, 以connect開頭, 僅處理一條HTTP請求
if buffer.as_slice()[0] == b'C' || buffer.as_slice()[0] == b'c' {
    max_req_num = 1;
}
  1. 構建HTTP伺服器,構建服務類:
/// http代理類處理類
struct Operate {
    /// 用戶名
    username: Option<String>,
    /// 密碼
    password: Option<String>,
    /// Stream類, https連接後給後續https使用
    stream: Option<TcpStream>,
    /// http代理keep-alive的復用
    sender: Option<Sender<RecvRequest>>,
    /// http代理keep-alive的復用
    receiver: Option<Receiver<ProtResult<RecvResponse>>>,
}

構建HTTP服務

// 需要將已讀的數據buffer重新加到server的已讀cache中, 否則解析會出錯
let mut server = Server::new_by_cache(inbound, None, buffer);
// 構建HTTP服務回調
let mut operate = Operate {
    username: username.clone(),
    password: password.clone(),
    stream: None,
    sender: None,
    receiver: None,
};
server.set_max_req(max_req_num);
let _e = server.incoming(&mut operate).await?;
if let Some(outbound) = &mut operate.stream {
    let mut inbound = server.into_io();
    let _ = copy_bidirectional(&mut inbound, outbound).await?;
}

此時我們已將數據用HTTP服務進行處理,收到相應的請求再進行給遠端做轉發:

HTTP核心處理回調,此處我們用的是async_trait非同步回調


#[async_trait]
impl OperateTrait for &mut Operate {
    async fn operate(&mut self, request: &mut RecvRequest) -> ProtResult<RecvResponse> {
        // 已連接直接進行後續處理
        if let Some(sender) = &self.sender {
            sender.send(request.replace_clone(Body::empty())).await?;
            if let Some(res) = self.receiver.as_mut().unwrap().recv().await {
                return Ok(res?)
            }
            return Err(ProtError::Extension("already close by other"))
        }
        // 獲取要連接的對象
        let stream = if let Some(host) = request.get_connect_url() {
            match HealthCheck::connect(&host).await {
                Ok(v) => v,
                Err(e) => {
                    return Err(ProtError::from(e));
                }
            }
        } else {
            return Err(ProtError::Extension("unknow tcp stream"));
        };

        // 賬號密碼存在,將獲取`Proxy-Authorization`進行校驗,如果檢驗錯誤返回407協議
        if self.username.is_some() && self.password.is_some() {
            let mut is_auth = false;
            if let Some(auth) = request.headers_mut().remove(&"Proxy-Authorization") {
                if let Some(val) = auth.as_string() {
                    is_auth = self.check_basic_auth(&val);
                }
            }
            if !is_auth {
                return Ok(Response::builder().status(407).body("")?.into_type());
            }
        }

        // 判斷用戶協議
        match request.method() {
            &Method::Connect => {
                // https返回200內容直接進行遠端和客戶端的雙向綁定
                self.stream = Some(stream);
                return Ok(Response::builder().status(200).body("")?.into_type());
            }
            _ => {
                // http協議,需要將客戶端的內容轉發到服務端,並將服務端數據轉回客戶端
                let client = Client::new(ClientOption::default(), MaybeHttpsStream::Http(stream));
                let (mut recv, sender) = client.send2(request.replace_clone(Body::empty())).await?;
                match recv.recv().await {
                    Some(res) => {
                        self.sender = Some(sender);
                        self.receiver = Some(recv);
                        return Ok(res?)
                    },
                    None => return Err(ProtError::Extension("already close by other")),
                }
            }
        }

    }
}

密碼校驗,由Basic的密碼加密方法,先用base64解密,再用:做拆分,再與用戶密碼比較

pub fn check_basic_auth(&self, value: &str) -> bool
{
    use base64::engine::general_purpose;
    use std::io::Read;

    let vals: Vec<&str> = value.split_whitespace().collect();
    if vals.len() == 1 {
        return false;
    }

    let mut wrapped_reader = Cursor::new(vals[1].as_bytes());
    let mut decoder = base64::read::DecoderReader::new(
        &mut wrapped_reader,
        &general_purpose::STANDARD);
    // handle errors as you normally would
    let mut result: Vec<u8> = Vec::new();
    decoder.read_to_end(&mut result).unwrap();

    if let Ok(value) = String::from_utf8(result) {
        let up: Vec<&str> = value.split(":").collect();
        if up.len() != 2 {
            return false;
        }
        if up[0] == self.username.as_ref().unwrap() ||
            up[1] == self.password.as_ref().unwrap() {
            return true;
        }
    }

    return false;
}

小結

代理在電腦網路很常見,比如伺服器群組內部通常只會開一個口進行對外訪問,就可以通過內網代理來進行處理,從而更好的保護內網伺服器。代理讓我們網路更安全,但是警惕非正規的代理可能會竊取您的數據。請用HTTPS內容訪問更安全。

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


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

-Advertisement-
Play Games
更多相關文章
  • 這篇筆記深入介紹了AOP(面向切麵編程),這個技術可以在代碼中以模塊化的方式實現橫切關註點。它解決了業務層代碼中存在的問題,如額外功能代碼的冗餘和每個方法都需要書寫一遍額外功能代碼的情況。 AOP在Spring中的實現主要依靠Aspect切麵、Advice通知和Pointcut切入點的組合。Advi... ...
  • ArrayList是一個使用List介面實現的Java類。顧名思義,Java ArrayList提供了動態數組的功能,其中數組的大小不是固定的。它實現了所有可選的列表操作,並允許所有元素,包括null。 ...
  • 原文: https://openaigptguide.com/ai-picture-generator/ 在人工智慧(AI)圖像生成技術的推動下,各類AI圖片生成網站如雨後春筍般涌現,為我們的日常生活提供了豐富多彩的視覺體驗。 AI圖片生成技術原理 人工智慧(AI)圖片生成技術原理是通過電腦程式使 ...
  • 背景及問題 如下程式所示: #include<iostream> class MyString { public: MyString() = default; MyString(const char* data) { printf("%s", "MyString Constructed!!\n"); ...
  • 引言 在ChatGpt火了這麼久,他的那種單字單字返回的格式可能讓很多朋友感到好奇,在之前我用c#寫了一個版本的,同時支持IAsyncEnumerable以及SSE,今天把之前寫的Java版本的也發出來,和大家一起學習,有不對的地方,歡迎各位大佬指正。 Code 我這邊用的是JDK21版本,可以看到 ...
  • 需求 有些應用每次啟動都需要用管理員許可權運行,比如Python註入dll時,編輯器或cmd就需要以管理員許可權運行,不然註入就會失敗。 這篇文章用編程怎麼修改配置實現打開某個軟體都是使用管理員運行,就不用每次都右鍵點擊以管理員身份運行此程式。主要是給小白配置,防止他忘了以管理員許可權運行,又跑過來問我為 ...
  • Qt 是一個跨平臺C++圖形界面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以通過拖拽的方式將不同組件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹`TableWidget`表格組件的常用方法及靈活運用。`QTableWidget` 是 Qt 中用於顯示表格數據... ...
  • QMdiArea(Multiple Document Interface Area)是Qt中用於創建多文檔界面的組件。它提供了一種在單個視窗中管理多個文檔的方式,每個文檔通常是一個子視窗(`QMdiSubWindow`)。該組件主要用於設計多文檔界面應用程式,具備有多種窗體展示風格,實現了在父窗體中... ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...