16. 从零开始编写一个类nginx工具, 反向代理upstream源码实现
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中的数据同步等大的小的均用到了这一项技术。