事情是这样的:客户手里有一台海外服务器,机器本身只有一条默认路由,但他们的业务需要两种不同的出网方式一部分系统更新和依赖安装要走本地的正常线路,另一部分第三方 API 调用要固定从另一个出口走。客户不希望为这个需求再单独买一台机器,于是让我在这台机器上用 Squid 搭一个正向代理,靠 ACL 做域名级分流。
听起来简单,实际动手之后我踩了三个坑:ACL 的匹配顺序和我以为的相反、DNS 解析没跟着代理走、还有一台机器上两个 Squid 实例抢同一个端口。这篇把完整配置和排查过程写下来,如果你也在做代理分流,可以直接拿去做对照。
如果你需要在服务器上部署一套自己的出口,或者只是想先找一台网络干净的机器把环境跑起来,热陈互联(原生IP代理与海外节点) 这类提供原生 IP 的商家值得先看看报价和节点说明;如果你需要的是稳定的静态住宅出口,peakpassvpn(全球代理网络与静态住宅IP) 也提供带用量管理的专属出口方案。
一、先搞清楚方向:正向代理和反向代理不是一回事
这是最容易搞混的地方。Nginx 那种是反向代理:客户端不用知道后端在哪,代理服务器主动去拉后端资源。而 Squid 当正向代理用时,是客户端主动来找它:客户端配置 http_proxy 之后,所有 HTTP 请求先发给 Squid,Squid 再按规则决定从哪个出口发出去。
所以 Squid 正向代理的配置文件核心就三块:http_port 是它自己监听的端口,acl 定义规则,http_access 把 ACL 和放行动作串起来。理解了这个结构,剩下的都是细节。

二、最小可用的 Squid 配置
先装包:Debian/Ubuntu 系是 apt install squid,CentOS 系走 yum install squid。装完主配置在 /etc/squid/squid.conf。先用一份极简配置跑通,别一上来就往里塞几十条规则。
第一处:http_port 与监听地址。默认配置是 http_port 3128,但 http_port 后面可以跟多个端口,还能指定只监听回环:
http_port 127.0.0.1:3128
http_port 3129
第二个实例要用另一个端口,这一点后面还会用到。另外 Squid 3.2 之后默认会强制检查各个 http_port 对应的 vhost 是否唯一,如果两个实例都监听 0.0.0.0 同一个端口,第二个会直接起不来,日志里报 FATAL: Bungled ... http_port。
第二处:先写 ACL,再写 access 规则。ACL 的匹配规则是自上而下、第一条命中就停,这一点跟很多人直觉相反。我第一次写成了这样:
acl all_domains dstdomain .example.com
acl api_domains dstdomain .api-partner.com
http_access allow api_domains
http_access allow all_domains
http_access deny all
看起来通吃百搭,但 dstdomain 里的 . 前缀含义是“该域名及其所有子域”,两条规则其实高度重叠。更要命的是我最后又加了一条 http_access allow localnet,这条排在最后,导致前面所有判断在命中时被它提前截住。所以顺序必须是:先 deny 掉不该走的,再 allow 精确的,最后才是兜底。我后来改成这样:
acl SSL_ports port 443
acl Safe_ports port 80 21 443 70 210 1025-65535
acl CONNECT method CONNECT
acl api_domains dstdomain .api-partner.com
acl localnet src 10.0.0.0/8
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access deny api_domains !api_src
http_access allow api_domains api_src
http_access allow localnet
http_access deny all
这里 http_access deny api_domains !api_src 这一行是整个分流的核心:命中 api 域名、但来源 IP 不在允许网段内的,直接拒绝。允许列表放在 deny 之后、Squid 自带的 http_access deny all 兜底之前,顺序不能动。

三、最坑的一处:DNS 没跟着代理走
配完之后 apt update 直接卡住,日志里是 Could not resolve host。原因是 DNS 解析发生在客户端本机,客户端拿到的 IP 再交给 Squid,Squid 只是按 IP 转发。如果分流规则是按域名写的(dstdomain),Squid 手上只有 IP,就只能靠 cache_peer 或者日志里记录的域名反查,容易出现“规则写了但没生效”的错觉。
解决办法有两个方向。一个是在客户端把 /etc/resolv.conf 指到本机 Squid 配合 dns_nameservers 使用,让 Squid 去做 DNS 解析;另一个是客户端直接用 socks 代理(http:// 换成 socks5h://),socks5h 里的 h 表示由代理端做域名解析,域名原样传给出口节点。
我最后用的是 socks5h,因为按域名分流的诉求下,只有 h 模式能保证域名信息完整传到出口侧。配置里加上:
dns_nameservers 1.1.1.1 8.8.8.8
acl api_src src 10.0.0.0/8
http_access allow api_domains api_src
改完 squid -k reconfigure 之后,再 curl -x socks5h://127.0.0.1:3128 -s https://ifconfig.me 验证出口 IP,才算真的切过去了。
四、客户端怎么接,以及两个实例怎么共存
命令行临时用 export http_proxy=http://127.0.0.1:3128 https_proxy=http://127.0.0.1:3128,apt 需要额外加上 Acquire::http::Proxy 和 Acquire::https::Proxy 两项配置。Docker 则要写进 daemon 的 systemd 单元里,否则重启后配置丢失这一点我写过一篇专门讲 Docker 拉镜像超时的排查,和本文是同一类问题。
如果需要两套完全独立的规则集,别试图在一个 Squid 里堆几百行条件,直接起第二个实例,给它单独的配置文件和端口:
squid -f /etc/squid/squid-api.conf -z
squid -f /etc/squid/squid-api.conf
第二个实例启动前要记得 squid -k parse -f 配置文件 单独校验一遍语法,配置文件写错了会直接吞掉启动信息,日志里只有一行 Uncaught backtrace。

五、别忘了把这几条一起做掉
一是别用默认端口对外。3128 是公开的扫描目标,如果确实要让内网其他机器用,走 http_port 192.168.1.0/24:3128 限定来源网段,而不是 0.0.0.0。
二是客户端认证。用 auth_param 配 basic 认证并对密码做 MD5,配置文件里存的是哈希而不是明文。如果只在内网用,也至少把 http_access allow localnet 的网段写死,别图省事写 http_access allow all。
三是看日志确认分流真的生效。access.log 里记的是客户端 IP、命中的是哪个 ACL(比如 api_domains)和目标 URL。把 log_access 打开看真实流量,比反复 curl 猜测靠谱得多。这台机器上我最后是靠日志确认 apt 的流量确实没走 api 那条规则,才把最后一个疑问消掉的。
四是给 Squid 本身留资源。开了缓存的话 cache_dir 的磁盘要够,maximum_object_size 太小会让大文件反复回源;不开缓存就把 cache deny all 写上,纯转发场景更省事。

回头看这半小时的折腾,真正花时间的就两件事:ACL 的匹配顺序是自上而下第一条命中即停,DNS 解析必须由代理侧完成才能做域名级分流。剩下的都是配置细节,抄一遍基本都能跑通。Squid 这类工具的价值就在于规则可读、可审计,出问题查 access.log 就能定位,比很多黑盒方案踏实得多。


喜欢
顶
难过
囧
围观
无聊





