35. 干货系列从零用Rust编写负载均衡及代理,代理服务器的源码升级改造
wmproxy
wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子
项目地址
国内: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
项目设计目标
在同一个端口上同时支持HTTP/HTTPS/SOCKS5代理,即假设监听8090端口,那么可以设置如下:
curl --proxy socks5://127.0.0.1:8090 http://www.baidu.com
curl --proxy http://127.0.0.1:8090 http://www.baidu.com
curl --proxy http://127.0.0.1:8090 https://www.baidu.com
以上方案需要都可以兼容打通,才算成功。
初始方案
不做HTTP服务器,仅简单的解析数据流,然后进行数据转发
pub async fn process<T>(
    username: &Option<String>,
    password: &Option<String>,
    mut inbound: T,
) -> Result<(), ProxyError<T>>
where
    T: AsyncRead + AsyncWrite + Unpin,
{
    let mut outbound;
    let mut request;
    let mut buffer = BinaryMut::new();
    loop {
        let size = {
            let mut buf = ReadBuf::uninit(buffer.chunk_mut());
            inbound.read_buf(&mut buf).await?;
            buf.filled().len()
        };
        if size == 0 {
            return Err(ProxyError::Extension("empty"));
        }
        unsafe {
            buffer.advance_mut(size);
        }
        request = webparse::Request::new();
        // 通过该方法解析标头是否合法, 若是partial(部分)则继续读数据
        // 若解析失败, 则表示非http协议能处理, 则抛出错误
        // 此处clone为浅拷贝,不确定是否一定能解析成功,不能影响偏移
        match request.parse_buffer(&mut buffer.clone()) {
            Ok(_) => match request.get_connect_url() {
                Some(host) => {
                    match HealthCheck::connect(&host).await {
                        Ok(v) => outbound = v,
                        Err(e) => {
                            Self::err_server_status(inbound, 503).await?;
                            return Err(ProxyError::from(e));
                        }
                    }
                    break;
                }
                None => {
                    if !request.is_partial() {
                        Self::err_server_status(inbound, 503).await?;
                        return Err(ProxyError::UnknownHost);
                    }
                }
            },
            Err(WebError::Http(HttpError::Partial)) => {
                continue;
            }
            Err(_) => {
                return Err(ProxyError::Continue((Some(buffer), inbound)));
            }
        }
    }
    match request.method() {
        &Method::Connect => {
            log::trace!(
                "https connect {:?}",
                String::from_utf8_lossy(buffer.chunk())
            );
            inbound.write_all(b"HTTP/1.1 200 OK\r\n\r\n").await?;
        }
        _ => {
            outbound.write_all(buffer.chunk()).await?;
        }
    }
    let _ = copy_bidirectional(&mut inbound, &mut outbound).await?;
    Ok(())
}
此方案仅做浅解析,处理相当高效,但遇到如下问题:
- HTTP/HTTPS代理服务器需要验证密码
 - HTTP服务存在不同的协议,此方法只兼容HTTP/1.1,无法兼容明确的HTTP/2协议
 - 请求的协议头有些得做修改,此方法无法修改
 
改造方案
- 引入HTTP服务器介入
 - 但是因为需要兼容不同协议,只有等确定协议后才能引入协议,需要预读数据,进行协议判定。
 - HTTPS代理协议只处理一组Connect协议,之后需要解除http协议进行双向绑定。
 
- 预读数据
 
- Socks5:第一个字节为
0X05,非ascii字符,其它协议不会影响 - Https: https代理必须发送Connect方法,所以必须以
CONNECT或者connect开头,且查询其它HTTP方法没有以C开头的,这里仅判断第一个字符为C或者c,该协议仅处理一条http请求不参与后续TLS握手协议等保证数据安全 - 其它开头的均被认为http代理
 
let mut buffer = BinaryMut::with_capacity(24);
let size = {
    let mut buf = ReadBuf::uninit(buffer.chunk_mut());
    inbound.read_buf(&mut buf).await?;
    buf.filled().len()
};
if size == 0 {
    return Err(ProxyError::Extension("empty"));
}
unsafe {
    buffer.advance_mut(size);
}
// socks5 协议, 直接返回, 交给socks5层处理
if buffer.as_slice()[0] == 5 {
    return Err(ProxyError::Continue((Some(buffer), inbound)));
}
let mut max_req_num = usize::MAX;
// https 协议, 以connect开头, 仅处理一条HTTP请求
if buffer.as_slice()[0] == b'C' || buffer.as_slice()[0] == b'c' {
    max_req_num = 1;
}
- 构建HTTP服务器,构建服务类:
 
/// http代理类处理类
struct Operate {
    /// 用户名
    username: Option<String>,
    /// 密码
    password: Option<String>,
    /// Stream类, https连接后给后续https使用
    stream: Option<TcpStream>,
    /// http代理keep-alive的复用
    sender: Option<Sender<RecvRequest>>,
    /// http代理keep-alive的复用
    receiver: Option<Receiver<ProtResult<RecvResponse>>>,
}
构建HTTP服务
// 需要将已读的数据buffer重新加到server的已读cache中, 否则解析会出错
let mut server = Server::new_by_cache(inbound, None, buffer);
// 构建HTTP服务回调
let mut operate = Operate {
    username: username.clone(),
    password: password.clone(),
    stream: None,
    sender: None,
    receiver: None,
};
server.set_max_req(max_req_num);
let _e = server.incoming(&mut operate).await?;
if let Some(outbound) = &mut operate.stream {
    let mut inbound = server.into_io();
    let _ = copy_bidirectional(&mut inbound, outbound).await?;
}
此时我们已将数据用HTTP服务进行处理,收到相应的请求再进行给远端做转发:
HTTP核心处理回调,此处我们用的是async_trait异步回调
#[async_trait]
impl OperateTrait for &mut Operate {
    async fn operate(&mut self, request: &mut RecvRequest) -> ProtResult<RecvResponse> {
        // 已连接直接进行后续处理
        if let Some(sender) = &self.sender {
            sender.send(request.replace_clone(Body::empty())).await?;
            if let Some(res) = self.receiver.as_mut().unwrap().recv().await {
                return Ok(res?)
            }
            return Err(ProtError::Extension("already close by other"))
        }
        // 获取要连接的对象
        let stream = if let Some(host) = request.get_connect_url() {
            match HealthCheck::connect(&host).await {
                Ok(v) => v,
                Err(e) => {
                    return Err(ProtError::from(e));
                }
            }
        } else {
            return Err(ProtError::Extension("unknow tcp stream"));
        };
        // 账号密码存在,将获取`Proxy-Authorization`进行校验,如果检验错误返回407协议
        if self.username.is_some() && self.password.is_some() {
            let mut is_auth = false;
            if let Some(auth) = request.headers_mut().remove(&"Proxy-Authorization") {
                if let Some(val) = auth.as_string() {
                    is_auth = self.check_basic_auth(&val);
                }
            }
            if !is_auth {
                return Ok(Response::builder().status(407).body("")?.into_type());
            }
        }
        // 判断用户协议
        match request.method() {
            &Method::Connect => {
                // https返回200内容直接进行远端和客户端的双向绑定
                self.stream = Some(stream);
                return Ok(Response::builder().status(200).body("")?.into_type());
            }
            _ => {
                // http协议,需要将客户端的内容转发到服务端,并将服务端数据转回客户端
                let client = Client::new(ClientOption::default(), MaybeHttpsStream::Http(stream));
                let (mut recv, sender) = client.send2(request.replace_clone(Body::empty())).await?;
                match recv.recv().await {
                    Some(res) => {
                        self.sender = Some(sender);
                        self.receiver = Some(recv);
                        return Ok(res?)
                    },
                    None => return Err(ProtError::Extension("already close by other")),
                }
            }
        }
    }
}
密码校验,由Basic的密码加密方法,先用base64解密,再用:做拆分,再与用户密码比较
pub fn check_basic_auth(&self, value: &str) -> bool
{
    use base64::engine::general_purpose;
    use std::io::Read;
    let vals: Vec<&str> = value.split_whitespace().collect();
    if vals.len() == 1 {
        return false;
    }
    let mut wrapped_reader = Cursor::new(vals[1].as_bytes());
    let mut decoder = base64::read::DecoderReader::new(
        &mut wrapped_reader,
        &general_purpose::STANDARD);
    // handle errors as you normally would
    let mut result: Vec<u8> = Vec::new();
    decoder.read_to_end(&mut result).unwrap();
    if let Ok(value) = String::from_utf8(result) {
        let up: Vec<&str> = value.split(":").collect();
        if up.len() != 2 {
            return false;
        }
        if up[0] == self.username.as_ref().unwrap() ||
            up[1] == self.password.as_ref().unwrap() {
            return true;
        }
    }
    return false;
}
小结
代理在计算机网络很常见,比如服务器群组内部通常只会开一个口进行对外访问,就可以通过内网代理来进行处理,从而更好的保护内网服务器。代理让我们网络更安全,但是警惕非正规的代理可能会窃取您的数据。请用HTTPS内容访问更安全。
点击 [关注],[在看],[点赞] 是对作者最大的支持
35. 干货系列从零用Rust编写负载均衡及代理,代理服务器的源码升级改造的更多相关文章
- arcgis api 3.x for js 共享干货系列之二自定义 Navigation 控件样式风格(附源码下载)
		
0.内容概览 自定义 Navigation 控件样式风格 源码下载 1.内容讲解 arcgis api 3.x for js 默认的Navigation控件样式风格如下图:这样的风格不能说不好,各有各 ...
 - Docker系列-(3) Docker-compose使用与负载均衡
		
上一篇文章介绍了docker镜像的制作与发布,本文主要介绍实际docker工程部署中经常用到的docker-compose工具,以及docker的网络配置和负载均衡. Docker-compose介绍 ...
 - Nginx服务器部署 负载均衡 反向代理
		
Nginx服务器部署负载均衡反向代理 LVS Nginx HAProxy的优缺点 三种负载均衡器的优缺点说明如下: LVS的优点: 1.抗负载能力强.工作在第4层仅作分发之用,没有流量的产生,这个特点 ...
 - [架构]辨析: 高可用 | 集群 | 主从 | 负载均衡 | 反向代理 | 中间件 | 微服务 | 容器 | 云原生 | DevOps | ...
		
词汇集 灾备 冷备份 双机热备份 异地容灾备份 云备份 灾难演练 磁盘阵列(RAID) 故障切换 心跳监测 高可用 集群 主从复制(Master-Slave) 多集群横向扩容(master-clust ...
 - 死磕nginx系列--使用upsync模块实现负载均衡
		
问题描述 nginx reload是有一定损耗的,如果你使用的是长连接的话,那么当reload nginx时长连接所有的worker进程会进行优雅退出,并当该worker进程上的所有连接都释放时,进程 ...
 - hbase源码系列(一)Balancer 负载均衡
		
看源码很久了,终于开始动手写博客了,为什么是先写负载均衡呢,因为一个室友入职新公司了,然后他们遇到这方面的问题,某些机器的硬盘使用明显比别的机器要多,每次用hadoop做完负载均衡,很快又变回来了. ...
 - 架构之Nginx(负载均衡/反向代理)
		
Nginx ("engine x") 是一个高性能的 HTTP 和 反向代理 服务器 ,也是一个 IMAP/POP3/SMTP 代理 服务器 . Nginx 是由 Igor Sys ...
 - nginx域名转发 负载均衡 反向代理
		
公司有三台机器在机房,因为IP不够用,肯定要分出来,所以要建立单IP 多域名的反向代理, 就是当请求www.abc.com 跳转到本机, 请求www.bbc.com 跳转到192.168.0.35 机 ...
 - 《手把手教你》系列技巧篇(六)-java+ selenium自动化测试-阅读selenium源码(详细教程)
		
1.简介 前面几篇基础系列文章,足够你迈进了Selenium门槛,再不济你也至少知道如何写你第一个基于Java的Selenium自动化测试脚本.接下来宏哥介绍Selenium技巧篇,主要是介绍一些常用 ...
 - PHP扩展编写、PHP扩展调试、VLD源码分析、基于嵌入式Embed SAPI实现opcode查看
		
catalogue . 编译PHP源码 . 扩展结构.优缺点 . 使用PHP原生扩展框架wizard ext_skel编写扩展 . 编译安装VLD . Debug调试VLD . VLD源码分析 . 嵌 ...
 
随机推荐
- 深入探讨API调用性能优化与错误处理
			
 随着互联网技术的不断发展,API(应用程序接口)已经成为软件系统中重要的组成部分.而优化API调用的性能以及处理错误和异常情况则是保障系统稳定性和可靠性的关键.本文将从以下几个方面来探讨如何进行性 ...
 - Blazor前后端框架Known-V1.2.14
			
V1.2.14 Known是基于C#和Blazor开发的前后端分离快速开发框架,开箱即用,跨平台,一处代码,多处运行. Gitee: https://gitee.com/known/Known Git ...
 - Arrays.asList():使用指南
			
Arrays.asList() 是一个 Java 的静态方法,它可以把一个数组或者多个参数转换成一个 List 集合.这个方法可以作为数组和集合之间的桥梁,方便我们使用集合的一些方法和特性.本文将介绍 ...
 - 「codeforces - 1621G」Weighted Increasing Subsequences
			
link. 一个 dp(拜谢 ly)和切入点都略有不同的做法,并不需要观察啥性质. 原问题针对子序列进行规划,自然地想到转而对前缀进行规划.接下来我们考虑一个前缀 \([1, i]\) 以及一个 \( ...
 - 运行在容器中Postgres数据库数据损坏后如何恢复?
			
前言 在使用 K8S 部署 RSS 全套自托管解决方案- RssHub + Tiny Tiny Rss, 我介绍了将 RssHub + Tiny Tiny RSS 部署到 K8s 集群中的方案. 其中 ...
 - open3d -- voxel_down_sample
			
官网文档 parameter: Input: open3d.geometry.Pointcloud点云类 voxel_size: 体素单位长度 Return: 处理后的点云类 Description: ...
 - PXC集群脑裂导致节点是无法加入无主的集群
			
一套2节点的MySQL PXC集群,第1节点作为主用节点长时间的dml操作,导致大量的事务阻塞,出现异常,此时查看第2节点显示是primary状态,但无事务阻塞情况. 此时第1节点无法正常提供服务,于 ...
 - 触发器引起的ADG备库同步错误
			
数据库alert日志报错ORA-16000,查看对应的trc文件,大致如下报错: *** 2020-10-27 14:09:03.340*** SESSION ID:(3340.75) 2020-10 ...
 - .Net析构函数再论(CLR源码级的剖析)
			
前言 碰到一些问题,发觉依旧没有全面了解完全析构函数.本篇继续看下析构函数的一些引申知识. 概述 析构函数目前发现的总共有三个标记,这里分别一一介绍下.先上一段代码: internal class P ...
 - ExcelPatternTool 开箱即用的Excel工具包现已发布!
			
目录 ExcelPatternTool 功能 特点: 快速开始 使用说明 常规类型 高级类型 Importable注解 Exportable注解 IImportOption导入选项 IExportOp ...