16. 從零開始編寫一個類nginx工具, 反向代理upstream原始碼實現

2023-10-23 12:02:21

wmproxy

wmproxy將用Rust實現http/https代理, socks5代理, 反向代理, 靜態檔案伺服器,後續將實現websocket代理, 內外網穿透等, 會將實現過程分享出來, 感興趣的可以一起造個輪子法

專案 wmproxy

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

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

瞭解反向代理

反向代理(Reverse Proxy)是一種伺服器架構的技術,位於使用者端和目標伺服器之間,處理來自使用者端的所有請求,並代表目標伺服器處理與使用者端的互動。

保護源站

在使用者端存取伺服器的時候,其實並不關心目標的地址在哪,只要資料能夠正常返回,簽名能夠正常的握手,就認為是正常的。
而通常源站的防護等級相對會較弱,比如源站一般沒有防禦DDOS的能力,暴露了源站的地址也就意味著被滲透被攻擊的概率大大升高,從而使服務變得極不穩定。

加速傳輸

通常反向代理可以遍佈各個節點,然後再通過專有線路來存取源站,或者一次請求快取結果多次返回就可以減少和源站通訊,減少源站壓力,就典型的結構如CDN就可以大大的提高使用者端的存取速度,減少延遲

防火牆作用

由於所有的客戶機請求都必須通過代理伺服器存取遠端站點,因此可以在代理伺服器上設定限制,過濾某些不安全資訊,如WAF防火牆之類。

反向代理有哪些設定

以下是一份nginx的反向代理的設定

http {
    upstream backend {
        server 192.168.0.14:8080 weight=10 fail_timeout=3s ; 
        server 192.168.0.15:8081 weight=10; 
    }
    server {
        listen 80;  #監聽80的伺服器埠
        server_name wm-proxy.com;  #監聽的域名
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        location /products {
            proxy_pass http://backend;
            proxy_set_header Host $host;
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Origin' '*';
        }
        
        location / {
            root wmproxy;
            index index.html index.htm;
        }
    }
    server {
        listen 80;  #監聽80的伺服器埠
        server_name localhost;  #監聽的域名
       
        location / {
            proxy_pass http://www.baidu.com;
            proxy_set_header Host $host;
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Origin' '*';
        }
    }
}

以上設定內容主要有幾點需實現:

upstream

這是反向連線的代理池,可能設定了多個的資料可存取的源地址,此處需要實現各種策略來平衡存取它,如weight權重模式,ip_hash按使用者端地址來對映相同的源站地址,保證同一個使用者端只進入一個源站,如fair按後端伺服器的響應時間來分配請求,響應時間短的優先分配。

各自的健康檢查引數需要和全域性的進行區分

fail_timeout失敗的重試時間
max_fails超過這失敗次數則認為不可連

多個server同時監聽同一個埠

反向代理可設定多個server同時監聽同一個埠,按server_name來區分要存取的的源站地址

同一個埠,多個證書的問題

需要根據使用者端傳輸的域名來自動選擇對應的證書進行解析來返回資料,保證資料的正確。

父級的設定要對映到子級的選項

比如設定在proxy_set_header的每個選項在他子級的location都需要進行設定,而在Rust中要獲取父類別的結構相當的麻煩,這點需要正確的解決

location的多種結構支援

location可能是反向代理,可能是檔案伺服器,需要多種設定支援

實現原始碼

以下是各upstream的定義

#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SingleStreamConfig {
    /// 存取地址
    pub addr: SocketAddr,
    /// 權重
    #[serde(default = "default_weight")]
    pub weight: u16,
    /// 失敗的恢復時間
    #[serde_as(as = "DurationSeconds<u64>")]
    #[serde(default = "fail_timeout")]
    fail_timeout: Duration,
    /// 當前連續失敗的次數
    #[serde(default = "default_fall_times")]
    fall_times: usize,
    /// 當前連續成功的次數
    #[serde(default = "default_rise_times")]
    rise_times: usize,
}

這邊用到了serde_with庫中的serde_as,把數位秒解析成Duration型別。我們在檢查是否存活的時候會帶入相應的引數與全域性的做區分開來

/// 檢測狀態是否能連線
pub fn check_fall_down(addr: &SocketAddr, fail_timeout: &Duration, fall_times: &usize, rise_times: &usize) -> bool {
    ...
}

關於多個埠監聽,開始時我們會遍歷所有的埠,且只會繫結一次

let mut bind_port = HashSet::new();
for value in &self.server.clone() {
    // 已監聽的埠存到Set裡面
    if bind_port.contains(&value.bind_addr.port()) {
        continue;
    }
    bind_port.insert(value.bind_addr.port());
    let listener = TcpListener::bind(value.bind_addr).await?;
    listeners.push(listener);
}

保證只會繫結一次埠。等解析完Req的時候再進行轉發,保證能正確的處理轉發

同一個埠多個證書的問題

因為使用者端傳送ClientHello的時候我們可以知道是從哪個域名過來的,所以我們可以根據發過來的域名選擇正確的證書,就可以解決多個證書的問題,在rustls中,我們用ResolvesServerCertUsingSni來進行解決,下面相關原始碼

let config = rustls::ServerConfig::builder().with_safe_defaults();
let mut resolve = ResolvesServerCertUsingSni::new();
for value in &self.server.clone() {
    let mut is_ssl = false;
    if value.cert.is_some() && value.key.is_some() {
        let key = sign::any_supported_type(&Self::load_keys(&value.key)?)
            .map_err(|_| ProtError::Extension("unvaild key"))?;
        let ck = CertifiedKey::new(Self::load_certs(&value.cert)?, key);
        resolve.add(&value.server_name, ck).map_err(|e| {
            println!("{:?}", e); ProtError::Extension("key error")
        })?;
        is_ssl = true;
    }

    tlss.push(is_ssl);
}

let config = config
    .with_no_client_auth()
    .with_cert_resolver(Arc::new(resolve));
Ok((Some(TlsAcceptor::from(Arc::new(config))), tlss, listeners))

ResolvesServerCertUsingSni可以設定多個域名的證書,但證書必須和域名強匹配,Accept的時候會根據域名選擇相應的證書。

子級需要能存取父級的設定問題
在Rust因為所有權的問題,一個物件肯定會歸屬於一個地方的所有權,所以無法在不經常加工的情況實現類似其它語言的parent->getChild()child->getParent(),而此處比如location需要共用server的資料,如root引數。目前查資料比較公認的有以下方式:
用指標的方向(raw pointer),但是指標無法Send,也就是無法線上程間轉移。

struct Parent {
    child: Child,
}

struct Child {
    parent: *const Parent,
}

fn main() {
    let mut child = Child {
        parent: std::ptr::null(),
    };
    let parent = Parent { child };
    child.parent = &parent;
}

用共用計數方法(Rc)

use std::rc::Rc;

// 所有的Child都將擁有該物件的參照
struct Inner;

struct Parent {
    child: Child,
    inner: Rc<Inner>,
}

struct Child {
    parent: Rc<Inner>, // or Weak<Inner> if that's desirable
}

fn main() {
    let inner = Rc::new(Inner);
    let child = Child {parent: Rc::clone(&inner)};
    let parent = Parent {child, inner};
}

用臨時的生命週期,獲取Child的時候做特殊處理

struct Parent {
  pub children: Vec<Child>,
}

impl Parent {
 fn get_child(&'a self, name) -> DynamicChild<'a> {
   DynamicChild { parent: self, child: ...}
 }
}

struct Child {
 a: u64,
 b: String,
}

struct DynamicChild<'a> {
 pub data: &'a Child,
 pub parent: &'a Parent,
}

impl<'a> DynamicChild<'a> {
 fn do_thing_with_parent(&self) -> usize {
  self.parent.children.len()
 }
}

Rust為了保證安全,但凡有所有權歸屬的問題,就會變得比較麻煩,我們這裡會在資料序列化的時候,把父級的設定直接寫入到子級的設定,以這種方式子級就有完整的資料,也可以避免存取父級的內容。

/// 將設定引數提前共用給子級
pub fn copy_to_child(&mut self) {
    for server in &mut self.server {
        server.upstream.append(&mut self.upstream.clone());
        server.copy_to_child();
    }
}

此時保證location這一層處理的能得到完整的資料,即可以避免存取父級節點。

location的多種結構支援

location可能是靜態檔案伺服器,也可能是反向代理,也可能是後續的fast-cgi等。
location根據rule進行req中的path匹配,如果填有Method方法也根據Method是否匹配。然後再根據相應的分支選項進行處理匹配。

let host = req.get_host().unwrap_or(String::new());
// 不管有沒有匹配, 都執行最後一個
for (index, s) in value.server.iter().enumerate() {
    if s.server_name == host || host.is_empty() || index == server_len - 1 {
        let path = req.path().clone();
        for l in s.location.iter() {
            if l.is_match_rule(&path, req.method()) {
                return l.deal_request(req).await;
            }
        }
        // ...
    }
}

結語

此時關於反向代理的幾個初步問題已經處理完成反向代理操作。反向代理在網際網路已經組成了密不可分的組成部分,成為了網際網路的基石之一。像雲伺服器的負載均衡,K8S中的資料同步等大的小的均用到了這一項技術。