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中的数据同步等大的小的均用到了这一项技术。

16. 从零开始编写一个类nginx工具, 反向代理upstream源码实现的更多相关文章

  1. 从零开始编写一个BitTorrent下载器

    从零开始编写一个BitTorrent下载器 BT协议 简介 BT协议Bit Torrent(BT)是一种通信协议,又是一种应用程序,广泛用于对等网络通信(P2P).曾经风靡一时,由于它引起了巨大的流量 ...

  2. nginx的反向代理和负载均衡的一个总结

    之前一直觉的nginx的反向代理和负载均衡很厉害的样子,最近有机会接触了一下公司的这方面的技术,发现技术就是一张窗户纸呀,捅破了啥都明白了! 接下来先看一下nginx的反向代理: 简单的来说就是ngi ...

  3. 22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表。然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法showB输出大写的英文字母表。最后编写主类C,在主类的main方法 中测试类A与类B。

    22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表.然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法sh ...

  4. 35.按要求编写Java程序: (1)编写一个接口:InterfaceA,只含有一个方法int method(int n); (2)编写一个类:ClassA来实现接口InterfaceA,实现int method(int n)接口方 法时,要求计算1到n的和; (3)编写另一个类:ClassB来实现接口InterfaceA,实现int method(int n)接口 方法时,要求计算n的阶乘(n

      35.按要求编写Java程序: (1)编写一个接口:InterfaceA,只含有一个方法int method(int n): (2)编写一个类:ClassA来实现接口InterfaceA,实现in ...

  5. 编写一个类,其中包含一个排序的方法Sort(),当传入的是一串整数,就按照从小到大的顺序输出,如果传入的是一个字符串,就将字符串反序输出。

    namespace test2 { class Program { /// <summary> /// 编写一个类,其中包含一个排序的方法Sort(),当传入的是一串整数,就按照从小到大的 ...

  6. 二、 编写一个类,用两个栈实现队列,支持队列的基本操作(add,poll,peek)

    请指教交流! package com.it.hxs.c01; import java.util.Stack; /* 编写一个类,用两个栈实现队列,支持队列的基本操作(add,poll,peek) */ ...

  7. 如何编写一个SQL注入工具

    0x01  前言 一直在思考如何编写一个自动化注入工具,这款工具不用太复杂,但是可以用最简单.最直接的方式来获取数据库信息,根据自定义构造的payload来绕过防护,这样子就可以. 0x02 SQL注 ...

  8. 从零开始编写一个vue插件

    title: 从零开始编写一个vue插件 toc: true date: 2018-12-17 10:54:29 categories: Web tags: vue mathjax 写毕设的时候需要一 ...

  9. 如何让多个不同类型的后端网站用一个nginx进行反向代理实际场景分析

    前段时间公司根据要求需要将聚石塔上服务器从杭州整体迁移到张家口,刚好趁这次机会将这些乱七八糟的服务器做一次梳理和整合,断断续续一个月迁移完成 大概优化掉了1/3的机器,完成之后遇到了一些问题,比如曾今 ...

  10. 题目一:编写一个类Computer,类中含有一个求n的阶乘的方法

    作业:编写一个类Computer,类中含有一个求n的阶乘的方法.将该类打包,并在另一包中的Java文件App.java中引入包,在主类中定义Computer类的对象,调用求n的阶乘的方法(n值由参数决 ...

随机推荐

  1. 文盘Rust -- FFI 浅尝

    rust FFI 是rust与其他语言互调的桥梁,通过FFI rust 可以有效继承 C 语言的历史资产.本期通过几个例子来聊聊rust与C 语言交互的具体步骤. 场景一 调用C代码 创建工程 car ...

  2. Spring6 初始

    Spring6 初始 @ 目录 Spring6 初始 每博一文案: 1. 初始 Spring6 1.1 OCP开闭原则 1.2 依赖倒置原则DIP 1.3 控制反转IoC 2. Spring 初始 2 ...

  3. linux基础命令及常用命令总结

    1.ls命令 ls命令是最基础的命令之一,作用是列出当前目录下所有的文件和目录.ls命令有很多选项可以使用,比较常用的是-l选项,可以以详细信息的形式列出所有文件和目录的信息. 示例:列出当前目录下的 ...

  4. Redis解决网络抖动问题

    Redis解决网络抖动问题 所谓网络抖动问题, 简单来说就是防止用户短暂的时间内对同一个接口多次点击访问 这里利用的是redis锁的原子性和with Statement上下文管理器实现, 另外该类还支 ...

  5. ubuntu 终端选中黏贴、自带截图

    鼠标选中 -- 复制 鼠标中键 -- 粘贴 注意,在tmux中,这个操作需要加上 Shift 键. PrtSc:截图整个桌面保存到Pictures Ctrl + PrtSc:截图整个桌面到剪贴板 Sh ...

  6. TCP超时分析

    参考链接: Linux 建立 TCP 连接的超时时间分析 Linux 建立 TCP 连接的超时时间分析 Linux 系统默认的建立 TCP 连接的超时时间为 127 秒. 2 分 7 秒即 127 秒 ...

  7. 文心一言 VS 讯飞星火 VS chatgpt (66)-- 算法导论6.5 5题

    五.试分析在使用下列循环不变量时,HEAP-INCREASE-KEY 的正确性:在算法的第4~6行 while循环每次迭代开始的时候,子数组 A[1..A.heap-size]要满足最大堆的性质.如果 ...

  8. croc-文件传输工具

    前言 croc是一款用go语言开发的命令行文件传输工具,该工具允许两台计算机设备以一种简单和安全的方式来传输文件. GitHub项目地址 环境信息 IP 系统版本 croc版本 说明 192.168. ...

  9. 解决 Blazor 中因标签换行导致的行内元素空隙问题

    实践过不同前端框架的朋友应该都知道,对于同一个样式,在不同框架上的表现都会有不同,时时需要做"适配",在 Blazor 上也不例外.在做 Ant Design Blazor 时就深 ...

  10. SpringBoot3文件管理

    目录 一.简介 二.工程搭建 1.工程结构 2.依赖管理 三.上传下载 1.配置管理 2.上传下载 四.Excel文件 1.Excel创建 2.Excel读取 3.解析监听 4.导入导出 五.参考源码 ...