HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 代理技术全栈手册

    • 代理技术全栈手册 - HiHuo
    • 原理篇

      • 第01章 代理是什么:正向 / 反向 / 透明 / 隧道的统一模型
      • 第02章 代理与网络层级:L3 / L4 / L5 / L7 在哪里截断流量
      • 第03章 一个请求穿过代理的一生:连接生命周期全景
    • 协议篇

      • 第04章 HTTP 代理协议:绝对 URI、CONNECT 隧道、转发头与连接池
      • 第05章 HTTPS 与 TLS 代理:终止 / 透传 / MITM / SNI / mTLS
      • 第06章 SOCKS 协议:SOCKS4/4a/5 与 UDP ASSOCIATE 报文级解析
      • 第07章 HTTP/2、gRPC 与 HTTP/3(QUIC) 代理的挑战
      • 第08章 代理自动配置:PAC / WPAD / 系统代理 / NO_PROXY
    • 层级与转发篇

      • 第09章 L4 代理:TCP/UDP 转发与连接级负载均衡
      • 第10章 L7 代理:协议感知与基于内容的路由
      • 第11章 透明代理:iptables REDIRECT/DNAT、TPROXY 与 eBPF 劫持
      • 第12章 数据搬运的艺术:splice / sendfile / 零拷贝 / io_uring
    • 组件横评篇

      • 第13章 Nginx / OpenResty:反向代理、upstream 与 Lua 可编程
      • 第14章 HAProxy:L4/L7、ACL、健康检查与 stick table
      • 第15章 Envoy:xDS 动态配置与 filter chain,为何是云原生数据面
      • 第16章 Traefik / Caddy:自动服务发现与自动 HTTPS
      • 第17章 Squid 与正向/缓存代理:企业出网、缓存与审计
      • 第18章 mitmproxy:抓包、改包、脚本化调试
      • 第19章 内网穿透与隧道:frp / gost / SSH 隧道 / ngrok
      • 第20章 科学上网生态的技术原理(技术中立)
    • 多语言手写篇

      • 第21章 Go:100 行手写 HTTP/CONNECT + SOCKS5 代理
      • 第22章 Rust:基于 tokio 的高性能 TCP 代理
      • 第23章 Python:asyncio 实现,适合调试与脚本
      • 第24章 C:epoll 裸写与零拷贝,及语言选型对比
    • 容器与K8s篇

      • 第25章 Docker 里的代理:HTTP_PROXY、build/pull 与 daemon 配置
      • 第26章 Sidecar 与流量劫持:Istio init-container 的 iptables 原理
      • 第27章 Ingress 与南北流量:Ingress-nginx 与 Gateway API
      • 第28章 Egress 与出网治理:出口网关、registry mirror、审计
      • 第29章 Service Mesh 数据面:Envoy Sidecar 全链路
    • 进阶篇

      • 第30章 可编程代理:Lua / Wasm / eBPF / xDS,代理的"软件定义"
      • 第31章 性能调优:并发模型、连接池、超时与重试、压测
      • 第32章 排错决策树:502 / 504 / 握手失败 / 环路 / 泄漏
      • 第33章 代理安全:开放代理、SSRF、凭证泄漏与攻击面
    • 底层机制篇

      • 第34章 代理的背压与流控:一个代理最难的部分
      • 第35章 socket 与 TCP 状态机:半关闭、超时、连接生命周期
      • 第36章 HTTP/2 帧、流控与 HPACK:h2 代理的内部机制
      • 第37章 负载均衡算法推导与韧性状态机
      • 第38章 Capstone:把玩具代理改造成生产级骨架
    • 综合实战篇

      • 第39章 企业多跳转发链:拓扑、协议矩阵与贯穿性难题
      • 第40章 端到端实战:把 6 类流量全代理通
      • 第41章 更刁钻的流量:gRPC、长轮询、WebRTC、大文件、双向流
      • 第42章 可落地完整参考实现:一套能跑的多协议转发栈
    • 附录

      • 附录 A:代理协议报文速查(HTTP / SOCKS / PAC / PROXY protocol)
      • 附录 B:组件选型决策树
      • 附录 C:抓包与命令速查

第21章 Go:100 行手写 HTTP/CONNECT + SOCKS5 代理

学习目标

  • 用 Go 标准库从零写出一个能跑的 HTTP 正向代理(含 CONNECT 隧道)
  • 按 RFC 1928 手撕 SOCKS5 协议:握手、地址解析、双向转发
  • 理解 Go 为何是写代理的"顺手"语言:goroutine-per-connection + io.Copy + Hijack
  • 亲手用 curl -x / curl --socks5 验证自己写的代理

前置知识

  • 第04章 HTTP 代理协议(absolute-form、CONNECT、hop-by-hop 头)
  • 第06章 SOCKS 协议(报文级解析,本章是它的代码落地)
  • 会一点 Go,装了 Go 1.21+

原理:任何代理的核心都是这四步

抛开协议细节,所有代理的骨架都一样:

  ┌─ 1. Accept ──────── 接受客户端连接
  ├─ 2. 解析目标 ────── 从协议里读出"要去哪"(HTTP 看 URL,SOCKS 看请求报文)
  ├─ 3. Dial 目标 ───── 自己发起到源站的连接
  └─ 4. 双向拷贝 ────── 在两条连接间 bidirectional copy,直到一方关闭

不同协议的差异只在第 2 步"怎么知道目标"。Go 把第 1、4 步做到了极致省心:

  • 第 1 步:for { c, _ := ln.Accept(); go handle(c) }——一个连接一个 goroutine,几行搞定百万并发模型
  • 第 4 步:io.Copy(dst, src)——在 Linux 上,TCP→TCP 的 io.Copy 底层自动走 splice(2) 零拷贝(见 第12章),不经用户态缓冲

所以"灵活性"在 Go 里几乎是免费的——你只需专注第 2 步的协议解析。


代码一:HTTP 正向代理(含 CONNECT)

Go 的 net/http 自带 HTTP 解析,正向代理可以站在它肩上:普通请求用 Transport.RoundTrip 转发,CONNECT 用 Hijack 拿到裸连接做隧道。

// http_proxy.go —— go run http_proxy.go,监听 :8080
package main

import (
	"io"
	"log"
	"net"
	"net/http"
	"time"
)

// hop-by-hop 头:代理必须删除,不得转发(见第04章)
var hopHeaders = []string{
	"Connection", "Proxy-Connection", "Keep-Alive",
	"Proxy-Authenticate", "Proxy-Authorization",
	"Te", "Trailer", "Transfer-Encoding", "Upgrade",
}

func main() {
	server := &http.Server{
		Addr:    ":8080",
		Handler: http.HandlerFunc(handle),
	}
	log.Println("HTTP 代理已启动: 127.0.0.1:8080")
	log.Fatal(server.ListenAndServe())
}

func handle(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodConnect {
		handleConnect(w, r) // HTTPS 等加密流量走隧道
	} else {
		handleHTTP(w, r) // 明文 HTTP 直接转发
	}
}

// 明文 HTTP:absolute-form 的 URL 已被标准库解析进 r.URL,直接重发
func handleHTTP(w http.ResponseWriter, r *http.Request) {
	removeHopHeaders(r.Header)
	r.RequestURI = "" // RoundTrip 要求客户端请求必须清空 RequestURI

	resp, err := http.DefaultTransport.RoundTrip(r)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadGateway) // 502:到源站失败
		return
	}
	defer resp.Body.Close()

	removeHopHeaders(resp.Header)
	copyHeader(w.Header(), resp.Header)
	w.WriteHeader(resp.StatusCode)
	io.Copy(w, resp.Body)
}

// CONNECT:劫持底层 TCP,两条连接间盲转字节
func handleConnect(w http.ResponseWriter, r *http.Request) {
	dst, err := net.DialTimeout("tcp", r.Host, 10*time.Second) // r.Host = "example.com:443"
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadGateway)
		return
	}

	hij, ok := w.(http.Hijacker)
	if !ok {
		http.Error(w, "不支持 Hijack", http.StatusInternalServerError)
		dst.Close()
		return
	}
	client, _, err := hij.Hijack() // 拿回裸 net.Conn,标准库不再插手
	if err != nil {
		dst.Close()
		return
	}

	// 手动写 200 到被劫持的连接,告诉客户端"隧道已开"
	client.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))

	// 双向拷贝:任一方向结束就关闭两端
	go transfer(dst, client)
	transfer(client, dst)
}

func transfer(dst, src net.Conn) {
	defer dst.Close()
	defer src.Close()
	io.Copy(dst, src) // Linux 上 TCP→TCP 自动 splice 零拷贝
}

func removeHopHeaders(h http.Header) {
	for _, k := range hopHeaders {
		h.Del(k)
	}
}

func copyHeader(dst, src http.Header) {
	for k, vv := range src {
		for _, v := range vv {
			dst.Add(k, v)
		}
	}
}

跑起来验证:

go run http_proxy.go &

# 明文 HTTP:走 handleHTTP(absolute-form)
curl -x http://127.0.0.1:8080 http://httpbin.org/ip
# {"origin": "203.0.113.7"}

# HTTPS:走 handleConnect(CONNECT 隧道)
curl -x http://127.0.0.1:8080 https://example.com/ -s -o /dev/null -w "%{http_code}\n"
# 200

就这 80 行,你已经有了一个能代理明文 HTTP 和 HTTPS 的可用正向代理。Hijack 是关键——它让你从"HTTP 语义"掉回"裸 TCP 字节流",这正是隧道所需。


代码二:SOCKS5 代理(裸 TCP 手撕协议)

SOCKS5 没有现成标准库解析,正好让我们逐字节实现 RFC 1928。流程三段:协商认证方法 → 读取 CONNECT 请求 → 转发。

// socks5.go —— go run socks5.go,监听 :1080
package main

import (
	"encoding/binary"
	"fmt"
	"io"
	"log"
	"net"
)

func main() {
	ln, err := net.Listen("tcp", ":1080")
	if err != nil {
		log.Fatal(err)
	}
	log.Println("SOCKS5 代理已启动: 127.0.0.1:1080")
	for {
		c, err := ln.Accept()
		if err != nil {
			continue
		}
		go handle(c)
	}
}

func handle(client net.Conn) {
	defer client.Close()

	// ── 阶段 1:方法协商 ──
	// 客户端: VER(0x05) NMETHODS METHOD...
	head := make([]byte, 2)
	if _, err := io.ReadFull(client, head); err != nil || head[0] != 0x05 {
		return
	}
	methods := make([]byte, int(head[1]))
	if _, err := io.ReadFull(client, methods); err != nil {
		return
	}
	// 服务端: VER(0x05) METHOD(0x00=无需认证)
	client.Write([]byte{0x05, 0x00})

	// ── 阶段 2:读取请求 ──
	// 客户端: VER CMD RSV ATYP DST.ADDR DST.PORT
	req := make([]byte, 4)
	if _, err := io.ReadFull(client, req); err != nil {
		return
	}
	ver, cmd, atyp := req[0], req[1], req[3]
	if ver != 0x05 || cmd != 0x01 { // 只支持 CONNECT(0x01)
		reply(client, 0x07)         // 0x07 = 命令不支持
		return
	}

	host, err := readAddr(client, atyp)
	if err != nil {
		reply(client, 0x08) // 0x08 = 地址类型不支持
		return
	}
	portBuf := make([]byte, 2)
	if _, err := io.ReadFull(client, portBuf); err != nil {
		return
	}
	target := fmt.Sprintf("%s:%d", host, binary.BigEndian.Uint16(portBuf))

	// ── 阶段 3:拨号 + 应答 + 转发 ──
	dst, err := net.Dial("tcp", target)
	if err != nil {
		reply(client, 0x05) // 0x05 = 连接被拒
		return
	}
	defer dst.Close()
	reply(client, 0x00) // 0x00 = 成功

	go io.Copy(dst, client)
	io.Copy(client, dst)
}

// 按 ATYP 读出目标地址
func readAddr(c net.Conn, atyp byte) (string, error) {
	switch atyp {
	case 0x01: // IPv4:4 字节
		b := make([]byte, 4)
		_, err := io.ReadFull(c, b)
		return net.IP(b).String(), err
	case 0x04: // IPv6:16 字节
		b := make([]byte, 16)
		_, err := io.ReadFull(c, b)
		return net.IP(b).String(), err
	case 0x03: // 域名:1 字节长度 + N 字节
		l := make([]byte, 1)
		if _, err := io.ReadFull(c, l); err != nil {
			return "", err
		}
		b := make([]byte, int(l[0]))
		_, err := io.ReadFull(c, b)
		return string(b), err
	}
	return "", fmt.Errorf("未知 ATYP: %d", atyp)
}

// 应答: VER REP RSV ATYP BND.ADDR BND.PORT(BND 全填 0 即可)
func reply(c net.Conn, rep byte) {
	c.Write([]byte{0x05, rep, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
}

跑起来验证:

go run socks5.go &

# --socks5:本地解析 DNS,发 IPv4/IPv6 ATYP
curl --socks5 127.0.0.1:1080 http://httpbin.org/ip
# {"origin": "203.0.113.7"}

# --socks5-hostname:远端解析 DNS,发域名 ATYP(走我们的 0x03 分支)
curl --socks5-hostname 127.0.0.1:1080 https://example.com/ -s -o /dev/null -w "%{http_code}\n"
# 200

SOCKS5 比 HTTP 代理"更底层":它不关心你传的是 HTTP、TLS 还是任意 TCP,只负责"帮你连到 host:port"。所以同一个 80 行 SOCKS5,能代理 HTTP、HTTPS、SSH、数据库连接——这就是 SOCKS 的通用性。两者对比详见 第06章。

抓包看自己写的 SOCKS5 握手

sudo tcpdump -i lo -n -X 'tcp port 1080' 2>/dev/null | head -20

你能亲眼看到二进制握手:开头 05 01 00(VER=5, NMETHODS=1, METHOD=0),服务端回 05 00,然后 05 01 00 03 0b ...(CONNECT + 域名长度 11 + "httpbin.org")。把 第06章 的报文图和这段抓包对照,协议就刻进脑子里了。


为什么 Go 写代理这么顺:灵活性来自哪

能力Go 怎么做到对代理意味着什么
高并发连接goroutine-per-connection,每连接几 KB 栈不用写 epoll 事件循环就能扛海量长连接
零拷贝转发io.Copy(tcpA, tcpB) 自动 splice(2)数据不进用户态,吞吐接近内核转发
掉回裸字节http.Hijacker同一端口既做 L7 解析又做 L4 隧道
协议解析encoding/binary、bufio手撕二进制协议(SOCKS)几十行
超时/取消context、SetDeadline连接级超时控制简单可靠

这也是为什么云原生代理 Traefik、frp、gost、v2ray-core、mihomo(Clash) 全用 Go 写——开发效率和性能的甜点。对比其它语言的取舍(Rust 更快更省、C 更接近极限、Python 更适合调试)见 第24章 语言选型。


排错

现象原因解决
RoundTrip 报 RequestURI can't be set忘了 r.RequestURI = ""转发前清空
CONNECT 后客户端卡住无响应没写 200 Connection Established,或没换行 \r\n\r\n检查手写的状态行
HTTPS 代理证书错误你的代理没做 MITM 是正常的(隧道不解密)想看明文见 第05章
SOCKS5 curl: (97) Can't complete SOCKS5ATYP 分支没读全字节,或应答格式错核对 readAddr 与 reply 的字节数
连接泄漏、goroutine 暴涨io.Copy 一端结束没关另一端用 defer Close() 成对关闭(如代码所示)
大量 CLOSE_WAIT同上,半关闭未处理任一方向 EOF 即关闭双端

本章小结

  • 代理骨架永远是 Accept → 解析目标 → Dial → 双向拷贝,差异只在"怎么解析目标"。
  • HTTP 代理可站在 net/http 上,普通请求用 RoundTrip、CONNECT 用 Hijack 转隧道。
  • SOCKS5 需手撕协议:方法协商 → CONNECT 请求(ATYP 解析) → 应答 → 转发,但它对载荷无感,更通用。
  • Go 的 goroutine + io.Copy(splice) + Hijack 让"灵活性"近乎免费,这是它成为代理首选语言的原因。

下一章 第22章 我们换用 Rust + tokio,看异步运行时如何在不牺牲性能的前提下榨干每一核。

动手作业:给本章的 HTTP 代理加上 407 代理认证(校验 Proxy-Authorization),再给 SOCKS5 加上用户名/密码认证(method 0x02,RFC 1929)。两个加起来不超过 30 行——你会真正体会到"代理的灵活性"。

Next
第22章 Rust:基于 tokio 的高性能 TCP 代理