4. 用Rust手把手编写一个wmproxy(代理,内网穿透等), TLS加密通讯

2023年 9月 30日 100.7k 0

用Rust手把手编写一个wmproxy(代理,内网穿透等), 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年弃用 datatracker.ietf.org/doc/rfc2246…
TLS1.1 2006年 RFC4346 2021年弃用 datatracker.ietf.org/doc/rfc4346…
TLS1.2 2008年 RFC5246 正在使用 datatracker.ietf.org/doc/rfc5246…
TLS1.3 2018年 RFC8446 正在使用 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.公钥私钥的生成
  • 准备两个非常大的素数p和q(转化成二进制后1024位或者4096或者更大位数,位数越多越难破解);
  • 计算出两个大素数的乘积n=pq;
  • 同样的方法计算m=(p-1)(q-1),这里的m为n的欧拉函数
  • 找到一个数e(1 < e < m),满足(e,m)的最大公约数为1,即互素
  • 找到数字d,需满足ed mod m = 1,即余数为1
  • 此时生成完毕,公钥为(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_SHA256TLS_AES_256_GCM_SHA384TLS_CHACHA20_POLY1305_SHA256TLS_AES_128_CCM_SHA256TLS_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中存在两种角色,

  • 末端的处理服务器
  • 中间方只进行流量转发
  • 关于TLS的参数有以下参数

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

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

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

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

    image.png

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

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

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

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

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

    源码说明

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

    async fn deal_stream(&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然后内部做协议解析,会减少握手时间,只是在流量非常大的时候会出现某条请求耗光了所有的带宽。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论