用Rust手把手编写一个Proxy(代理), TLS加密通讯

项目 ++wmproxy++

gite: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

为什么选择TLS

了解TLS

安全传输层协议(TLS)用于在两个通信应用程序之间提供保密性和数据完整性。

该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。

TLS版本的历程

版本 发表年份 RFC文件 弃用年份 RFC链接
TLS1.0 1999年 RFC2246 2021年弃用 https://datatracker.ietf.org/doc/rfc2246/
TLS1.1 2006年 RFC4346 2021年弃用 https://datatracker.ietf.org/doc/rfc4346/
TLS1.2 2008年 RFC5246 正在使用 https://datatracker.ietf.org/doc/rfc5246/
TLS1.3 2018年 RFC8446 正在使用 https://datatracker.ietf.org/doc/rfc8446/

TLS协议的优势是与高层的应用层协议(如HTTP、FTP、Telnet等)无耦合。应用层协议能透明地运行在TLS协议之上,由TLS协议进行创建加密通道需要的协商和认证。应用层协议传送的数据在通过TLS协议时都会被加密,从而保证通信的私密性。

我们此时正应用他与应用层完全不耦合,又经历20年的发展历程非常的完善和安全,完全可以信任。

sequenceDiagram
Client->>Server: TLS协议版本、随机数、支持的加密套件和对应公钥A

Server-)Server: 生成随机数B,根据信息生成密钥
Server-->>Client: 选用加密套件,服务端随机数,服务端证书
Server-->>Client: 使用的P、G、公钥B与签名
Server-->>Client: 握手报文的信息(服务端加密)

Client-)Client: 验证证书,使用a、B计算出K,得到密钥
Client->>Server: 握手报文的信息(客户端加密)
Client->>Server: 应用数据(客户端加密)
Server-->>Client: 应用数据(服务端加密)

了解RSA算法

1. 算法原理

算法本身基于一个简单的数论知识:给出两个素数,很容易将它们相乘,然而给出它们的乘积,想得到这两个素数就显得尤为困难。如果能够解决大整数(比如几百位的整数)分解的快速方法,那么 RSA 算法将轻易被破解。

2.公钥私钥的生成
  1. 准备两个非常大的素数p和q(转化成二进制后1024位或者4096或者更大位数,位数越多越难破解);
  2. 计算出两个大素数的乘积n=pq;
  3. 同样的方法计算m=(p-1)(q-1),这里的m为n的欧拉函数
  4. 找到一个数e(1 < e < m),满足(e,m)的最大公约数为1,即互素
  5. 找到数字d,需满足ed mod m = 1,即余数为1
  6. 此时生成完毕,公钥为(n,e),私钥为(n, d)
3. RSA加密
对明文x,用公钥(n, e)对x加密,将x转换成数字,通过公式得出密文y
y = x^e mod n

4. RSA解密

对明文y,用私钥(n, d)对y解密
x = y^d mod n

5. 小数测试

取p=5,q=11,得到n=p*q=55

m=(p-1)(q-1) = 40

取e=3,根据ed mod m = 1,可取d=27

此时公钥(n, e)=(55, 3)

此时私钥(n, d)=(55, 27)

提供明文a = 14,用公钥加密则密文c = a ^ e mod n = 14 ^ 3 mod 55 = 49

解密密文b = 49,用私钥解密则明文d = b ^ d mod n = 49 ^ 27 mod 55 = 14

6. 性能分析

因为RSA用到了指数级的计算,位数又是至少1024位起的,所以计算量非常的庞大,所以RSA的算法效率并不高,所以TLS除一开始密文交换的时候用到RSA,后续均用得到的密文做对称加密以减少计算量,TLS1.3所用如下TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256等对称加密算法。

7. Nginx证书文件pem及key

key文件,即包含-----BEGIN RSA PRIVATE KEY-----的文件,这里面包含的信息有n, e, d, p, q等完整的RSA信息,也是保证安全最重要的信息,格式类似如下

RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER, -- n
publicExponent INTEGER, -- e
privateExponent INTEGER, -- d
prime1 INTEGER, -- p
prime2 INTEGER, -- q
exponent1 INTEGER, -- d mod (p-1)
exponent2 INTEGER, -- d mod (q-1)
coefficient INTEGER, -- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos OPTIONAL
}

pem文件,包含了公钥信息(n, d)及证书链信息,可以知道谁签发的。

加密节点实现

角色说明,在wmproxy中存在两种角色,

  1. 末端的处理服务器
  2. 中间方只进行流量转发

关于TLS的参数有以下参数

pub struct Proxy {
/// 连接服务端是否启用tls
ts: bool,
/// 接收客户端是否启用tls
tc: bool,
/// tls证书所用的域名
domain: Option<String>,
/// 公开的证书公钥文件
cert: Option<String>,
/// 隐私的证书私钥文件
key: Option<String>,
}

因为加密存在可能的性能损耗,若在私有网络里不存在传输安全理论上可以不用开启加密传输。如果存在多个节点,前面节点已启用过加密,理论上后面节点也无需多次加密。

直接用https传输可能暴露什么?

因为客户端发起Client Hello的时候必须带上访问的domain,也就是网络的嗅探方虽然无法知道你访问的具体内容,但是可以知道你访问的网站列表。如:

启动二级代理
  1. 在本地启动代理
wmproxy -b 127.0.0.1 -p 8090 -S 127.0.0.1:8091 --ts

因为纯转发,所以在当前节点设置账号密码没有意义-S表示连接到的二级代理地址,有该参数则表示是中转代理,否则是末端代理。--ts表示连接父级代理的时候需要用加密的方式链接

  1. 在远程启动代理
wmproxy --user proxy --pass proxy -b 0.0.0.0 -p 8091 --tc

--tc表示接收子级代理的时候需要用加密的方式链接,可以--cert指定证书的公钥,--key指定证书的私钥,--domain指定证书的域名,如果不指定,则默认用自带的证书参数

至此通过代理访问的,我们已经没有办法得到真正的请求地址,只能得到代理发起的请求

源码说明

关于TLS依赖,选择的是rustls,tokio-rustls。

那么关于客户端的连接,那就有两种情况,一种是TcpStream,另一种是TlsStream<TcpStream>,我们的处理函数不确定传入的是哪种类型,所以此前的入参TcpStream全部改成泛型T,类似

async fn deal_stream<T>(&mut self, inbound: T) -> ProxyResult<()>
where T: AsyncRead + AsyncWrite + Unpin {
}

这样子只要可以异步读和写都可以成为入参的流。

如果存在tc参数,那么会将客户端转成TlsStream以便继续处理

if let Some(a) = accept.clone() {
let inbound = a.accept(inbound).await;
if let Ok(inbound) = inbound {
// 获取的流跟正常内容一样读写, 在内部实现了自动加解密
let _ = self.deal_stream(inbound).await;
} else {
println!("accept error = {:?}", inbound.err());
}
} else {
let _ = self.deal_stream(inbound).await;
};

客户端连接

let connector = TlsConnector::from(tls_client.unwrap());
let stream = TcpStream::connect(&server).await?;
// 这里的域名只为认证设置
let domain = rustls::ServerName::try_from(&*domain.unwrap_or("soft.wm-proxy.com".to_string()))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid dnsname"))?; if let Ok(mut outbound) = connector.connect(domain, stream).await {
// connect 之后的流跟正常内容一样读写, 在内部实现了自动加解密
let _ = tokio::io::copy_bidirectional(&mut inbound, &mut outbound).await?;
}

这里利用的是TLS与上层解藕,只要他参与握手完之后,完全按我们的通讯来定。

后续改进

现在每个请求都和代理服务端进行一次请求握手,当开启断开非常多的时候会比较耗性能,可以考虑共用一条socket然后内部做协议解析,会减少握手时间,只是在流量非常大的时候会出现某条请求耗光了所有的带宽。

用Rust手把手编写一个Proxy(代理), TLS加密通讯的更多相关文章

  1. EFK教程(4) - ElasticSearch集群TLS加密通讯

    基于TLS实现ElasticSearch集群加密通讯 作者:"发颠的小狼",欢迎转载 目录 ▪ 用途 ▪ ES节点信息 ▪ Step1. 关闭服务 ▪ Step2. 创建CA证书 ...

  2. EFK-4::ElasticSearch集群TLS加密通讯

    转载自:https://mp.weixin.qq.com/s?__biz=MzUyNzk0NTI4MQ==&mid=2247483822&idx=1&sn=6813b22eb5 ...

  3. 新的知识点来了-ES6 Proxy代理 和 去银行存款有什么关系?

    ES给开发者提供了一个新特性:Proxy,就是代理的意思.也就是我们这一节要介绍的知识点. 以前,ATM还没有那么流行的时候(暴露年纪),我们去银行存款或者取款的时候,需要在柜台前排队,等柜台工作人员 ...

  4. Java实战_手把手编写记事本

    Java运用SWT插件编写桌面记事本应用程序 可实现windows系统桌面记事本基本功能.傻瓜式教学,一步一步手把手操作.小白也可自己编写出完整的应用程序. 须要工具:Eclipse(带SWT插件) ...

  5. 自己写一个java.lang.reflect.Proxy代理的实现

    前言 Java设计模式9:代理模式一文中,讲到了动态代理,动态代理里面用到了一个类就是java.lang.reflect.Proxy,这个类是根据代理内容为传入的接口生成代理用的.本文就自己写一个Pr ...

  6. 手把手教你编写一个具有基本功能的shell(已开源)

    刚接触Linux时,对shell总有种神秘感:在对shell的工作原理有所了解之后,便尝试着动手写一个shell.下面是一个从最简单的情况开始,一步步完成一个模拟的shell(我命名之为wshell) ...

  7. 手把手教你编写一个简单的PHP模块形态的后门

    看到Freebuf 小编发表的用这个隐藏于PHP模块中的rootkit,就能持久接管服务器文章,很感兴趣,苦无作者没留下PoC,自己研究一番,有了此文 0×00. 引言 PHP是一个非常流行的web ...

  8. Hexo+NexT(六):手把手教你编写一个Hexo过滤器插件

    Hexo+NexT介绍到这里,我认为已经可以很好地完成任务了.它所提供的一些基础功能及配置,都已经进行了讲解.你已经可以随心所欲地配置一个自己的博客环境,然后享受码字的乐趣. 把博客托管到Github ...

  9. caddy & grpc(3) 为 caddy 添加一个 反向代理插件

    caddy-grpc 为 caddy 添加一个 反向代理插件 项目地址:https://github.com/yhyddr/caddy-grpc 前言 上一次我们学习了如何在 Caddy 中扩展自己想 ...

  10. C# 使用 Proxy 代理请求资源

    C# 使用 Proxy 请求资源,基于 HttpWebRequest 类 前言 这是上周在开发 C# 中使用 Proxy 代理时开发的一些思考和实践.主要需求是这样的,用户可以配置每次请求是否需要代理 ...

随机推荐

  1. AI 协助办公 |记一次用 GPT-4 写一个消息同步 App

    GPT-4 最近风头正劲,作为 NebulaGraph 的研发人员的我自然是跟进新技术步伐.恰好,现在有一个将 Slack channel 消息同步到其他 IM 的需求,看看 GPT-4 能不能帮我完 ...

  2. Python 安装教程,新手入门(超详细)含Pycharm开发环境安装教程

    目录 一.Python介绍 二.Python安装教程 (一)Python的下载 (二)Python的安装 三.Pycharm开发工具的安装 (一)Pycharm介绍 (二)Pycharm的下载 (三) ...

  3. 洛谷 P5979 [PA2014] Druzyny

    简要题意 有 \(n\) 个人,把他们划分成尽可能多的区间,其中第 \(i\) 个人要求它所在的区间长度大于等于 \(c_i\),小于等于 \(d_i\),求最多的区间数量以及如此划分的方案数. 数据 ...

  4. C++'s most vexing parse

    本文地址 https://www.cnblogs.com/wanger-sjtu/p/16876846.html C++'s most vexing parse 是 Scott Meyers 在其名著 ...

  5. CoFiltering:BestPracticesandTechniquesinTextGeneration

    目录 Co-Filtering: Best Practices and Techniques in Text Generation Introduction: Text generation has ...

  6. Java输入三个班每班三个人,输入成绩,分别计算每个班级的总分和平均分

    代码如下: public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int score; ...

  7. 解决 echarts柱状图x轴数据隔一个显示

    echarts柱状图x轴数据隔一个显示   在显示一个柱状图的时候,x轴的数据是隔一个显示的,但是hover的时候却能显示出当前轴x轴的名称,如下图: 那就只能说明这个轴的名称太长了,所以导致它默认就 ...

  8. 五分钟了解MES与MOM的区别和联系

    大家好,我是Edison. 上一篇,我们通过了解了MES系统的发展历程和标准体系.本篇,我们来快速了解一下近年来吵得很热的MOM是什么鬼,它和MES到底有什么区别和联系. MES是什么 MES (Ma ...

  9. PNG结构

    参考此博客 PNG的文件头总是固定的八个字节 89 50 4E 47 0D 0A 1A 0A 数据块长度13 00 00 00 0D 文件头数据块标识IDCH 49 48 44 52 13位数据块(I ...

  10. Oracle备份与还原(实用版)

    Oracle备份与还原 EXP和IMP是客户端工具程序,它们既可以在客户端使用,也可以在服务端使用. EXPDP和IMPDP是服务端的工具程序,他们只能在ORACLE服务端使用,不能在客户端使用. I ...