• 隐藏侧边栏
  • 展开分类目录
  • 关注微信公众号
  • 我的GitHub
  • QQ:1753970025
Chen Jiehua

使用Stunnel规避流量拦截 

最近部署代理用到了 Stunnel,经过一番折腾尝试才终于把配置搞定,在此记录一下~

背景

近日发现长期使用的 socks5 代理突然有点异常,访问国内网站正常,但访问 google 却总是提示“连接已重置”。查看日志发现:

2022-07-05 17:38:00 INFO     connecting www.google.com.hk:443 from xx.xx.xx.xx:42008
2022-07-05 17:38:00 ERROR    [Errno 104] Connection reset by peer

既然能正常访问国内网站,说明代理并未被直接屏蔽;但却又不能访问 google,说明部分流量遭到拦截过滤。

由于 socks5 工作在OSI七层模型中第5层(会话层),处于应用层之下、传输层之上,所以它能够对各种应用协议数据进行处理(比如:HTTP/HTTPS,SMTP,POP3等)。然而,socks5 代理只是简单的对数据包进行转发处理,并不进行任何加密;即便是访问 HTTPS 网站,连接过程中的数据虽然经过 SSL/TLS 加密无法被抓包,但连接建立之初的握手仍然是明文传输,所以会被拦截干扰。

既然如此,有没有什么办法可以对整个连接进行加密呢?答案就是 stunnel,官方对其介绍:

Stunnel is a proxy designed to add TLS encryption functionality to existing clients and servers without any changes in the programs’ code.

对于本身无法进行TLS或SSL通信的客户端及服务器,Stunnel可提供加密安全连接。Stunnel依赖openssl,使用基于X.509数字证书的公开密钥加密算法来保证SSL的安全连接。

部署

生成证书

由于 stunnel 需要 X.509 证书,我们先用 openssl 命令生成一个自签名的数字证书:

$ openssl req -new -x509 -days 3650 -nodes -out stunnel.pem -keyout stunnel.pem

参数简要说明:

  • -new:生成一个新的密钥;
  • -x509:生成一个自签名的X509证书;
  • -days:证书有效期,3650即10年;
  • -nodes:私钥不需要输入密码;
  • -out:证书的保存文件;
  • -keyout:私钥的保存文件;

生成证书时需要输入证书的一些相关信息,由于我们是自己使用也不需要第三方CA签名,所以全部使用默认值即可。

查看生成的 stunnel.pem 文件内容,可以发现其中包含了证书(CERTIFICATE)和私钥(PRIVATE KEY):

$ cat stunnel.pem
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC58ae6ts2Y0qtu
……中间部分省略……
UdXQnd9i/j/rQp4kMDYY72OldxyPElBcWChuj8cjrkK/H1e3aum98Bge4yD00pqC
v/S1yVvT0CZAl4B5mH7yhvoBKw==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDfzCCAmegAwIBAgIULLUUTjgqQt0u1kbay0iKLR8sPUIwDQYJKoZIhvcNAQEL
……中间部分省略……
w94HeU1EUyzGbZSqtELCjWDccq0OomspIgqsNRyFZ03yFGI=
-----END CERTIFICATE-----

服务端配置

首先我们需要进行 Stunnel 的服务端配置,在 Ubuntu 上可以直接用 apt 进行安装(如果你的系统没有提供stunnel软件包,可以到官网下载):

$ sudo apt install stunnel4
// 查看配置示例
$ cat /usr/share/doc/stunnel4/examples/stunnel.conf-sample

然后创建一个配置文件:

$ sudo vim /etc/stunnel/stunnel.conf

;分号表示注释
debug = 7
output = /var/log/stunnel.log
cert = /etc/stunnel/stunnel.pem
key = /etc/stunnel/stunnel.pem

[proxy]
accept = 2345
connect = 127.0.0.1:1080

配置简要说明:

  • debug:日志等级,7输出最详细,出现异常时方便定位问题;
  • output:日志位置;
  • cert:证书文件,前面用openssl生成自签名数字证书;
  • key:私钥文件,如果cert指定的证书已经包含了私钥,则可以省略;
  • [proxy]:服务名字,可以自定义;
  • accept:监听的地址;
  • connect:连接的后端服务地址;

客户端配置

接下来进行 Stunnel 的客户端配置,与服务端类似,先安装 stunnel,然后创建一个配置文件:

$ sudo vim /etc/stunnel/stunnel.conf

client = yes
output = /var/log/stunnel.log

[proxy]
accept = 1080
connect = stunnel server:2345

配置简要说明:

  • client:表示这是客户端配置;
  • accept:本地监听的地址;
  • connect:stunnel服务端的地址;

配置完成后,分别在服务端和客户端启动 stunnel 服务:

$ sudo service stunnel4 restart

这样子,本地客户端请求到 1080 端口的流量都会经过 stunnel 以 TLS 加密后发送到服务端,其中 stunnel 通信的端口 2345 可以自定义。

可以使用 curl 测试一下代理是否正常:

$ curl -x socks5h://127.0.0.1:1080 https://www.google.com.hk

最后,设置开机自动启动:

$ sudo systemctl enable stunnel4.service

鉴权

校验服务器

首先我们来思考一下 SSL 连接是怎样建立的,以及证书在这个过程中是如何发挥作用?

当一个 SSL 客户端连接到 SSL 服务器时,服务器会返回一个数字证书,以此证明我确实是你要请求的服务器,并非他人伪装。这个数字证书需要由证书认证中心(Certificate Authority,即所谓的 CA)签发,CA一般是第三方认证机构,他们也有自己的数字证书,并由根CA签发;客户端操作系统会预装一些受信任的根证书。

SSL 客户端收到服务器的数字证书后,它会进行校验:

  • 证书跟服务器使用的私钥相匹配(证书本质上是一个公钥,可以解密私钥加密的内容);
  • 证书由CA签发;
  • 客户端信任该CA;

由于我们采用的是自签名的证书,只满足第一个条件,而 stunnel 默认情况下并不对证书进行强制校验,因此按照上面的配置有可能遭到中间人攻击(Man in the middle, MITM),启动 stunnel 客户端时也可以从日志中看到提示:

2022.07.06 00:45:28 LOG4[ui]: Service [proxy] needs authentication to prevent MITM attacks

因此,我们可以在 stunnel 客户端配置中增加对服务器证书进行校验:

$ sudo vim /etc/stunnel/stunnel.conf

client = yes
output = /var/log/stunnel.log

[proxy]
accept = 1080
connect = stunnel server:2345
CAfile = /etc/stunnel/stunnel.pem
verifyPeer = yes

参数简要说明:

  • CAfile:CA证书文件,如果有多个CA可以直接将内容合并在同一个文件中,或者用CApath指定目录(但文件名有要求,配置比较麻烦);
  • verifyPeer:要求强制校验证书;

需要注意的是,CAfile 指定的文件只需要包含证书,前面使用 openssl 生成数字证书时可以分开证书和私钥,只把证书发给客户端,避免私钥泄露

$ openssl req -new -x509 -days 3650 -nodes -out stunnel_cert.pem -keyout stunnel_key.pem

$ cat stunnel_cert.pem
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUTc+th1R/9vyC38ailL6E/RxIhWswDQYJKoZIhvcNAQEL
……省略……
IDXc+w7wyAE9kceMFx0clCI/ERR/7/ic8tnf2sFoDwmChCz0P6fEohIp6+IMdnNr
j9OEbGjaK84IIv1fRyod
-----END CERTIFICATE-----

$ cat stunnel_key.pem
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDXcgHx5pegoNne
……省略……
MRlcp4bCi5xgYOtfLdQEznshzyxvjNHw+3fOdSrUzKMqANwvxOLANZofFF4elEGW
gnUs+echxaA4S7zxKY9jS6c=
-----END PRIVATE KEY-----

如果服务器与客户端的证书不匹配,则无法正常建立连接:

$ curl -x socks5h://127.0.0.1:1080 https://www.google.com.hk
curl: (97) Unable to receive initial SOCKS5 response.

stunnel 客户端日志报错:

2022.07.06 11:43:23 LOG4[0]: CERT: Pre-verification error: self-signed certificate
2022.07.06 11:43:23 LOG4[0]: Rejected by CERT at depth=0: C=CN, ST=State, L=City, O=Internet Widgits Pty Ltd
2022.07.06 11:43:23 LOG3[0]: SSL_connect: ../ssl/statem/statem_clnt.c:1883: error:0A000086:SSL routines::certificate verify failed
2022.07.06 11:43:23 LOG5[0]: Connection reset: 0 byte(s) sent to TLS, 0 byte(s) sent to socket

校验客户端

虽然客户端完成了服务器校验(防止中间人攻击),但你可能也留意到任何客户端都可以连接到我们服务器,这简直太危险了!所以我们还需要配置 stunnel 服务器对客户端进行校验。

与服务端类似,stunnel 客户端在连接时也可以提供证书给服务器进行校验(即 peer certificate),证书的生成方式与服务端完全一致。

$ openssl req -new -x509 -days 3650 -nodes -out server_cert.pem -keyout server_key.pem
$ openssl req -new -x509 -days 3650 -nodes -out client_cert.pem -keyout client_key.pem

服务端配置:

output = /var/log/stunnel.log
cert = /etc/stunnel/server_cert.pem
key = /etc/stunnel/server_key.pem

[proxy]
accept = 2345
connect = 127.0.0.1:1080
CAfile = /etc/stunnel/client_cert.pem
verifyPeer = yes

客户端配置:

client = yes
output = /var/log/stunnel.log
cert = /etc/stunnel/client_cert.pem
key = /etc/stunnel/client_key.pem

[proxy]
accept = 1080
connect = stunnel server:2345
CAfile = /etc/stunnel/server_cert.pem
verifyPeer = yes

配置说明:cert、key、CAfile、verifyPeer等配置项可以作为全局使用,可以针对每个服务单独设置。

如果 stunnel 客户端提供了不匹配的证书,则无法正常建立连接:

$ curl -x socks5h://127.0.0.1:1080 https://www.google.com.hk
curl: (97) Unable to receive initial SOCKS5 response.

stunnel 服务端日志报错:

2022.07.06 12:11:52 LOG4[0]: CERT: Pre-verification error: self signed certificate
2022.07.06 12:11:52 LOG4[0]: Rejected by CERT at depth=0: C=AU, ST=Some-State, O=Internet Widgits Pty Ltd
2022.07.06 12:11:52 LOG3[0]: SSL_accept: ../ssl/statem/statem_srvr.c:3701: error:1417C086:SSL routines:tls_process_client_certificate:certificate verify failed
2022.07.06 12:11:52 LOG5[0]: Connection reset: 0 byte(s) sent to TLS, 0 byte(s) sent to socket

stunnel 客户端日志提示:

2022.07.06 12:11:52 LOG3[1]: SSL_read: ../ssl/record/rec_layer_s3.c:1584: error:0A000418:SSL routines::tlsv1 alert unknown ca

此外,Stunnel也支持 PSK 验证,具体配置可以参考文档

测试

经过一番配置之后,理论上来说所有流量都经过 TLS 加密了,那要怎么验证一下呢?祭出大杀器 tcpdump。

直连 socks5 代理进行请求抓包(先执行tcpdump,再用curl请求),可以看到部分明文数据:

// 终端2
$ curl -x socks5h://my_socks5_server:1080 https://www.baidu.com

// 终端1
$ sudo tcpdump port 1080 -A | grep -i baidu
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on ens160, link-type EN10MB (Ethernet), snapshot length 262144 bytes
...9.   ...3.....=.<.5./.....u.........www.baidu.com.........
.0Beijing Baidu Netcom Science Technology Co., Ltd1.0...U...    baidu.com0.."0..        *.H.............0..
........+.b1......GFZjY"w..t0.'.g06p$.F.l...+Vu....lJ....^i.jW..X..=.'.V...q.rs....Bq....#h...@._..uu....`VI;.....n;hu3..9........[AX.+...owc' ..0...7..3...0....Hg..n=..2X...".5.k...M.\.....[G....(...\...*t..4.~D... ..t...^.....0..KNE.A.U...#(.W...]..... ..m..m.........0...0...U...........0....+..........0.0D..+.....0..8http://secure.globalsign.com/cacert/gsrsaovsslca2018.crt07..+.....0..+http://ocsp.globalsign.com/gsrsaovsslca20180V..U. .O0M0A.       +.....2..0402..+........&https://www.globalsign.com/repository/0...g.....0  ..U....0.0?..U...80604.2.0..http://crl.globalsign.com/gsrsaovsslca2018.crl0..a..U.....X0..T.        baidu.com..click.hm.baidu.com..cm.pos.baidu.com..log.hm.baidu.com..update.pan.baidu.com..wn.pos.baidu.com..*.91.com..*.aipage.cn..*.aipage.com..*.apollo.auto..*.baidu.com..*.baidubce.com..*.baiducontent.com..*.baidupcs.com..*.baidustatic.com..*.baifubao.com..*.bce.baidu.com..*.bcehost.com..*.bdimg.com..*.bdstatic.com..*.bdtjrcv.com..*.bj.baidub

经过 stunnel 加密转发的抓包,已经看不到明文数据了:

// 终端2
$ curl -x socks5h://127.0.0.1:1080 https://www.baidu.com

// 终端1(监听stunnel端口)
$ sudo tcpdump port 2345 -A | grep -i baidu
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on ens160, link-type EN10MB (Ethernet), snapshot length 262144 bytes

另外,经过 stunnel 加密后的 socks5 代理也能正常访问 google 了,说明流量也没有被拦截了。

至此,大功告成~

参考:

码字很辛苦,转载请注明来自ChenJiehua《使用Stunnel规避流量拦截》

评论