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

2023-12-22 09:00:09

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通道。可以在有這方面需求的人優化網路結構。

點選 [關注][在看][點贊] 是對作者最大的支援