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. fdisk 命令 创建分区 实现扩容

    fdisk 命令 创建分区 实现扩容 Linux fdisk命令简介 Linux fdisk 是一个创建和维护分区表的程序,它兼容 DOS 类型的分区表.BSD 或者 SUN 类型的磁盘列表. 菜单操 ...

  2. Windows电脑环境变量(用户变量、系统变量)的修改

      本文介绍在Windows 10操作系统中,进行用户变量.系统变量等两种环境变量的新建.修改与删除的详细方法.   在很多时候,我们需要对Windows电脑的环境变量加以修改,例如安装一些专业软件. ...

  3. Nginx TCP 负载均衡:stream 模块配置

    工作上遇到需要用nginx做负载均衡,参考了前同事留下的作业顺利搞定,感觉这块很有意义写篇文档记录. 参考:nginx tcp负载均衡(Stream模块)配置说明 参考:利用nginx进行TCP负载均 ...

  4. vulnhub billu:b0x

    知识点 SQLi.目录爆破.数据库操作.文件包含漏洞.提权.反弹shell 解题步骤 nmap扫描有80,22端口 nmap -sV -Pn -T 4 192.168.220.132 访问网页提示sq ...

  5. C# 中的 数组[]、ArrayList、List

    C# 中的 数组[].ArrayList.List 数组 在 C# 中,数组实际上是对象,而不只是如在 C 和 C++ 中的连续内存的可寻址区域. 属性: 数组可以是一维.多维或交错的. 创建数组实例 ...

  6. 利用Redis实现向量相似度搜索:解决文本、图像和音频之间的相似度匹配问题

    在自然语言处理领域,有一个常见且重要的任务就是文本相似度搜索.文本相似度搜索是指根据用户输入的一段文本,从数据库中找出与之最相似或最相关的一段或多段文本.它可以应用在很多场景中,例如问答系统.推荐系统 ...

  7. python-gitlab 一个简单demo

    背景 需要收集git仓库信息到数据库供前端展示 包括:仓库信息.仓库所有者.成员列表.提交信息.活跃情况等 需要定时启动.灵活触发 实现简介 使用gitlab v4 restful 接口 使用pyth ...

  8. 仅三天,我用 GPT-4 生成了性能全网第一的 Golang Worker Pool,轻松打败 GitHub 万星项目

    目录 1. 我写了一个超牛的开源项目 1.1 你看看这性能 1.2 你看看这功能 1.3 你猜我这一百天都经历了啥 2. 你有多久没写并发程序了? 3. 问:一个 Worker Pool 程序需要包含 ...

  9. 通过 tree shaking 移除无用代码

    tree shaking 依赖于ES Module 的静态语法分析,在项目编译时移除无用的代码以减少文件体积. usedExports 在文件中,我们可能定义了变量但是暂时又没有用到,这样会造成空间的 ...

  10. [etcd]基本数据库操作

    前言 etcd数据库操作基本围绕着对键值和目录的CRUD操作,以及生命周期的管理. 之前在单节点部署了三实例集群,而etcdctl默认找的是127.0.0.1:2379,所以这里先声明一个临时全局变量 ...