16. 从零用Rust编写正反向代理, 反向代理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中的数据同步等大的小的均用到了这一项技术。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/623759.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

gramine运行nodejs例程

首先&#xff0c; 修改js例程代码&#xff1a; const { Web3 } require(web3); const rpcURL "https://sepolia.infura.io/v3/40b89bc0f5584056b19626b521ee5874"; const web3 new Web3(rpcURL); const address "0xde51E698b4585Af1C8322cc084ABbdbDcfe533…

C++力扣题目450--删除二叉搜索树中的节点

给定一个二叉搜索树的根节点 root 和一个值 key&#xff0c;删除二叉搜索树中的 key 对应的节点&#xff0c;并保证二叉搜索树的性质不变。返回二叉搜索树&#xff08;有可能被更新&#xff09;的根节点的引用。 一般来说&#xff0c;删除节点可分为两个步骤&#xff1a; 首先…

Generalized Focal Loss论文个人理解

论文地址&#xff1a;Generalized Focal Loss: Towards Efficient Representation Learning for Dense Object Detection 论文解决问题 研究人员最近更加关注边界框的表示(representation)及其定位质量估计&#xff08;LQE&#xff0c;本论文中指的是IoU score&#xff09;&a…

将YOLO数据集转成COCO格式,单个文件夹转为单个json文件,例如.../images/train转为instance_train.json

写在前面 参考链接&#xff1a;objectdetection-tricks/tricks_4.py 相关视频教学&#xff1a;tricks_4 用于yolov5和v7中的yolo格式转换coco格式的脚本.(如何在v5和v7中输出ap_small,ap_middle,ap_large coco指标)还可以参考相关的VOC转COCO的方式&#xff1a;damo-yolo/voc2…

R语言【paleobioDB】——pbdb_reference():通过参数请求获得多条参考文献的基本信息

Package paleobioDB version 0.7.0 paleobioDB 包在2020年已经停止更新&#xff0c;该包依赖PBDB v1 API。 可以选择在Index of /src/contrib/Archive/paleobioDB (r-project.org)下载安装包后&#xff0c;执行本地安装。 Usage pbdb_references (...) Arguments 参数【...】…

MtimeMtimecmp

Mtime: 实时time计数器&#xff0c;可读可写&#xff1b;mtime必须按照一个固定的频率递增&#xff1b;如果count overflow了&#xff0c;则mtime的值需要卷绕&#xff1b;对于32/64的系统来说&#xff0c;mtime的值都是64bits的&#xff1b; 与mtime对应的&#xff0c;还有一…

【架构师成长之领域驱动开发】

架构师成长之路 1. 如何构建高质量应用&#xff1f;2. 三大设计原则&#xff1f;3.DDD妙招4. 最终的改造结果5.模型 项目中的“坏”味道 可维护性差&#xff1a;大量的第三方模块影响核心代码的稳定性可扩展性差&#xff1a;业务逻辑与数据存储相互依赖&#xff0c;无法复用可测…

项目中使用iframe引入html 解决路由错乱问题以及父子组件传值调用方法

iframe与外部之间传值 父组件 <iframeid"iframe"src"luckysheet/index.html"frameborder"0"scrolling"no"style"width: 100%; height: 60vh; border: 0"/>const frame document.getElementById(iframe);frame.onloa…

Python综合练习之图表

文章目录 文件目录如下图标效果timeline_bar_with_graphic.htmltable_base.html articles.jsonarticlesData.pyarticlesEchartsEntity.pyarticlesEntity.py Python学习了约一个月的时间&#xff0c;这是一篇综合练习的文章。主要做的内容是通过封装对象、实现抽象方法生成统计图…

不可预测的市场中,为何有人持续胜出?

为什么经济学家和证券分析师难以预测经济或股价走势&#xff0c;而少数投资大师却能几十年持续复利&#xff1f;这两个问题看似矛盾&#xff0c;既然无法预测&#xff0c;为何又能产生确定性的赚钱结果呢&#xff1f; 有人认为这是因为幸存者偏差。然而&#xff0c;三十年以上连…

优优嗨聚集团:债务逾期,如何应对与解决?

在现代社会&#xff0c;债务问题已成为越来越多人面临的难题。债务逾期不仅会给个人带来巨大的经济压力&#xff0c;还会影响个人信用记录&#xff0c;甚至可能引发法律纠纷。那么&#xff0c;当债务逾期时&#xff0c;我们应该如何应对与解决呢&#xff1f; 一、了解债务情况 …

14.STL 常用算法

14、STL常用算法 概述&#xff1a; 算法主要是由头文件 、、 组成 是所有STL头文件中最大的一个&#xff0c;范围涉及到比较、 交换、查找、遍历操作、复制、修改等等 体积很小&#xff0c;只包括几个在序列上面进行简单数学运算的模板函数 定义了一些模板类,用以声明函数对象…

1.3 面试经典150题 - 删除有序数组中的重复项

删除有序数组中的重复项 class Solution:def removeDuplicates(self, nums: List[int]) -> int:# 处理边界数据if not nums: return 0if len(nums) 1: return 1# 两个指针&#xff0c;一个记录当前有多少个不重复值的个数&#xff0c;一个记录最新遍历到的值count 1tmp …

C# ObjectArx 绘制表格并设置单元格合并

第一行默认是标题&#xff0c;可设置行【RowType】进行设置类型 Document doc Application.DocumentManager.MdiActiveDocument;using (Transaction tr doc.TransactionManager.StartOpenCloseTransaction()){BlockTable bt tr.GetObject(doc.Database.BlockTableId, OpenMo…

GZ075 云计算应用赛题第9套

2023年全国职业院校技能大赛&#xff08;高职组&#xff09; “云计算应用”赛项赛卷9 某企业根据自身业务需求&#xff0c;实施数字化转型&#xff0c;规划和建设数字化平台&#xff0c;平台聚焦“DevOps开发运维一体化”和“数据驱动产品开发”&#xff0c;拟采用开源OpenSt…

Python3 中常用字符串函数介绍

介绍 Python 中有几个与 字符串数据类型相关的内置函数。这些函数让我们能够轻松修改和操作字符串。我们可以将函数视为在代码元素上执行的操作。内置函数是在 Python 编程语言中定义的&#xff0c;并且可以随时供我们使用的函数。 在本教程中&#xff0c;我们将介绍在 Pytho…

导轨式信号隔离变送器比例阀门线性驱动器4-20mA/0-5V/0-10V转0-165mA/0-80mA/0-1A/0-2A/0-4A

主要特性 精度、线性度误差等级&#xff1a; 0.1、0.2、0.5 级4-20mA/0-5V/0-10V 等标准信号输入0~100mA/0~500mA/0~1A/0-5A 等电流信号输出0~1V(max 2A)/0~10V/0-24V(max 5A) 等电压信号输出信号输入/信号输出 3000VDC 隔离辅助电源&#xff1a;12V、15V 或 24V 直流单电源供…

【微服务】日志搜集elasticsearch+kibana+filebeat(单机)

日志搜集eskibanafilebeat&#xff08;单机&#xff09; 日志直接输出到es中&#xff0c;适用于日志量小的项目 基于7.17.16版本 主要配置在于filebeat&#xff0c; es kibana配置改动不大 环境部署 es kibana单机环境部署 略 解压即可 常见报错&#xff0c;百度即可。 记录…

Android 和 IOS 设备唯一ID如何选择

我们在做Android/IOS应用或游戏的时候,或许总会碰到要获取设备唯一id来标识或跟踪玩家。但随着系统安全性的提高,加强用户的隐私安全,获取设备唯一ID变得越来越困难了,下面我们来分析一下,看看哪些还可以满足我们需求,同时,如果您有关于获取设备唯一ID的新发现,欢迎留言…

【Vue监听属性详细介绍】

Vue监听属性详细介绍 1. 监听属性2. watch 属性3. 计算属性&#xff08;Computed Properties&#xff09;4. 侦听器&#xff08;Listeners&#xff09;方法5. .sync 修饰符6. 注意事项 1. 监听属性 在Vue中&#xff0c;监听属性是一个十分重要的特性&#xff0c;它允许你监听和…