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的反向代理的配置

  1. http {
  2. upstream backend {
  3. server 192.168.0.14:8080 weight=10 fail_timeout=3s ;
  4. server 192.168.0.15:8081 weight=10;
  5. }
  6. server {
  7. listen 80; #监听80的服务端口
  8. server_name wm-proxy.com; #监听的域名
  9. proxy_set_header X-Real-IP $remote_addr;
  10. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  11. location /products {
  12. proxy_pass http://backend;
  13. proxy_set_header Host $host;
  14. add_header 'Access-Control-Allow-Credentials' 'true';
  15. add_header 'Access-Control-Allow-Origin' '*';
  16. }
  17. location / {
  18. root wmproxy;
  19. index index.html index.htm;
  20. }
  21. }
  22. server {
  23. listen 80; #监听80的服务端口
  24. server_name localhost; #监听的域名
  25. location / {
  26. proxy_pass http://www.baidu.com;
  27. proxy_set_header Host $host;
  28. add_header 'Access-Control-Allow-Credentials' 'true';
  29. add_header 'Access-Control-Allow-Origin' '*';
  30. }
  31. }
  32. }

以上配置内容主要有几点需实现:

upstream

这是反向连接的代理池,可能配置了多个的数据可访问的源地址,此处需要实现各种策略来平衡访问它,如weight权重模式,ip_hash按客户端地址来映射相同的源站地址,保证同一个客户端只进入一个源站,如fair按后端服务器的响应时间来分配请求,响应时间短的优先分配。

各自的健康检查参数需要和全局的进行区分

fail_timeout失败的重试时间

max_fails超过这失败次数则认为不可连

多个server同时监听同一个端口

反向代理可配置多个server同时监听同一个端口,按server_name来区分要访问的的源站地址

同一个端口,多个证书的问题

需要根据客户端传输的域名来自动选择对应的证书进行解析来返回数据,保证数据的正确。

父级的配置要映射到子级的选项

比如配置在proxy_set_header的每个选项在他子级的location都需要进行设置,而在Rust中要获取父类的结构相当的麻烦,这点需要正确的解决

location的多种结构支持

location可能是反向代理,可能是文件服务器,需要多种配置支持

实现源码

以下是各upstream的定义

  1. #[serde_as]
  2. #[derive(Debug, Clone, Serialize, Deserialize)]
  3. pub struct SingleStreamConfig {
  4. /// 访问地址
  5. pub addr: SocketAddr,
  6. /// 权重
  7. #[serde(default = "default_weight")]
  8. pub weight: u16,
  9. /// 失败的恢复时间
  10. #[serde_as(as = "DurationSeconds<u64>")]
  11. #[serde(default = "fail_timeout")]
  12. fail_timeout: Duration,
  13. /// 当前连续失败的次数
  14. #[serde(default = "default_fall_times")]
  15. fall_times: usize,
  16. /// 当前连续成功的次数
  17. #[serde(default = "default_rise_times")]
  18. rise_times: usize,
  19. }

这边用到了serde_with库中的serde_as,把数字秒解析成Duration类型。我们在检查是否存活的时候会带入相应的参数与全局的做区分开来

  1. /// 检测状态是否能连接
  2. pub fn check_fall_down(addr: &SocketAddr, fail_timeout: &Duration, fall_times: &usize, rise_times: &usize) -> bool {
  3. ...
  4. }

关于多个端口监听,开始时我们会遍历所有的端口,且只会绑定一次

  1. let mut bind_port = HashSet::new();
  2. for value in &self.server.clone() {
  3. // 已监听的端口存到Set里面
  4. if bind_port.contains(&value.bind_addr.port()) {
  5. continue;
  6. }
  7. bind_port.insert(value.bind_addr.port());
  8. let listener = TcpListener::bind(value.bind_addr).await?;
  9. listeners.push(listener);
  10. }

保证只会绑定一次端口。等解析完Req的时候再进行转发,保证能正确的处理转发

同一个端口多个证书的问题

因为客户端发送ClientHello的时候我们可以知道是从哪个域名过来的,所以我们可以根据发过来的域名选择正确的证书,就可以解决多个证书的问题,在rustls中,我们用ResolvesServerCertUsingSni来进行解决,下面相关源码

  1. let config = rustls::ServerConfig::builder().with_safe_defaults();
  2. let mut resolve = ResolvesServerCertUsingSni::new();
  3. for value in &self.server.clone() {
  4. let mut is_ssl = false;
  5. if value.cert.is_some() && value.key.is_some() {
  6. let key = sign::any_supported_type(&Self::load_keys(&value.key)?)
  7. .map_err(|_| ProtError::Extension("unvaild key"))?;
  8. let ck = CertifiedKey::new(Self::load_certs(&value.cert)?, key);
  9. resolve.add(&value.server_name, ck).map_err(|e| {
  10. println!("{:?}", e); ProtError::Extension("key error")
  11. })?;
  12. is_ssl = true;
  13. }
  14. tlss.push(is_ssl);
  15. }
  16. let config = config
  17. .with_no_client_auth()
  18. .with_cert_resolver(Arc::new(resolve));
  19. Ok((Some(TlsAcceptor::from(Arc::new(config))), tlss, listeners))

ResolvesServerCertUsingSni可以配置多个域名的证书,但证书必须和域名强匹配,Accept的时候会根据域名选择相应的证书。

子级需要能访问父级的配置问题

在Rust因为所有权的问题,一个对象肯定会归属于一个地方的所有权,所以无法在不经常加工的情况实现类似其它语言的parent->getChild()child->getParent(),而此处比如location需要共享server的数据,如root参数。目前查资料比较公认的有以下方式:

用指针的方向(raw pointer),但是指针无法Send,也就是无法在线程间转移。

  1. struct Parent {
  2. child: Child,
  3. }
  4. struct Child {
  5. parent: *const Parent,
  6. }
  7. fn main() {
  8. let mut child = Child {
  9. parent: std::ptr::null(),
  10. };
  11. let parent = Parent { child };
  12. child.parent = &parent;
  13. }

用共享计数方法(Rc)

  1. use std::rc::Rc;
  2. // 所有的Child都将拥有该对象的引用
  3. struct Inner;
  4. struct Parent {
  5. child: Child,
  6. inner: Rc<Inner>,
  7. }
  8. struct Child {
  9. parent: Rc<Inner>, // or Weak<Inner> if that's desirable
  10. }
  11. fn main() {
  12. let inner = Rc::new(Inner);
  13. let child = Child {parent: Rc::clone(&inner)};
  14. let parent = Parent {child, inner};
  15. }

用临时的生命周期,获取Child的时候做特殊处理

  1. struct Parent {
  2. pub children: Vec<Child>,
  3. }
  4. impl Parent {
  5. fn get_child(&'a self, name) -> DynamicChild<'a> {
  6. DynamicChild { parent: self, child: ...}
  7. }
  8. }
  9. struct Child {
  10. a: u64,
  11. b: String,
  12. }
  13. struct DynamicChild<'a> {
  14. pub data: &'a Child,
  15. pub parent: &'a Parent,
  16. }
  17. impl<'a> DynamicChild<'a> {
  18. fn do_thing_with_parent(&self) -> usize {
  19. self.parent.children.len()
  20. }
  21. }

Rust为了保证安全,但凡有所有权归属的问题,就会变得比较麻烦,我们这里会在数据序列化的时候,把父级的配置直接写入到子级的配置,以这种方式子级就有完整的数据,也可以避免访问父级的内容。

  1. /// 将配置参数提前共享给子级
  2. pub fn copy_to_child(&mut self) {
  3. for server in &mut self.server {
  4. server.upstream.append(&mut self.upstream.clone());
  5. server.copy_to_child();
  6. }
  7. }

此时保证location这一层处理的能得到完整的数据,即可以避免访问父级节点。

location的多种结构支持

location可能是静态文件服务器,也可能是反向代理,也可能是后续的fast-cgi等。

location根据rule进行req中的path匹配,如果填有Method方法也根据Method是否匹配。然后再根据相应的分支选项进行处理匹配。

  1. let host = req.get_host().unwrap_or(String::new());
  2. // 不管有没有匹配, 都执行最后一个
  3. for (index, s) in value.server.iter().enumerate() {
  4. if s.server_name == host || host.is_empty() || index == server_len - 1 {
  5. let path = req.path().clone();
  6. for l in s.location.iter() {
  7. if l.is_match_rule(&path, req.method()) {
  8. return l.deal_request(req).await;
  9. }
  10. }
  11. // ...
  12. }
  13. }

结语

此时关于反向代理的几个初步问题已经处理完成反向代理操作。反向代理在互联网已经组成了密不可分的组成部分,成为了互联网的基石之一。像云服务器的负载均衡,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. 数据挖掘18大算法实现以及其他相关经典DM算法:决策分类,聚类,链接挖掘,关联挖掘,模式挖掘。图算法,搜索算法等

    数据挖掘18大算法实现以及其他相关经典DM算法:决策分类,聚类,链接挖掘,关联挖掘,模式挖掘.图算法,搜索算法等 算法码源见文末 1.算法目录 18大DM算法 包名 目录名 算法名 Associati ...

  2. 【Oracle】行转列的函数wm_concat,listagg,xmlagg,pivot以及动态行转列

    [Oracle]行转列的几种情况 表的数据如下 朴实无华的函数 1.wm_concat 使用格式: select 分组字段,wm_concat(要转换的列名) from 表名 group by 分组字 ...

  3. vscode中react组件

    通过使用这个插件我们可以很方便的进行组件/方法/文件的导入 本篇博客仅对插件进行介绍翻译,便于自己以后使用 常用片段列表 imr: 引入 React import React from 'react' ...

  4. 投个 3D 冰壶,上班玩一玩

    ​本篇文章将介绍如何使用物理引擎和图扑 3D 可视化技术来呈现冰壶运动的模拟. Oimo.js 物理引擎 Oimo.js 是一个轻量级的物理引擎,它使用 JavaScript 语言编写,并且基于 Oi ...

  5. Oracle使用SQL截取某字符串

    很多小伙伴在使用Oracle的时候,想通过SQL来提取根据某一字符串截取来获得的字符串,他苦于对SQL不是很熟悉,但是现在你可以放心啦,现在先恭喜你找到了答案.因为在这里我已经为你写好了相关的函数以及 ...

  6. Spring的Bean标签配置(一)

    Bean标签基本配置 由于配置对象交由Spring来创建 默认情况下它调用的的是类中的无参构造函数,如果没有无参构造函数则不会创建成功 id:唯一标识符号,反射是通过无参构造创建对象的. class: ...

  7. MariaDB start 报错:mysql-bin.index' not found (Errcode: 2) (Errcode: 13)

    问题是修改配置log-bin=/data/mysql/binlog/mysql-bin后出现的. 报错:Errcode: 2 mkdir -p /data/mysql/binlog ## 和正常的DB ...

  8. Linux 脚本:冒泡排序

    #!/bin/bash arr=(3 2 5 4 1) len=${#arr[@]} for i in $(seq 1 $len) ; do index=$(($len - $i - 1)) for ...

  9. 论文解读(BERT-DAAT)《Adversarial and Domain-Aware BERT for Cross-Domain Sentiment Analysis》

    论文信息 论文标题:Adversarial and Domain-Aware BERT for Cross-Domain Sentiment Analysis论文作者:论文来源:2020 ACL论文地 ...

  10. Wampserver64 报错:无法启动此程序,因为计算机中丢失 MSVCR110.dll。尝试重新安装该程序以解决此问题。

    缺少环境配置, 程序下载地址如下: https://www.microsoft.com/zh-cn/download/confirmation.aspx?id=30679 点击下载,下载完成后,双击程 ...