Showing Posts From
网络
拆解 SOCKS5:用 Go 实现 CONNECT、BIND 与 UDP ASSOCIATE
这是 Relay Observatory 代理项目实现系列的第二篇。完整代码放在 GitHub。 上一篇实现了 Go HTTP 正向代理。SOCKS5 与 HTTP 代理不同:它不解析 HTTP 文本,而是在 TCP 连接上交换紧凑的二进制报文,因此可以代理 SSH、数据库协议和其他 TCP 应用。 SOCKS5 是一段状态机 一条 SOCKS5 连接依次经过: TCP Accept ↓ 认证方式协商 ↓ 用户名密码认证 ↓ 读取命令与目标地址 ↓ CONNECT / BIND / UDP ASSOCIATE ↓ 数据转发与流量统计每一步都依赖前一步完成,不能把它当成普通 HTTP Handler。 第一步:协商认证方式 客户端首先发送: +-----+----------+----------+ | VER | NMETHODS | METHODS | +-----+----------+----------+ | 1 | 1 | 1~255 | +-----+----------+----------+例如: 05 01 02含义是:05:SOCKS5; 01:提供一种认证方式; 02:用户名密码认证。读取固定长度协议字段时,应使用 io.ReadFull: header := make([]byte, 2) if _, err := io.ReadFull(client, header); err != nil { return err }methods := make([]byte, int(header[1])) if _, err := io.ReadFull(client, methods); err != nil { return err }不能假设一次 Read 就会拿到完整报文。TCP 是字节流,一次写入可能被拆成多次读取,也可能与后续数据一起到达。 服务端选择 0x02: 05 02如果客户端没有提供支持的方法,则返回: 05 FFFF 表示“所有认证方式都不接受”。 RFC 1929 用户名密码认证 用户名密码认证不是 RFC 1928 主协议的一部分,而是 RFC 1929 扩展: +-----+------+----------+------+----------+ | VER | ULEN | UNAME | PLEN | PASSWD | +-----+------+----------+------+----------+ | 1 | 1 | 1~255 | 1 | 1~255 | +-----+------+----------+------+----------+这里的版本是 01,不是 SOCKS 版本 05。 认证成功返回: 01 00失败返回: 01 01生产实现不应保存明文密码。项目把用户存入 SQLite,并使用 bcrypt: identity, ok := authenticator.Authenticate( username, string(passwordBytes), )需要注意:RFC 1929 只定义了密码格式,没有提供传输加密。客户端到 SOCKS5 服务端之间如果是不可信公网,密码仍可能被窃听,应再套 VPN 或 TLS。 第二步:解析通用请求 认证成功后,三种命令共享同一报文结构: +-----+-----+-----+------+----------+----------+ | VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | +-----+-----+-----+------+----------+----------+ | 1 | 1 | 1 | 1 | Variable | 2 | +-----+-----+-----+------+----------+----------+CMD:值 命令01 CONNECT02 BIND03 UDP ASSOCIATEATYP:值 地址 长度01 IPv4 4 字节03 域名 1 字节长度 + 域名04 IPv6 16 字节端口固定为两字节大端序: port := binary.BigEndian.Uint16(portBytes)把地址解析抽成 endpoint 后,TCP 请求和 UDP 报文可以复用同一套编码器: type endpoint struct { Host string Port uint16 }func (e endpoint) Address() string { return net.JoinHostPort(e.Host, strconv.Itoa(int(e.Port))) }net.JoinHostPort 很重要,它会正确处理 IPv6 的方括号: 2001:db8::1 + 443 => [2001:db8::1]:443CONNECT:代理主动连接目标 CONNECT 与 HTTP CONNECT 的隧道阶段类似: upstream, err := dialer.DialContext( context.Background(), "tcp", target.Address(), )连接成功后回复: +-----+-----+-----+------+----------+----------+ | VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | +-----+-----+-----+------+----------+----------+其中 REP=00 表示成功,BND.ADDR/BND.PORT 是代理用于这条上游连接的本地地址。 之后启动两个 io.Copy: client --------上传--------> upstream client <-------下载--------- upstreamCONNECT 适用于绝大多数 TCP 场景,也是 curl 的 socks5h:// 主要使用的命令。 BIND:为什么必须返回两次 BIND 的连接方向相反。代理不主动连接目标,而是创建临时监听端口: 客户端 ----控制连接----> SOCKS5 代理 目标端 ----反向连接----> 代理临时端口因此协议需要两次响应。 第一次响应 代理调用: listener, err := net.ListenTCP( "tcp", &net.TCPAddr{IP: localIP, Port: 0}, )端口 0 表示让操作系统分配空闲端口。第一次响应把这个监听地址告诉客户端: REP=00, BND.ADDR=代理地址, BND.PORT=临时端口等待并校验目标 客户端在请求中提供的 DST.ADDR/DST.PORT 表示预期连接者。代理不能接受第一个随便连入的连接,否则临时端口可能被第三方抢占。 校验逻辑包括:请求端口非零时必须匹配来源端口; 请求 IP 非 0.0.0.0/:: 时必须匹配来源 IP; 请求是域名时解析全部 IP 后比较; 不匹配的连接关闭并继续等待; 超过 30 秒返回超时。第二次响应 目标通过校验后,代理再次返回成功,这次 BND.ADDR/BND.PORT 是目标的真实来源地址。 客户端只有收到第二次响应后,才能开始把 BIND 连接当成普通 TCP 隧道使用。 UDP ASSOCIATE:TCP 控制,UDP 传数据 UDP ASSOCIATE 最容易误解。它不是把 UDP 塞进 TCP,而是: TCP:客户端 -------- 会话生命周期 -------- SOCKS5 代理UDP:客户端 ---- SOCKS5 UDP 报文 ----> 代理 ---- 原始 UDP ----> 目标 UDP:客户端 <--- SOCKS5 UDP 报文 ----- 代理 <--- 原始 UDP ----- 目标客户端发送 UDP ASSOCIATE 后,代理创建 UDP socket,并通过 TCP 回复它的地址。 TCP 控制连接必须保持打开。一旦 TCP 断开,代理立即关闭对应 UDP socket: go func() { _, _ = io.Copy(io.Discard, controlConnection) _ = relaySocket.Close() }()这个 TCP 连接没有业务数据,只承担“租约”作用。 SOCKS5 UDP 报文格式 客户端不能把原始 UDP payload 直接发给代理,因为代理不知道目标地址。每个数据报都要带 Header: +------+------+------+----------+----------+----------+ | RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | +------+------+------+----------+----------+----------+ | 2 | 1 | 1 | Variable | 2 | Variable | +------+------+------+----------+----------+----------+例如把 hello udp 发到 127.0.0.1:53: 00 00 | 00 | 01 | 7F 00 00 01 | 00 35 | hello udp RSV FRAG ATYP IP PORT DATA代理解析 Header,把 DATA 作为普通 UDP 发给目标。目标响应后,代理使用目标来源地址重新封装,再发回客户端。 为什么拒绝 FRAG != 0 FRAG 用于 UDP 分片。完整实现需要:按客户端与目标维护分片队列; 识别分片序号和结束位; 设置重组超时; 限制总大小与并发队列; 防止内存耗尽攻击。大多数 SOCKS5 客户端不会使用该能力。为了避免“看似支持、实际不安全”,实现中明确丢弃 FRAG != 0 的数据报。 这是协议实现中很重要的原则:不完整的复杂特性应该明确拒绝,而不是静默误处理。 UDP 中继的来源约束 公开 UDP 中继很容易变成反射攻击工具,因此至少要做三层限制:UDP 来源 IP 必须等于已认证 TCP 客户端 IP; 第一个合法 UDP 包锁定客户端真实端口; 目标响应必须来自客户端主动联系过的地址。contactedTargets[targetAddress.String()] = struct{}{}if _, allowed := contactedTargets[source.String()]; !allowed { continue }请求中的客户端地址经常是 0.0.0.0:0,这是因为客户端在发送第一个 UDP 包前可能还不知道 NAT 映射端口。因此“首次合法报文锁定”比盲目信任请求字段更实用。 三种命令的流量定义 项目采用:命令 上传 下载CONNECT 客户端到目标的 TCP 字节 目标到客户端的 TCP 字节BIND 客户端到反向连接者的字节 反向连接者到客户端的字节UDP ASSOCIATE 去除 SOCKS Header 后的 UDP payload 目标返回的 UDP payloadUDP 不统计 SOCKS5 封装头,便于衡量真正的业务数据。 测试不能只测 parser 协议 parser 单元测试只能证明字节解析正确。完整测试还应创建真实 socket: 测试客户端 -> SOCKS5 服务 -> 本地 TCP/UDP Echo Server覆盖:正确和错误密码; IPv4、IPv6、域名编码; CONNECT 双向数据; BIND 两次响应; UDP ASSOCIATE 双向回显; FRAG != 0 拒绝; 上传、下载和请求次数。再配合 go test -race,检查会话关闭、资源跟踪和统计写入是否存在数据竞争。 小结 SOCKS5 的难点不在 io.Copy,而在状态机和边界: 握手 -> 认证 -> 通用请求 -> 命令分派 -> 生命周期 -> 统计CONNECT 是主动拨号,BIND 是两阶段反向接入,UDP ASSOCIATE 则是 TCP 控制下的 UDP 中继。把地址编解码、命令处理和资源关闭拆开后,协议实现才会既可读又可测试。 下一篇进入数据层:用 SQLite 持久化代理用户、分协议流量与小时趋势。
从 TCP 到 HTTP:用 Go 实现带鉴权与流量统计的正向代理
这是 Relay Observatory 代理项目实现系列的第一篇。完整代码放在 GitHub。 很多 HTTP 代理教程只贴一个 ServeHTTP,却没有解释为什么 HTTPS 要使用 CONNECT、Hijack 返回的连接和缓冲区分别是什么,以及流量到底应该在哪一层统计。本文从连接模型开始,把这些问题串起来。 先建立正确的连接模型 正向代理同时持有两侧连接: 客户端 <------ client connection ------> 代理 代理 <------ upstream connection ----> 目标服务器对普通 HTTP,代理能够解析请求和响应: 客户端 -- HTTP Request --> 代理 -- HTTP Request --> 目标 客户端 <- HTTP Response -- 代理 <- HTTP Response -- 目标对 HTTPS,HTTP 内容在 TLS 内部已经加密。代理不能先读取 GET /,因为客户端在发送 HTTP 请求前必须与目标服务器完成 TLS 握手。 解决办法是客户端先向代理发送明文的 CONNECT: CONNECT example.com:443 HTTP/1.1 Proxy-Authorization: Basic ...代理连接 example.com:443,返回: HTTP/1.1 200 Connection Established此后代理只搬运 TLS 二进制数据,不解析内部 HTTP。 入口只做三件事 入口 Handler 最容易写乱。一个清楚的入口应该只负责:鉴权; 判断普通 HTTP 还是 CONNECT; 将流量统一入账。func (p *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { identity, ok := auth.AuthenticateBasicHeader( p.users, r.Header.Get("Proxy-Authorization"), ) if !ok { w.Header().Set("Proxy-Authenticate", `Basic realm="proxy"`) http.Error(w, "proxy authentication required", 407) return } protocol := traffic.ProtocolHTTP var uploaded, downloaded int64 if r.Method == http.MethodConnect { protocol = traffic.ProtocolHTTPS uploaded, downloaded = p.forwardTunnel(w, r) } else { uploaded, downloaded = p.forwardHTTP(w, r, identity.Username) } _ = p.traffic.Record(identity.ID, protocol, uploaded, downloaded) }http.MethodConnect 只是字符串常量 "CONNECT"。这里不是在判断网站是否以 https:// 开头,而是在判断客户端是否要求建立 TCP 隧道。 普通 HTTP:RoundTripper 完成一次往返 Go 的 http.RoundTripper 是一次 HTTP 请求的底层接口: type RoundTripper interface { RoundTrip(*http.Request) (*http.Response, error) }它完成: 代理 -- Request --> 目标 代理 <- Response -- 目标代理使用 RoundTripper 而不是高级的 http.Client,因为代理不应该替用户自动跟随重定向、保存 Cookie 或改变请求语义。 转发前需要整理请求: func prepareUpstreamRequest(r *http.Request) { if r.URL.Scheme == "" { r.URL.Scheme = "http" } if r.URL.Host == "" { r.URL.Host = r.Host } // 客户端请求使用 RequestURI;RoundTripper 要求它为空。 r.RequestURI = "" // 代理密码绝不能继续发给目标网站。 r.Header.Del("Proxy-Authorization") removeHopByHopHeaders(r.Header) }然后发送请求并把响应流式复制回客户端: response, err := transport.RoundTrip(r) if err != nil { http.Error(w, "upstream request failed", http.StatusBadGateway) return } defer response.Body.Close()copyHeaders(w.Header(), response.Header) w.WriteHeader(response.StatusCode)downloaded, err := io.Copy(w, response.Body)io.Copy 不会把整个文件读进内存。即使目标返回几 GB 文件,也只需要一个固定大小的缓冲区: 目标返回一块 -> 代理读取一块 -> 立即写给客户端Hop-by-Hop Header 为什么不能转发 Connection、Keep-Alive、Proxy-Authorization、Transfer-Encoding 等 Header 只描述当前一跳连接。 例如客户端与代理希望保持长连接,不代表代理与目标服务器必须使用同样的连接策略。因此这些 Header 必须在进入下一跳前删除。 还要处理 Connection 中动态声明的字段: for _, value := range header.Values("Connection") { for name := range strings.SplitSeq(value, ",") { header.Del(strings.TrimSpace(name)) } }只删除固定列表而忽略 Connection: Foo,会把本应仅对当前连接有效的 Foo 转发出去。 CONNECT:先连接目标,再接管客户端连接 第一步是代理主动连接目标: upstream, err := dialer.DialContext( r.Context(), "tcp", r.Host, )如果请求是: CONNECT example.com:443 HTTP/1.1那么 r.Host 是 example.com:443。DialContext 会完成 DNS 查询与 TCP 三次握手,并返回代理到目标服务器的 upstream 连接。 第二步是从 net/http 手中接管客户端连接: hijacker, ok := w.(http.Hijacker) if !ok { http.Error(w, "hijacking is not supported", 500) return }client, buffered, err := hijacker.Hijack()这里有三个容易混淆的对象:对象 含义client 客户端到代理的底层 TCP 连接upstream 代理到目标服务器的 TCP 连接buffered client 前面的缓冲读取器,不是第三条连接net/http 可能提前从客户端读取了一部分数据放进缓冲区。上传方向必须先读 buffered,否则会漏掉已经离开内核 socket、但还没交给业务代码的数据。 两个 io.Copy 组成全双工隧道 上传和下载必须同时进行: go func() { uploaded, _ := io.Copy(upstream, buffered) results <- uploaded }()go func() { downloaded, _ := io.Copy(client, upstream) results <- downloaded }()如果按顺序执行: io.Copy(upstream, client) io.Copy(client, upstream)第一行通常要等客户端关闭连接才返回,第二行永远没有机会及时执行,下载方向就被阻塞了。 当一个方向结束时,不应该立刻关闭整条 TCP 连接,而应优先使用半关闭: if tcp, ok := connection.(*net.TCPConn); ok { _ = tcp.CloseWrite() }CloseWrite 发送 FIN,表示“我不会再发送”,但仍允许把另一个方向尚未完成的数据读完。 流量统计应该统计哪一层 普通 HTTP 可以统计请求体和响应体: uploaded = request body bytes downloaded = response body bytes请求体通过装饰器计数: type countingReadCloser struct { io.ReadCloser bytes int64 }func (r *countingReadCloser) Read(p []byte) (int, error) { n, err := r.ReadCloser.Read(p) r.bytes += int64(n) return n, err }HTTPS CONNECT 无法看到内部请求体,因此统计的是 TLS 隧道原始字节。它会包含:TLS 握手; 证书; TLS record 开销; 加密后的 HTTP Header 和 Body。所以 curl %{size_download} 与代理下载量不会完全相等。前者通常只统计响应 Body,后者统计整条加密隧道。出现约千分之几的差异是正常现象。 用接口隔离认证和统计 代理层不应该知道密码存储在 .env、SQLite 还是远程服务: type Authenticator interface { Authenticate(username, password string) (Identity, bool) }type Recorder interface { Record( userID int64, protocol string, uploaded int64, downloaded int64, ) error }这样网络层只负责协议,存储层可以从内存平滑替换为 SQLite,测试也能使用轻量内存实现。 小结 Go HTTP 正向代理的核心不是某个库,而是两套不同的数据路径: 普通 HTTP: Request -> RoundTripper -> Response -> io.CopyHTTPS: CONNECT -> Dial -> Hijack -> 双向 io.Copy一旦分清 client、upstream、buffered 和两个方向的数据流,鉴权、流量统计、持久化都只是围绕主路径增加的边界能力。 下一篇继续实现 SOCKS5 的 CONNECT、BIND 与 UDP ASSOCIATE。
EasyTier 能 ping 通但 HTTP 不通:排查思路和解决方法
EasyTier 组网后能 ping 通对方,但 HTTP 访问失败——这是隧道已建立、应用层流量被拦截的典型现象。 快速定位(3 条命令) ① 客户端测试 TCP 连通性 curl -v http://对方EasyTierIP:端口Connection refused:网络通,服务没监听 Connection timed out:防火墙或路由拦截 返回 HTTP 内容:服务正常,排查客户端代理② 服务端抓包 sudo tcpdump -i tun0 tcp port 80同时客户端访问一次:抓包结果 结论看到 SYN 流量到了服务器,查服务监听或防火墙看不到 SYN 路由或 EasyTier 转发问题SYN 无 SYN-ACK 防火墙或服务未监听③ 查看监听端口 ss -lntp | grep -E ':80|:443|:8080'原因一:服务只监听 127.0.0.1(最常见) ss -lntp 看到 127.0.0.1:80,EasyTier 的 tun0 流量就无法到达服务。 修改方法: # uvicorn uvicorn main:app --host 0.0.0.0 --port 8000# nginx listen 0.0.0.0:80;# Node.js app.listen(80, '0.0.0.0')原因二:防火墙拦截 tun0 ping 用 ICMP,HTTP 用 TCP,如果防火墙只放行了 ICMP 或者对 EasyTier 网卡有限制: # 查看规则 sudo iptables -L -n sudo ufw status# 临时放行 tun0(测试用) sudo iptables -I INPUT -i tun0 -j ACCEPT sudo iptables -I FORWARD -i tun0 -j ACCEPT永久规则(以 ufw 为例): sudo ufw allow in on tun0原因三:Docker 端口绑定到 localhost docker ps如果看到 127.0.0.1:80->80/tcp,改成: docker run -p 80:80 ...原因四:服务绑定到特定网卡 IP 服务器有多个网卡(公网 IP + EasyTier IP),应用可能只绑到了公网 IP: ss -lntp | grep 8080如果看到 103.x.x.x:8080,需要改为 0.0.0.0:8080。 原因五:代理劫持 HTTP 流量 系统装了 Clash、sing-box 等,可能把内网 IP 的 HTTP 也走了代理: curl --noproxy '*' http://10.x.x.x如果这样能通,就是代理规则问题,把 EasyTier 网段加入 bypass 列表。 原因六:MTU 问题 现象:ping 小包正常,HTTP 建连后卡住或大文件下载失败。 ping -M do -s 1472 目标IP失败则把 EasyTier 网卡 MTU 调小: sudo ip link set tun0 mtu 1280EasyTier 配置自定义 IP 段 默认 dhcp = true 由网络中的 DHCP 节点分配 IP。如果要固定 IP: dhcp = false ipv4 = "10.88.0.2/24"如果加入的是他人的 EasyTier 网络,单方面改客户端配置通常不生效,需要整个网络统一规划地址段。 诊断工具速查 # 确认 EasyTier 接口和 IP ip addr show tun0# 确认路由(流量是否走 tun0) ip route | grep tun0# TCP 连通性测试 nc -vz 目标IP 80# 本机自测(服务端执行) curl -v http://127.0.0.1:80 curl -v http://tun0_IP:80
EasyTier 能 ping 通但 HTTP 访问不了:六种原因排查
现象 EasyTier 组网后:ping 10.126.126.x ✅ 成功 curl http://10.126.126.x ❌ 失败说明 EasyTier 隧道本身正常,问题在 TCP/应用层。 排查顺序 1. Web 服务只监听 127.0.0.1(最常见) ss -tlnp如果看到: 127.0.0.1:8080而不是: 0.0.0.0:8080说明服务拒绝来自 EasyTier 地址的连接。修复方式:服务 正确配置Nginx listen 0.0.0.0:80;Node.js app.listen(8080, '0.0.0.0')FastAPI/uvicorn uvicorn main:app --host 0.0.0.0Python http.server python -m http.server --bind 0.0.0.02. iptables 只放行了 ICMP,没放行 TCP sudo iptables -L -n sudo ufw status sudo firewall-cmd --list-all # CentOS临时放行 EasyTier 网卡: sudo iptables -I INPUT -i tun0 -j ACCEPT sudo iptables -I FORWARD -i tun0 -j ACCEPT如果 HTTP 立刻恢复,说明是防火墙问题,再添加永久规则。3. Docker 端口映射绑定到 127.0.0.1 docker ps如果端口映射是: 127.0.0.1:8080->8080/tcpEasyTier 无法访问。需要改为: docker run -p 8080:8080 ... # 正确 # 不要用 docker run -p 127.0.0.1:8080:8080 ...4. HTTP 请求被系统代理拦截 如果本机装了 Clash/V2Ray/sing-box: curl --noproxy '*' http://10.126.126.x如果加了 --noproxy 后能访问,说明代理规则把 EasyTier 内网地址错误地转发了出去。在代理工具里添加 10.0.0.0/8 为直连规则。5. MTU 问题 现象:ping 小包正常,HTTP 建连后卡住,大文件下载失败。 测试: ping -M do -s 1472 目标IP如果失败,降低 MTU: sudo ip link set tun0 mtu 12806. 路由未覆盖目标网段 ip route | grep tun0确认有类似: 10.126.126.0/24 dev tun0 proto kernel scope link src 10.126.126.1如果没有,HTTP 的 TCP 包会走默认网关出公网。 tcpdump 快速定位(推荐) 在服务端抓包: sudo tcpdump -i tun0 tcp port 80客户端发起请求后看结果:抓包结果 结论看到 SYN 包 流量到达服务端,问题是防火墙或服务监听地址看不到任何包 EasyTier 路由问题,或客户端代理拦截SYN 但没有 SYN-ACK 防火墙拦截(iptables DROP)SYN 后立即 RST 服务没在该端口监听配合 curl -v http://目标IP 看是 Connection refused 还是 Timeout,可以快速缩小问题范围。
Citrix Gateway 三种模式:Full VPN / Clientless / ICA Proxy 怎么区分
在公司里用 Citrix Gateway(以前叫 NetScaler Gateway)远程办公,能不能 ping 内网、能不能 mstsc 到某台内网机器——完全取决于管理员开的是哪种模式。三种模式差别很大: 三种模式 1. Full VPN(完整 VPN) 登录 Gateway 后建立 VPN 隧道,你的电脑相当于接进了公司内网。 能做的: ping 10.0.0.1 mstsc 10.0.0.100 \\10.0.0.50\share内网 API、SMB 共享、RDP 都能直接连。最接近传统 VPN。 2. Clientless VPN(无客户端 VPN) 只能通过 Gateway 的网页入口访问管理员发布过的内网网站: https://gateway.company.com/vpn/index.html ├─ OA 系统 ├─ JIRA ├─ GitLab └─ Exchange OWAGateway 帮你反向代理到内网。只有网页可访问——ping / RDP / SMB 全都不行。 3. ICA Proxy(最常见) 登录 Gateway 后看到的不是网页应用列表,而是远程桌面 / 应用: [Windows 桌面] [SAP] [Outlook] [Chrome]点击后:浏览器下载 .ica 文件 Citrix Workspace 客户端打开 连到内网一台服务器上运行你本机没进内网——只是远程操作一台内网机器。所有操作都在那台远端上完成,本机能看到的只是画面像素。 怎么判断自己是哪种 登录 Gateway 之后: 方法 1:看网卡 ipconfigFull VPN 会多出: 以太网适配器 Citrix Secure Access: 以太网适配器 Citrix VPN Adapter:看到就是 Full VPN。 方法 2:看路由 route printFull VPN 里有内网网段被路由到 Citrix 虚拟网卡: 10.0.0.0 255.0.0.0 10.0.0.1 <Citrix 网卡 IP>方法 3:直接 ping 内网 ping 10.x.x.x telnet 10.x.x.x 3389通 = Full VPN,不通 = Clientless 或 ICA Proxy。 方法 4:登录后看到什么 登录 Gateway 网页后:看到 大概率是一堆网页链接 Clientless VPN桌面 / 应用图标(点了下 ica) ICA Proxy什么都没看到,只有 VPN 状态 Full VPNICA Proxy 想访问其它内网资源 基本上不行。ICA Proxy 的设计初衷就是"给远程用户桌面 / 应用",不给完整内网访问权限。安全模型里这是"最小暴露面"的选择。 想访问需要管理员:开启 Full VPN 策略(Citrix Secure Access Client) 或者把你需要的服务发布成 Clientless 应用 或者给你专门开一个 RDP 会话,你从那台机器上操作传大文件的坑 ICA Proxy 模式下想把本机文件传到远端会话:Citrix Workspace 支持"客户端驱动映射",可以把本地磁盘挂进远端会话,管理员可能禁用 剪贴板:可能双向、可能单向、可能禁用,也是策略控制的 上传下载按钮:Citrix Workspace 本身有,但公司常常禁真被卡住只能靠在远端邮箱收发、通过 WebOA 上传中转等等。或者直接找 IT 开权限。 Full VPN 的坑 即使有 Full VPN 也不等于什么都能访问:分段路由:管理员可能只给部分内网网段路由,比如只给你 10.10.0.0/16,其它照样不通 应用层过滤:Gateway 可以按端口 / 协议限制,明明有路由但 RDP 不通 DNS:需要用公司内 DNS,否则内网域名解析不了route print 看到给了哪些网段,就只能访问那些网段。 一句话总结 Citrix Gateway 三种模式—— Full VPN 拿到内网路由、Clientless 只发布网页、ICA Proxy 只是远端桌面像素。看 ipconfig 有没有 Citrix 网卡最快分辨。想要更多权限,只能找管理员改策略。
CIDR 表示法:/24、/32 的含义和只匹配单个 IP 的写法
CIDR(无类别域间路由)用"IP地址/前缀长度"表示一段 IP 范围,前缀长度决定有多少个 IP 被包含在内。 /24 不等于单个 IP 43.255.122.56/24 表示前 24 位固定,等价于: 43.255.122.0 - 43.255.122.255共 256 个地址,包含 43.255.122.56,但也包含同网段的所有其他地址。 只匹配单个 IP:使用 /32 IPv4 地址是 32 位,/32 表示所有 32 位都固定: 43.255.122.56/32 → 仅匹配 43.255.122.56配置防火墙、ACL、路由规则、Cloudflare IP 规则时,允许或封禁单个 IP 都应使用 /32: # iptables iptables -A INPUT -s 43.255.122.56/32 -j ACCEPT# Nginx allow 43.255.122.56/32;如果不要求 CIDR 格式,直接写 IP 不加后缀也等同于 /32: 43.255.122.56常用前缀长度对照写法 匹配范围 地址数x.x.x.x/32 仅这一个 IP 1x.x.x.x/31 2 个(常用于点对点链路) 2x.x.x.x/30 4 个 4x.x.x.x/29 8 个 8x.x.x.x/28 16 个 16x.x.x.0/24 256 个(常见局域网段) 256x.x.0.0/16 65536 个 65536x.0.0.0/8 16777216 个(A 类网络) 167772160.0.0.0/0 所有 IPv4 地址 全部前缀长度与地址数的计算 地址数 = 2^(32 - 前缀长度)/32 → 2^0 = 1 /24 → 2^8 = 256 /16 → 2^16 = 65536网络地址与广播地址 在标准 IPv4 子网中,最小地址(如 43.255.122.0)是网络地址,最大地址(43.255.122.255)是广播地址,可用主机地址是中间的 254 个。但在防火墙规则和 CIDR 匹配中,这个区分通常不重要,/24 就是匹配全部 256 个地址。
CIDR 记法要写对:43.255.122.56/24 匹配的是一整段
配置防火墙、白名单、路由规则的时候,经常见到有人这样写: 43.255.122.56/24本意是想匹配单个 IP,结果匹配了 256 个。CIDR 的斜杠语法是"前面多少位固定",不是"IP 加上标签"。 /24 到底表示什么 43.255.122.56/24 意思是:IP 的前 24 位固定。IPv4 一共 32 位,24 位固定就是前三段(43.255.122)不变,最后一段(.56)被忽略、整段 0-255 都算命中。 等价于: 43.255.122.0 – 43.255.122.255 (256 个地址)所以: 43.255.122.0 ✅ 命中 43.255.122.56 ✅ 命中 43.255.122.100 ✅ 命中 43.255.122.255 ✅ 命中 43.255.123.56 ❌ 未命中想匹配单个 IP:/32 要精确匹配一个 IP,写 /32——32 位全都固定,就是它自己: 43.255.122.56/32如果配置格式不要求必须有 CIDR 后缀,直接写 IP 也行: 43.255.122.56/32 是最严格的等价形式。 常见 CIDR 对照写法 匹配范围 地址数43.255.122.56/32 只 43.255.122.56 143.255.122.56/31 43.255.122.56 – 57 243.255.122.56/30 43.255.122.56 – 59 443.255.122.56/29 43.255.122.56 – 63 843.255.122.56/28 43.255.122.48 – 63 1643.255.122.56/27 43.255.122.32 – 63 3243.255.122.56/26 43.255.122.0 – 63 6443.255.122.56/25 43.255.122.0 – 127 12843.255.122.56/24 43.255.122.0 – 255 25643.255.122.56/16 43.255.0.0 – 43.255.255.255 65,53643.255.122.56/8 43.0.0.0 – 43.255.255.255 16M+43.255.122.56/0 整个 IPv4 空间 全部注意:CIDR 里"起点 IP"随便写,实际会向下对齐。43.255.122.56/24 和 43.255.122.99/24 等价,都指 43.255.122.0/24 这个网段。 计算方法 给一个 CIDR A.B.C.D/N,怎么算范围? 掩码位数 → 主机位数 = 32 - N 主机位数决定了段长:主机位 = 0:1 个 IP(/32) 主机位 = 1:2 个(/31) 主机位 = 8:256 个(/24) 主机位 = 16:65536 个(/16) 主机位 = N:2^N 个起点:把 A.B.C.D 的后 (32-N) 位置零。 例:10.0.5.37/28N=28,主机位=4 段长 = 2^4 = 16 后 4 位置零:37 二进制 00100101 → 后 4 位置零 00100000 = 32 范围:10.0.5.32 – 10.0.5.47Linux 命令算 ipcalc 是最快的: $ ipcalc 43.255.122.56/24 Address: 43.255.122.56 Netmask: 255.255.255.0 = 24 Network: 43.255.122.0/24 HostMin: 43.255.122.1 HostMax: 43.255.122.254 Broadcast: 43.255.122.255 Hosts/Net: 254或者 Python 一行: import ipaddress net = ipaddress.ip_network("43.255.122.56/24", strict=False) print(net.network_address, net.broadcast_address, net.num_addresses)常见误区 误区 1:以为 /24 加个数字更保险不是。任何斜杠后缀都是"网段掩码",你越加越大。 误区 2:/32 单独 IP 时可以省掉大多数系统能省,但**某些严格配置格式(RBAC 白名单、AWS SG)**要求必须写 /32,别偷懒。 误区 3:以为 /0 是"无匹配"反过来——0.0.0.0/0 匹配整个 IPv4 空间,路由表里就是默认路由。 IPv6 版 IPv6 也用 CIDR: 2001:db8::1/128 单个 IP 2001:db8::/32 一整段 ::/0 整个 IPv6 空间规则一样,只是位数从 32 变成 128。 一句话总结 IP 加 /N 是网段,前 N 位固定其余任意。单个 IP 要写 /32(IPv6 是 /128)。别偷懒不加 CIDR 后缀,也别把 /24 当成"某个 IP 的编号"。
Python 通过 Shadowsocks SOCKS5 代理发送请求
核心原理 Python 不能直接解析 Shadowsocks 协议。标准做法是: Python ↓ SOCKS5 sslocal (本地 1080 端口) ↓ Shadowsocks 协议 远端 SS 服务器 ↓ 明文 目标网站先用 Shadowsocks 客户端在本地起一个 SOCKS5 代理,Python 通过这个代理访问外网。 方法一:requests[socks](推荐) 安装: pip install requests[socks]使用: import requestsproxies = { "http": "socks5://127.0.0.1:1080", "https": "socks5://127.0.0.1:1080" }r = requests.get( "https://httpbin.org/ip", proxies=proxies, timeout=10 )print(r.json())socks5:// 使用远端 DNS 解析;如果想让本地 DNS 解析,换成 socks5h://。 方法二:PySocks 全局劫持 安装: pip install pysocks设置全局默认代理,之后所有 socket 连接都走 SOCKS5: import socket import sockssocks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 1080) socket.socket = socks.socksocketimport requestsr = requests.get("https://httpbin.org/ip") print(r.json())适合需要代理所有网络调用(requests、httpx、urllib 等)的场景,但会影响进程内所有 socket,慎用。 方法三:环境变量 export ALL_PROXY=socks5://127.0.0.1:1080 export HTTPS_PROXY=socks5://127.0.0.1:1080 export HTTP_PROXY=socks5://127.0.0.1:1080或者在 Python 代码里设置: import osos.environ["ALL_PROXY"] = "socks5://127.0.0.1:1080"import requestsr = requests.get("https://httpbin.org/ip") print(r.json())requests 会自动读取 HTTP_PROXY / HTTPS_PROXY / ALL_PROXY 环境变量。 在代码里启动 sslocal 如果想让 Python 程序自己管理 Shadowsocks 子进程: import subprocess import time import requestsproc = subprocess.Popen([ "sslocal", "-s", "服务器IP", "-p", "8388", "-k", "密码", "-m", "aes-256-gcm", "-l", "1080" ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)time.sleep(1) # 等待 sslocal 就绪proxies = { "http": "socks5://127.0.0.1:1080", "https": "socks5://127.0.0.1:1080" }try: r = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10) print(r.json()) finally: proc.terminate()sslocal 来自 shadowsocks-libev 或 shadowsocks-rust 包,需要提前安装。 验证代理是否生效 import requestsproxies = { "http": "socks5://127.0.0.1:1080", "https": "socks5://127.0.0.1:1080" }r = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10) print(r.json()) # 返回的 origin IP 应为代理服务器 IP,不是本机 IP
Cloudflare 525 SSL Handshake Failed:原因排查与修复方法
Cloudflare 525 错误发生在 Cloudflare 与源站之间,不是用户与 Cloudflare 之间。含义:Cloudflare 已经接收到请求,但无法与源站完成 TLS 握手。 快速检测源站 TLS openssl s_client -connect 源站IP:443 -servername 你的域名正常握手会看到: New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384如果出现 handshake failure,问题在 Nginx 或证书配置。 原因一:Nginx 未使用 fullchain 证书(最常见) Let's Encrypt 证书必须用 fullchain.pem,不能只用叶证书: # 正确 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;检查: nginx -T | grep ssl_certificate如果配置的是 cert.pem 而非 fullchain.pem,Cloudflare 会因为证书链不完整而报 525。 openssl 验证时看到这行也是同一问题: verify error:num=20:unable to get local issuer certificate原因二:TLS 版本过低 Cloudflare 要求至少 TLS 1.2。Nginx 配置: ssl_protocols TLSv1.2 TLSv1.3;原因三:Cipher Suite 过于严格 ssl_ciphers HIGH:!aNULL:!MD5;检查源站支持的 cipher: nmap --script ssl-enum-ciphers -p 443 源站IP原因四:开启了客户端证书验证 ssl_verify_client on;Cloudflare 无法提供匹配的客户端证书,直接导致 525。 检查: nginx -T | grep verify_client原因五:443 端口未监听 telnet 源站IP 443或: curl -vk https://源站IP如果 Connection refused 说明 Nginx 没有监听 443。 原因六:IPv6 配置问题 Cloudflare 可能优先走 IPv6: dig AAAA 你的域名如果有 AAAA 记录,但 Nginx 的 IPv6 监听或防火墙有问题: openssl s_client -connect [IPv6地址]:443 -servername 你的域名测试是否成功。 Cloudflare SSL 模式 Cloudflare Dashboard → SSL/TLS → Overview 查看当前模式:模式 要求Flexible 源站无需 HTTPSFull 源站需要 HTTPS,证书可自签Full (strict) 源站需要有效证书,证书链完整使用 Full (strict) 时,源站必须配置 fullchain.pem。 完整排查步骤 # 1. 检查证书配置 nginx -T | grep ssl_certificate# 2. 测试握手(替换为实际源站 IP) openssl s_client -connect 源站IP:443 -servername 你的域名# 3. 检查 IPv6 dig AAAA 你的域名# 4. 检查客户端验证 nginx -T | grep verify_client# 5. 直接用 curl 测试 curl -Iv https://你的域名
Clash Verge Rev 规则覆写:prepend/append/delete 与 PROCESS-PATH-REGEX 详解
Clash Verge Rev 的规则覆写配置片段: prepend: - 'PROCESS-PATH-REGEX,/Applications/ToDesk.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/UURemote.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/WeChat.app.*,DIRECT' append: [] delete: []prepend / append / delete 的含义字段 作用prepend 在规则列表最前面插入,优先匹配append 在规则列表最后面追加,最后匹配delete 从现有规则中删除匹配项prepend 里的规则优先级最高,命中后直接执行,不再往下匹配。 PROCESS-PATH-REGEX 语法 PROCESS-PATH-REGEX,<正则表达式>,<策略>macOS 示例: PROCESS-PATH-REGEX,/Applications/WeChat.app.*,DIRECT匹配进程路径: /Applications/WeChat.app/Contents/MacOS/WeChatWindows 示例: PROCESS-PATH-REGEX,.*\\WeChat\\WeChat.exe,DIRECT注意 Windows 路径需要用 \\ 转义反斜杠。 为什么远程控制软件要设为 DIRECT ToDesk、TeamViewer、向日葵、AnyDesk 等远程桌面软件通过自有中转服务器建立连接,走代理后常见问题:连接黑屏 延迟显著增加 无法建立 P2P 通道 连接频繁中断把这类软件设置为 DIRECT 可以绕过代理,走最短路径连接中转服务器。 常用直连配置 prepend: # 远程桌面 - 'PROCESS-PATH-REGEX,/Applications/ToDesk.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/UURemote.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/SunloginClient.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/TeamViewer.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/AnyDesk.app.*,DIRECT' # 国内即时通讯 - 'PROCESS-PATH-REGEX,/Applications/WeChat.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/DingTalk.app.*,DIRECT'查找 macOS 进程真实路径 # 通过进程名查找 ps aux | grep ToDesk# 通过 PID 查看文件 lsof -p <PID> | head -5输出示例: /Applications/ToDesk.app/Contents/MacOS/ToDesk然后写规则: PROCESS-PATH-REGEX,/Applications/ToDesk.app.*,DIRECT让应用走代理 如果需要某个应用强制走代理(和默认行为相反),改策略组名称: prepend: - 'PROCESS-PATH-REGEX,/Applications/SomeApp.app.*,节点选择'前提是策略组 节点选择 在你的 proxy-groups 中存在。
Clash 用 PROCESS-PATH-REGEX 让特定 App 直连
日常科学上网走 Clash 系客户端,大多数应用没问题。但有些特殊 App 不适合走代理:远程控制类(ToDesk、向日葵、AnyDesk、TeamViewer):走代理会连不上或黑屏 微信 / QQ:中转导致延迟大或者协议对不上 游戏:延迟敏感、部分反作弊系统会怀疑 IP 一些内网 App:需要局域网直连Clash Meta(含 Verge Rev、ClashX Meta、OpenClash)提供 PROCESS-PATH-REGEX 规则,按进程路径匹配后强制直连。 规则语法 - 'PROCESS-PATH-REGEX,<正则>,<策略>'例: - 'PROCESS-PATH-REGEX,/Applications/WeChat.app.*,DIRECT'匹配路径包含 /Applications/WeChat.app 的进程,直连。 常用配置(macOS) prepend: # 远程控制软件 - 'PROCESS-PATH-REGEX,/Applications/ToDesk.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/UURemote.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/SunloginClient.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/TeamViewer.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/AnyDesk.app.*,DIRECT' # 通讯软件 - 'PROCESS-PATH-REGEX,/Applications/WeChat.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/QQ.app.*,DIRECT' # 开发工具(可选) - 'PROCESS-PATH-REGEX,/Applications/Docker.app.*,DIRECT'append: [] delete: []prepend 表示加到规则最前面(优先级最高,第一个匹配到就返回)。 Windows 上路径 Windows 版路径不同: - 'PROCESS-PATH-REGEX,.*\\WeChat\.exe$,DIRECT' - 'PROCESS-PATH-REGEX,.*\\ToDesk\.exe$,DIRECT' - 'PROCESS-PATH-REGEX,.*\\SunloginClient\.exe$,DIRECT'或者用 PROCESS-NAME(更省心): - 'PROCESS-NAME,WeChat.exe,DIRECT' - 'PROCESS-NAME,ToDesk.exe,DIRECT'PROCESS-NAME 只匹配文件名,不用管路径,跨平台更方便。 放哪儿 Clash Verge Rev: 配置界面 → 配置 → 脚本 / Rules 覆写。规则放在 prepend 数组里。 ClashX / ClashX Meta: ~/.config/clash/config.yaml 的 rules: 数组开头(或用 mixin/override)。 Clash for Windows(已停止更新): Parsers → 编辑 YAML 覆写规则。 想让某 App 走特定节点 改成策略组名字: - 'PROCESS-PATH-REGEX,/Applications/Chrome.app.*,🚀 节点选择'前提是策略组里有这个名字: proxy-groups: - name: 🚀 节点选择 type: select proxies: - 🇺🇸 美国 - 🇯🇵 日本 - 🇭🇰 香港查真实进程路径 不确定 App 的路径,先查一下(macOS): # 按名字找 PID pgrep -x WeChat# 用 PID 查完整路径 lsof -p <PID> | head -5# 或者一句话 ps -ax -o command | grep WeChat | grep -v grepWindows: Get-Process | Where-Object Name -like "*WeChat*" | Select-Object Name, Path规则匹配优先级 Clash 从上往下匹配,第一个命中就返回。所以 prepend 里的规则会覆盖后面的常规规则。 要精细控制某类流量的策略: rules: # 高优先级:某个 App 强制直连 - PROCESS-NAME,WeChat.exe,DIRECT # 中优先级:国内 IP 直连 - GEOIP,CN,DIRECT # 中优先级:广告拦截 - DOMAIN-SUFFIX,doubleclick.net,REJECT # 兜底:所有其它走代理 - MATCH,🚀 节点选择顺序错了效果就变了。 常见问题 1. 规则没生效Clash Meta 才支持 PROCESS-* 系列规则,老版 Clash 不支持。看客户端版本,升级到 Meta 内核。 2. macOS 上要给 Clash 权限系统偏好 → 安全性与隐私 → 隐私 → 允许 Clash 访问其他应用的进程信息。 3. 路径匹配正则要转义. 在正则里是任意字符,严格匹配要写 \.: - 'PROCESS-PATH-REGEX,/Applications/WeChat\.app.*,DIRECT'4. 微信直连了还是登不上微信是走多个域名的,有的不在国内。除了 App 直连,可能还要加域名规则: - DOMAIN-SUFFIX,weixin.qq.com,DIRECT - DOMAIN-SUFFIX,wechat.com,DIRECT一句话总结 远程控制、微信、游戏这类"不适合走代理"的应用,用 PROCESS-PATH-REGEX 或 PROCESS-NAME 走直连。规则要放在 prepend(最高优先级),只有 Clash Meta 内核才支持这类规则。
MITM 抓包代理的实现方案:从用户态代理到驱动层
六种实现层次对比方案 典型工具 特点用户态代理 mitmproxy, Charles, Burp Suite 配置简单,需安装根证书TUN 虚拟网卡 Clash Meta, sing-box, Surge 不依赖系统代理,可抓 UDP/游戏WinDivert WinDivert + 自定义程序 Windows 驱动层,游戏加速器常用WFP 企业安全/DLP 产品 微软官方,性能高,不易检测NDIS Filter Wireshark/Npcap 二层帧,可抓所有流量Socket Hook Frida, Xposed 拦截加密前明文,绕过证书绑定用户态代理(最常见) 流量路径: App → 系统代理 → MITM Proxy → 目标服务器HTTPS 需要:生成 CA 根证书,安装到系统信任 收到 CONNECT hostname:443 后,动态签发 hostname 的域名证书 与客户端建立一条 TLS,与服务端建立另一条 TLS,在中间解密/重新加密TUN 虚拟网卡方案 App → 虚拟网卡(TUN) → 用户态协议栈 → 真实网卡Linux 创建 TUN: int tun_fd = open("/dev/net/tun", O_RDWR); // 配置后:所有流量走 TUN,用户态读 IP 包 read(tun_fd, buf, sizeof(buf));优点:不需要配系统代理,UDP 也能抓,Clash/sing-box 都用这套。 Python 实现最小 HTTPS MITM 第一步:生成 CA openssl genrsa -out ca.key 2048 openssl req -x509 -new -key ca.key -out ca.crt -days 3650安装 ca.crt 到系统根证书信任。 第二步:动态签发域名证书 from cryptography import x509 from cryptography.x509.oid import NameOID from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa import datetimedef gen_cert(hostname: str, ca_key, ca_cert): key = rsa.generate_private_key(public_exponent=65537, key_size=2048) cert = ( x509.CertificateBuilder() .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, hostname)])) .issuer_name(ca_cert.subject) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(datetime.datetime.utcnow()) .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365)) .add_extension(x509.SubjectAlternativeName([x509.DNSName(hostname)]), critical=False) .sign(ca_key, hashes.SHA256()) ) return key, cert第三步:TLS 劫持 import ssl# 客户端侧:用伪造证书与客户端握手 def mitm_tls_client(client_socket, hostname, certfile, keyfile): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ctx.load_cert_chain(certfile, keyfile) return ctx.wrap_socket(client_socket, server_side=True)# 服务端侧:正常连接真实服务器 def connect_server(hostname, port): ctx = ssl.create_default_context() raw = socket.create_connection((hostname, port)) return ctx.wrap_socket(raw, server_hostname=hostname)两端建立后,中间就能读到明文 HTTP 请求: req = client_tls.recv(8192) print(req.decode()) # POST /v1/chat/completions ...HTTP 解析推荐 不要手动解析 HTTP,使用成熟库: pip install h11 # 轻量,推荐 # 或 pip install httptools证书缓存 对同一域名动态生成一次后缓存,避免每次重复生成: cert_cache = {}def get_cert(hostname): if hostname not in cert_cache: key, cert = gen_cert(hostname, ca_key, ca_cert) cert_cache[hostname] = (key, cert) return cert_cache[hostname]最小模块划分 自己实现一个基础 HTTPS MITM 代理,核心代码约 500~1000 行: proxy/ ├── listener.py # 监听连接 ├── connect.py # 处理 CONNECT 方法 ├── tls.py # TLS 劫持 ├── cert.py # 动态证书生成/缓存 ├── http.py # HTTP 解析 └── logger.py # 请求日志局限性 用户态 HTTP Proxy 在以下场景会遇到问题:QUIC/HTTP3(基于 UDP,不走 CONNECT) App 硬编码不走系统代理 Certificate Pinning(固定证书哈希) gRPC over HTTP2对于需要捕获所有流量的场景(如 AI Agent 全局监听),TUN + gVisor Netstack 扩展性更好。
进程联网监控工具:WFN / Sniffnet / Picosnitch / LuLu
想知道"哪个进程偷偷联网"并实时弹通知,不同系统有不同的成熟开源方案。 Windows:Windows Firewall Notifier(WFN) 最接近需求的工具:进程发起网络连接时弹窗,可以选择允许或拒绝。 特点:基于 Windows 自带防火墙(WFP) 实时弹窗通知:显示进程名、目标 IP/域名、端口 允许/拒绝并记住规则 免费开源(GlassWire 的免费替代)安装后运行即可,不需要修改系统设置。 Windows / Linux / macOS:Sniffnet(流量可视化) Rust 编写,UI 现代,侧重流量监控而非拦截:显示哪个程序在联网、发送/接收了多少流量 支持桌面通知 可过滤特定协议或国家 跨平台(Windows/Linux/macOS)# Windows/macOS 下载安装包 # Linux cargo install sniffnet适合:想了解整体网络情况,而不是逐个审批连接请求。 Linux:Picosnitch eBPF 实现,低开销,功能最强: pip install picosnitch sudo picosnitch start功能:新进程联网时立即通知(systemd/D-Bus 通知) 按可执行文件统计流量 关联父进程(识别子进程发起的连接) 可选接入 VirusTotal 检查新进程 hash Web UI 查看历史记录配置文件(~/.config/picosnitch/config.json): { "Desktop notifications": true, "VT API key": "your_virustotal_api_key" }适合:需要安全审计,或在服务器上追踪异常网络行为。 macOS:LuLu macOS 下最受欢迎的免费联网防火墙:新连接弹窗(允许/拒绝/记住) 显示进程路径和目标地址 支持规则管理 Objective-See 出品(macOS 安全工具知名团队)下载安装包后启用,效果类似 Little Snitch(商业软件)的免费替代。 对比选择工具 系统 特点 是否可拦截WFN Windows 弹窗审批,集成防火墙 ✅Sniffnet 跨平台 流量可视化,UI 好看 ❌Picosnitch Linux eBPF,安全审计,VirusTotal ❌(仅监控)LuLu macOS 弹窗审批,免费 ✅根据场景选择 想控制哪些程序能联网:WFN(Windows)、LuLu(macOS) 想看流量统计和来源:Sniffnet(跨平台) 服务器安全审计/发现异常进程:Picosnitch(Linux) 排查可疑进程(如挖矿木马):先用 Picosnitch/WFN 发现网络连接,再配合 ss -antp | grep <PID> 查目标 IP
跨域 CORS 完整解决方案:后端、Nginx、前端代理全覆盖
跨域的本质 浏览器的同源策略阻止了跨源请求:协议、域名、端口任一不同即为跨域。服务端不受此限制,同源策略是浏览器行为,后端直接请求后端不存在跨域问题。 浏览器在发送非简单请求前会先发一个 OPTIONS 预检请求(preflight),服务端必须正确响应: Access-Control-Allow-Origin: https://example.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization后端配置(根本解法) Node.js / Express 最简单的做法是手动加响应头,或使用 cors 中间件: // 手动写 app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS"); res.header("Access-Control-Allow-Headers", "Content-Type,Authorization"); if (req.method === "OPTIONS") return res.sendStatus(200); next(); });// 或使用 cors 包 const cors = require("cors"); app.use(cors({ origin: "https://your-frontend.com", methods: ["GET", "POST"], allowedHeaders: ["Content-Type", "Authorization"] }));生产环境不要用 *,写具体允许的 origin。带 credentials 时更不能用 *: app.use(cors({ origin: "https://your-frontend.com", credentials: true // 允许 Cookie }));Spring Boot 单个接口: @CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST}) @RestController public class MyController { @GetMapping("/api/data") public String getData() { ... } }全局配置(推荐统一管理): @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://your-frontend.com") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }Nginx 反向代理配置 适合把前端和后端统一在同一域名下,或给第三方接口套一层代理: location /api/ { proxy_pass http://backend:8080; add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type, Authorization" always; add_header Access-Control-Allow-Credentials true always; if ($request_method = OPTIONS) { return 204; } }always 参数确保在非 200 响应(如 4xx、5xx)时也带上跨域头。 前端开发环境代理 开发阶段用本地代理把请求转发到后端,不产生跨域: Vite: // vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } });Vue CLI / webpack-dev-server: // vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } };Create React App: // package.json { "proxy": "http://localhost:8080" }JSONP(仅 GET,老项目兼容) 利用 <script> 标签不受同源限制: <script src="https://api.example.com/data?callback=onData"></script> <script> function onData(data) { console.log(data); // { "name": "test" } } </script>服务端返回: onData({"name":"test"})JSONP 只支持 GET,现代项目不推荐,但对接老旧第三方接口时可能还会遇到。 后端转发(BFF 模式) 前端请求自己的服务端,由服务端再去请求第三方接口: 浏览器 → 自己的后端(同源)→ 第三方 API这样浏览器始终只看到同源请求,完全绕开 CORS 限制。适合第三方接口不支持配置 CORS 的情况。 方案速查场景 推荐方案生产环境,自己控制后端 后端配置 CORS 响应头生产环境,用 Nginx Nginx 加 Access-Control-* 头开发环境调试 本地代理(Vite/Vue CLI/CRA)第三方接口不支持 CORS 后端转发(BFF)老项目只有 GET 需求 JSONP临时调试(不上线) 浏览器 CORS 插件
TUN/TAP 虚拟网卡与 VPN 代理:TUN0 路由拦截与 TAP0901 驱动
TUN vs TAP特性 TUN TAP模拟对象 IP 网络接口 以太网卡工作层 第三层(IP 层) 第二层(数据链路层)处理单元 IP 数据包 以太网帧支持协议 仅 IP IP、ARP、广播等典型用途 全局路由代理 局域网桥接CPU 开销 较小 较大TUN 适合"全部流量走 VPN",TAP 适合"需要访问远程 LAN 内设备"的桥接场景。 TUN 模式的流量拦截 VPN 客户端启用 TUN 模式的流程:创建虚拟网卡 tun0 修改系统路由表,将默认路由(0.0.0.0/0)指向 tun0 所有出站 IP 包经过 tun0 → VPN 客户端加密封装 → 通过真实网卡发送到 VPN 服务器[本地应用] | (IP 包) v [tun0 虚拟网卡] | (加密封装) v [VPN 客户端程序] | v [VPN 服务器 -> 目标网络]TUN 全局模式会代理 localhost 开启全局代理时,若未排除环回地址,127.0.0.1 的流量也会被 tun0 拦截,导致本地服务访问失败。 排除 localhost(OpenVPN 配置) route 127.0.0.1 255.0.0.0 net_gateway或在客户端设置中勾选"分流模式",只代理外网 IP。 TAP0901 驱动(Windows) TAP-Windows Adapter V9(tap0901)是 OpenVPN 在 Windows 上使用的虚拟以太网卡驱动,安装后在网络适配器列表中以普通网卡形式出现。 驱动通过设备文件暴露接口: \\.\Global\{TAP-Adapter-GUID}.tapJava 通过 JNA 读写数据包 // 打开设备文件 HANDLE h = Kernel32.INSTANCE.CreateFile( "\\\\.\\Global\\{GUID}.tap", WinNT.GENERIC_READ | WinNT.GENERIC_WRITE, 0, null, WinNT.OPEN_EXISTING, WinNT.FILE_ATTRIBUTE_NORMAL, null );// 读取以太网帧 byte[] buf = new byte[2048]; IntByReference bytesRead = new IntByReference(); Kernel32.INSTANCE.ReadFile(h, buf, buf.length, bytesRead, null);// 写入以太网帧 Kernel32.INSTANCE.WriteFile(h, buf, bytesRead.getValue(), bytesRead, null);操作虚拟网卡需要管理员权限。TAP0901 只在 Windows 上可用;Linux 和 macOS 使用系统内置的 TUN/TAP 接口。 实现 VPN 客户端的选型目标 方案 难度仅代理指定应用流量 纯 Socket / SOCKS5 代理 低全局系统流量拦截 TUN/TAP 驱动 + JNI/JNA 高Java 只能做用户空间的协议与加密逻辑;全局流量拦截必须借助内核级虚拟网卡驱动(TUN/TAP)。
