Squid透明代理

我在 私有云架构 中使用 Apple AirPort 结合 私有云DNS服务(dnsmasq)和共享因特网(ICS) 实现了局域网内部无线客户端能够共享上网。同时,为了解决带宽资源,同时方便内部服务器快速安装和更新软件,在 zcloud 主机上部署了 Squid代理服务 作为代理。进一步,通过 APT无阻碍代理架构 解决了访问Google等 Kubernetes Atlas 软件仓库更新软件问题。

不过,对于普通用户而言,例如手机客户端,连接到WiFi上,虽然通过 iptables配置因特网共享连接(ICS) 确实能够上网。但是,受阻于GFW,无法自由访问信息。配置手机客户端的代理也非常麻烦,切换网络还要切换代理。所以,更好的解决方法是使用 透明代理 ,也就是让所有客户端外出访问能够自动经过代理服务器,这样只需要在代理服务器上实现 翻墙 就能够解决用户的 痛点

透明代理需要代理服务器实现 中间人 HTTPS代理,但是这种透明代理模式需要用代理服务器的SSL/TLS证书代替客户端想要访问的网站SSL/TLS证书。这种模式对于客户端是不能容忍的安全隐患,虽然可以通过导入代理服务器的自签名证书来绕过这个验证问题,但是对于客户端部署有要求,所以实际使用效果并不是完全透明,依然需要配置客户端。

实践发现另一个问题是 Squid父级socks代理 失效,虽然我最终实现了透明代理,但是无法使用之前配置的 Squid父级socks代理 翻墙,所以感觉非常沮丧。有可能有其他方法可以克服这个问题,但是暂时没有精力来继续折腾了。

squid透明代理 探索过程中,发现通过 DNSmasq部署DHCP WPAD(WEB代理自动发现)DNSmasq部署DNS WPAD(WEB代理自动发现) 结合 Nginx部署WPAD服务 实现动态配置客户端代理,可以更为方便实现局域网 透明 翻墙访问。

http透明代理

http透明代理的原理其实非常简单,就是在网关的内网接口上启用 iptables 的转发,将访问WEB端口重定向到 squid 的代理端口上;此时还需要激活 squidintercept (拦截) 功能,这样就能实现对客户端无感的透明代理。

  • 修订 /etc/squid/squid.conf 配置,添加一个 intercept 配置行,注意,需要有两个代理端口,一个是传统的 forward proxy 一个是 intercept

    http_port 8080
    http_port 3128 intercep
    
  • 修订 私有云DNS服务(dnsmasq)和共享因特网(ICS) 中的 ics.sh (配置MASQUERADE)脚本,添加:

    # squid transparent proxy
    sudo iptables -t nat -A PREROUTING -i br0 -p tcp --dport 80 -j DNAT --to 192.168.6.200:3128
    sudo iptables -t nat -A PREROUTING -i br0:1 -p tcp --dport 80 -j DNAT --to 192.168.6.200:3128
    sudo iptables -t nat -A PREROUTING -i eno4 -p tcp --dport 80 -j REDIRECT --to-port 3128
    

完整脚本参考 私有云DNS服务(dnsmasq)和共享因特网(ICS) 中的 ics.sh

https透明代理

https代理实现则比较复杂,早期的squid并没有提供透明拦截SSL连接(transparently intercept SSL connections)。不过,从2011 年开始,squid开始支持作为SSL中间人代理(act as a man in the middle)。这个设置背后的原理就是解密HTTPS连接然后进行内容过滤。

不过,需要担心的是透明代理打断了HTTPS流量,也就导致了可能存在到的和法律问题。不过,对于个人使用或者能够控制安全性的环境,可以采用这种模式。

警告

透明代理HTTPS相当于代理服务器是中间人,需要绝对保障代理服务器安全,否则会导致安全泄密。

技术上,在编译 squid 源代码时候,需要激活 --enable-ssl 选项以支持 SslBump ,也就是squid透明连接SSL流量的技术要求。

SSL key

一个非常重要的步骤是为终端用户创建squid使用的证书,在测试环境,可以采用自签名证书。生产环境,则使用 let's encryption 提供的证书。

  • 生成SSL key:

    mkdir -p /etc/squid/certs/
    cd /etc/squid/certs/
    # This puts the private key and the self-signed certificate in the same file
    openssl req -new -newkey rsa:4096 -sha256 -days 3650 -nodes -x509 -keyout myCA.pem -out myCA.pem
    # This can be added to browsers
    openssl x509 -in myCA.pem -outform DER -out myCA.der
    

初始化SSL数据库

  • 初始化SSL数据库: Squid需要生成一个新的 fake (伪造) 自签名证书用来 bump (碰撞) SSL连接,这些都会保存在一个目录中:

以下是Ubuntu 20中执行命令(用户名是 proxy ):

sudo /usr/lib/squid/security_file_certgen -c -s /var/lib/ssl_db -M 4MB
sudo chown proxy:proxy -R /var/lib/ssl_db

以下是 Fedora系统执行命令(用户名是 squid ):

sudo /usr/lib64/squid/security_file_certgen -c -s /var/lib/ssl_db -M 4MB
sudo chown squid:squid -R /var/lib/ssl_db

修正客户端证书

客户端默认不使用自签名证书,所以需要做以下修复:

  • 浏览器: 使用 myCA.der 来导入证书

  • Linux: 复制cert文件到 /etc/pki/ca-trust/source/anchors 目录下然后运行 update-ca-trust

  • Node.js: 待验证

  • Headless Chrome (Puppeteer): 待验证

配置iptables

  • 使用 PREROUTING

    sudo iptables -t nat -A PREROUTING -i br0 -p tcp --dport 443 -j DNAT --to 192.168.6.200:3128
    sudo iptables -t nat -A PREROUTING -i br0:1 -p tcp --dport 443 -j DNAT --to 192.168.6.200:3128
    sudo iptables -t nat -A PREROUTING -i eno4 -p tcp --dport 443 -j REDIRECT --to-port 3128
    

配置

  • /etc/squid/squid.conf 中配置:

    http_port 8080
    http_port 3128 intercept
    http_port 3129 intercept ssl-bump cert=/etc/squid/cert/myCA.pem \
      generate-host-certificates=on dynamic_cert_mem_cache_size=4MB
    
    # For squid 3.5.x
    # sslcrtd_program /usr/local/squid/libexec/ssl_crtd -s /var/lib/ssl_db -M 4MB
    
    # For squid 4.x
    # sslcrtd_program /usr/local/squid/libexec/security_file_certgen -s /var/lib/ssl_db -M 4MB
    
    sslcrtd_program /usr/lib/squid/security_file_certgen -s /var/lib/ssl_db -M 4MB
    
    acl step1 at_step SslBump1
    
    ssl_bump peek step1
    ssl_bump bump all
    
  • 启动:

    sudo systemctl start squid
    
  • 然后观察 /var/log/squid/*.log

HTTP透明代理错误:No forward-proxy ports

注意,在 squid.conf 配置中如果只配置一行:

http_port 3128 intercept

来代替原先默认的:

http_port 3128

就会出现 squid 无法启动问题。此时检查 systemctl status squid 会看到如下报错:

...
ERROR: No forward-proxy ports configured.

这个报错原因实际上是因为 squid 在配置 intercept 模式端口同时还需要保留一个 forward proxy 端口,也就是不惜类似如下配置:

http_port 8080
http_port 3128 intercept

即使实际上不使用 forward proxy 也需要配置一行 http_port 8080 (其他端口也可以)

HTTPS透明代理:error:invalid-request

启动了https透明代理,我发现日志中出现大量:

1646123605.853      0 192.168.6.22 NONE_NONE/400 3685 - error:invalid-request - HIER_NONE/- text/html
1646123606.086      0 192.168.6.22 NONE_NONE/000 0 - error:transaction-end-before-headers - HIER_NONE/- -
1646123606.094      0 192.168.6.22 NONE_NONE/400 3685 - error:invalid-request - HIER_NONE/- text/html

这个报错似乎是因为配置了:

http_port 3129 intercept ssl-bump cert=/etc/squid/cert/myCA.pem \
  generate-host-certificates=on dynamic_cert_mem_cache_size=4MB

修改成 https_port

https_port 3129 intercept ssl-bump cert=/etc/squid/cert/myCA.pem \
  generate-host-certificates=on dynamic_cert_mem_cache_size=4MB

此时日志报错显示:

==> access.log <==
1646124814.877    164 192.168.6.22 NONE_NONE/000 0 CONNECT 203.119.129.34:443 - ORIGINAL_DST/203.119.129.34 -
1646124815.121     80 192.168.6.22 NONE_NONE/000 0 CONNECT 59.82.15.79:443 - ORIGINAL_DST/59.82.15.79 -

==> cache.log <==
2022/03/01 16:53:36 kid1| ERROR: failure while accepting a TLS connection on conn239 local=117.18.232.200:443 remote=192.168.6.22:53993 FD 28 flags=33: 0x5561efffa6a0*1
    current master transaction: master55

这个报错其实已经说明建立了HTTPS代理了,仔细观察 /var/lib/ssl_db/certs 可以看到缓存了大量的远端服务器证书

failure while accepting a TLS 表示当尝试TLS连接失败。参考 Intercept HTTPS CONNECT messages with SSL-Bump 提到 Modern DH/EDH ciphers usage :

现代化 DH/EDH exchanges/ciphers 选项 tls-dh= 所以我尝试 (这个方法估计可行,类似tls配置):

https_port 3129 intercept ssl-bump cert=/etc/squid/certs/myCA.pem \
  generate-host-certificates=on dynamic_cert_mem_cache_size=4MB \
  tls-dh=/etc/squid/certs/dhparam.pem

但是此时报错变成监测到不正确端口:

==> cache.log <==
2022/03/01 20:47:48 kid1| SECURITY ALERT: Host header forgery detected on conn27 local=192.168.6.200:3128 remote=192.168.6.253:56774 FD 16 flags=33 (intercepted port does not match 443)
    current master transaction: master57
2022/03/01 20:47:48 kid1| SECURITY ALERT: By user agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0
    current master transaction: master57
2022/03/01 20:47:48 kid1| SECURITY ALERT: on URL: www.google.com:443
    current master transaction: master57
2022/03/01 20:47:48 kid1| kick abandoning conn27 local=192.168.6.200:3128 remote=192.168.6.253:56774 FD 16 flags=33
    connection: conn27 local=192.168.6.200:3128 remote=192.168.6.253:56774 FD 16 flags=33

==> access.log <==
1646138868.558      0 192.168.6.253 NONE_NONE/409 4212 CONNECT www.google.com:443 - HIER_NONE/- text/html

此外,根据该文档提示 TLS ,有一个 squid 外连网站的设置:

sslproxy_cipher EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH+aRSA+RC4:EECDH:EDH+aRSA:HIGH:!RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS

或者使用:

sslproxy_cipher HIGH:MEDIUM:!RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS

或者使用:

tls_outgoing_options cipher=HIGH:MEDIUM:!RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS

我尝试了一下 sslproxy_cipher HIGH:MEDIUM:!RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS ,结果,访问 google 也同样出现:

2022/03/01 20:59:41 kid1| SECURITY ALERT: Host header forgery detected on conn29 local=192.168.6.200:3128 remote=192.168.6.253:56782 FD 18 flags=33 (intercepted port does not match 443)
 current master transaction: master61

我注意到一个奇怪的问题,我的配置中设置了:

http_port 8080
http_port 3128 intercept
https_port 3129 intercept ssl-bump cert=/etc/squid/certs/myCA.pem \
  generate-host-certificates=on dynamic_cert_mem_cache_size=4MB

https端口明明是 3129 为何日志中显示是 3128?

汗,我发现我忘记修改浏览器的网络设置,原来浏览器的网络设置了 通过 192.168.6.200:8123 代理,这样浏览器是明确设置了代理,而现在squid是配置了透明代理(443),这就导致了两者冲突。

同样,在Linux终端中设置环境变量 http_proxy=http://192.168.6.200:3128/ 也会导致直接访问 3128 端口也会有同样的日志报错。原理相同

类似 Re: “intercepted port does not match 443” 一定要确保透明代理时客户端不能配置代理设置,否则冲突

另一种证书配置

解决参考: Using Squid to Proxy SSL Sites

重新生成证书(需要合并CA证书):

openssl req -new -newkey rsa:2048 -sha256 -days 3650 -nodes -x509 -extensions v3_ca -keyout squid-ca-key.pem -out squid-ca-cert.pem

# 结合文件
cat squid-ca-cert.pem squid-ca-key.pem >> squid-ca-cert-key.pem
  • 移动到证书目录:

    sudo mkdir /etc/squid/certs
    sudo mv squid-ca-cert-key.pem /etc/squid/certs/
    sudo chown proxy:proxy -R /etc/squid/certs
    
  • 配置 /etc/squid/squid.conf

    http_port 3128 intercept
    https_port 3129 intercept ssl-bump cert=/etc/squid/certs/squid-ca-cert-key.pem \
      generate-host-certificates=on dynamic_cert_mem_cache_size=16MBB
    
    sslcrtd_program /usr/lib/squid/security_file_certgen -s /var/lib/ssl_db -M 16MB
    
    acl step1 at_step SslBump1
    ssl_bump peek step1
    ssl_bump bump all
    ssl_bump splice all
    

此时启动报错:

Mar 01 17:11:55 zcloud squid[499390]: FATAL: mimeLoadIcon: cannot parse internal URL: http://zcloud:0/squid-internal-static/icons/silk/image.png

这个报错是因为使用了 transparent 模式之后,无法处理下载文件,所以还要在添加一行不带 interrupt 的配置:

http_port 8080

但是即使能够运行squid,还是会出现日志中有:

2022/03/01 17:43:00 kid1| ERROR: failure while accepting a TLS connection on conn1486 local=203.119.129.34:443 remote=192.168.6.22:63105 FD 24 flags=33: 0x563fb47ef180*1
 current master transaction: master57

这说明证书方法配置可以,但是还是需要解决 squid 向远端发送请求问题

完整参考配置

以下是我经过实践和不断摸索,初步形成的能够工作的 squid 透明代理 配置:

squid透明代理 squid.conf
acl localnet src 0.0.0.1-0.255.255.255  # RFC 1122 "this" network (LAN)
acl localnet src 10.0.0.0/8             # RFC 1918 local private network (LAN)
acl localnet src 100.64.0.0/10          # RFC 6598 shared address space (CGN)
acl localnet src 169.254.0.0/16         # RFC 3927 link-local (directly plugged) machines
acl localnet src 172.16.0.0/12          # RFC 1918 local private network (LAN)
acl localnet src 192.168.0.0/16         # RFC 1918 local private network (LAN)
acl localnet src fc00::/7               # RFC 4193 local private network range
acl localnet src fe80::/10              # RFC 4291 link-local (directly plugged) machines

acl SSL_ports port 443
acl Safe_ports port 80          # http
acl Safe_ports port 21          # ftp
acl Safe_ports port 22          # ssh
acl Safe_ports port 443         # https
acl Safe_ports port 70          # gopher
acl Safe_ports port 210         # wais
acl Safe_ports port 1025-65535  # unregistered ports
acl Safe_ports port 280         # http-mgmt
acl Safe_ports port 488         # gss-http
acl Safe_ports port 591         # filemaker
acl Safe_ports port 777         # multiling http
acl CONNECT method CONNECT

http_upgrade_request_protocols WebSocket allow all

http_access deny !Safe_ports

http_access deny CONNECT !SSL_ports

http_access allow localhost manager
http_access deny manager

http_access allow localnet
http_access allow localhost

http_access deny all

http_port 8080
http_port 3128 intercept
https_port 3129 intercept ssl-bump cert=/etc/squid/certs/myCA.pem \
  generate-host-certificates=on dynamic_cert_mem_cache_size=4MB
  #tls-dh=/etc/squid/certs/dhparam.pem
  #options=SINGLE_DH_USE,SINGLE_ECDH_USE tls-dh=/etc/squid/certs/dhparam.pem

sslproxy_cipher HIGH:MEDIUM:!RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS

sslcrtd_program /usr/lib/squid/security_file_certgen -s /var/lib/ssl_db -M 4MB

acl step1 at_step SslBump1
ssl_bump peek step1
ssl_bump bump all
ssl_bump splice all

#sslproxy_cert_error allow all
#sslproxy_flags DONT_VERIFY_PEER
#tls_outgoing_options options=NO_SSLv3,SINGLE_DH_USE,SINGLE_ECDH_USE

cache_dir ufs /var/spool/squid 100 16 256

coredump_dir /var/spool/squid

refresh_pattern ^ftp:           1440    20%     10080
refresh_pattern ^gopher:        1440    0%      1440
refresh_pattern -i (/cgi-bin/|\?) 0     0%      0
refresh_pattern .               0       20%     4320

客户端配置

实际上客户端此时如果没有导入服务器证书,浏览器或者下载工具都会拒绝 https 连接,因为都发现了中间人攻击。

此时可以在浏览器中点击 Advanced ,然后接受安全风险并继续访问

  • 当使用firefox访问 https://baidu.com ,则只要接受 squid 自签名证书就可以继续访问,只是浏览器上会多一个感叹号

  • 但是访问 https://www.google.com 则不行,因为google的安全策略(HSTS)不允许证书例外:

    www.google.com has a security policy called HTTP Strict Transport Security (HSTS), which means that Firefox can only connect to it securely. You can’t add an exception to visit this site.
    

要彻底解决这个问题,还是需要将前面生成的服务器证书 /etc/squid/certs/myCA.der 导入到浏览器中,之后访问所有的https网站都能正常工作了

备注

也是就是说,虽然透明代理可以减少客户端的代理配置,但是实际上并不是很容易使用,必须在浏览器和操作系统中导入这个中间人(代理服务器)证书才能正常工作。这种情况,也就只有技术人员自己使用,或者企业对证书有完全控制,能够在企业电脑或移动设备中安装企业证书才能解决。

这个实践我虽然完成,但是并没有达到预期的简便易用目标。这也是现代HTTPS加密安全性带来的限制,要想绕开安全加密,实际上并不是非常容易(安全性有保障)。

参考