NaiveProxy 协议
NaiveProxy 使用 CONNECT 方法将代理流量伪装为正常的 HTTP/2 或 HTTP/3 流量。inbound 实现了兼容 NaiveProxy 的服务器并支持 padding,而 outbound 使用 Cronet(Chromium 网络栈)库来模拟真实的 Chrome 客户端。
源码: protocol/naive/inbound.go, protocol/naive/inbound_conn.go, protocol/naive/outbound.go, protocol/naive/quic/
Inbound 架构
type Inbound struct {
inbound.Adapter
ctx context.Context
router adapter.ConnectionRouterEx
logger logger.ContextLogger
listener *listener.Listener
network []string
networkIsDefault bool
authenticator *auth.Authenticator
tlsConfig tls.ServerConfig
httpServer *http.Server
h3Server io.Closer
}双传输层:HTTP/2 + HTTP/3
NaiveProxy 同时支持 HTTP/2(TCP)和 HTTP/3(QUIC)。网络默认为 TCP,可选 UDP 用于 HTTP/3:
if common.Contains(inbound.network, N.NetworkUDP) {
if options.TLS == nil || !options.TLS.Enabled {
return nil, E.New("TLS is required for QUIC server")
}
}HTTP/2 服务器(TCP)
TCP 监听器通过 h2c(HTTP/2 明文)提供 HTTP/2 服务,可选 TLS:
n.httpServer = &http.Server{
Handler: h2c.NewHandler(n, &http2.Server{}),
}
go func() {
listener := net.Listener(tcpListener)
if n.tlsConfig != nil {
// 确保 HTTP/2 ALPN 存在
if !common.Contains(n.tlsConfig.NextProtos(), http2.NextProtoTLS) {
n.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, n.tlsConfig.NextProtos()...))
}
listener = aTLS.NewListener(tcpListener, n.tlsConfig)
}
n.httpServer.Serve(listener)
}()HTTP/3 服务器(QUIC)
HTTP/3 通过可配置的函数指针初始化:
var ConfigureHTTP3ListenerFunc func(ctx, logger, listener, handler, tlsConfig, options) (io.Closer, error)这在 protocol/naive/quic/inbound_init.go 中外部注册,使用 sing-quic 库并支持可配置的拥塞控制。
CONNECT 请求处理
核心协议逻辑在 ServeHTTP 中:
func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
// 1. 拒绝非 CONNECT 请求
if request.Method != "CONNECT" {
rejectHTTP(writer, http.StatusBadRequest)
return
}
// 2. 要求 padding 头部(区分 NaiveProxy 与普通 CONNECT)
if request.Header.Get("Padding") == "" {
rejectHTTP(writer, http.StatusBadRequest)
return
}
// 3. 通过 Proxy-Authorization 头部认证
userName, password, authOk := sHttp.ParseBasicAuth(request.Header.Get("Proxy-Authorization"))
if authOk {
authOk = n.authenticator.Verify(userName, password)
}
if !authOk {
rejectHTTP(writer, http.StatusProxyAuthRequired)
return
}
// 4. 发送带 padding 的响应
writer.Header().Set("Padding", generatePaddingHeader())
writer.WriteHeader(http.StatusOK)
writer.(http.Flusher).Flush()
// 5. 从自定义或标准头部提取目标地址
hostPort := request.Header.Get("-connect-authority")
if hostPort == "" {
hostPort = request.URL.Host
}
// 6. 为前 8 帧使用 padding 包装连接
// HTTP/1.1:劫持连接
// HTTP/2:使用 request.Body + response writer
}拒绝行为
拒绝时,连接使用 RST 而非优雅关闭,以模拟真实 Web 服务器的行为:
func rejectHTTP(writer http.ResponseWriter, statusCode int) {
hijacker, ok := writer.(http.Hijacker)
if !ok {
writer.WriteHeader(statusCode)
return
}
conn, _, _ := hijacker.Hijack()
if tcpConn, isTCP := common.Cast[*net.TCPConn](conn); isTCP {
tcpConn.SetLinger(0) // RST 而非 FIN
}
conn.Close()
}Padding 协议
padding 协议为前 8 次读写操作添加随机 padding 以抵抗流量指纹识别。
常量和结构
const paddingCount = 8
type paddingConn struct {
readPadding int // 已读取的带 padding 帧数
writePadding int // 已写入的带 padding 帧数
readRemaining int // 当前帧中剩余的数据字节数
paddingRemaining int // 需要跳过的剩余 padding 字节数
}Padding 头部格式
Padding HTTP 头部使用从字符集 !#$()+<>?@[]^{}~` 中选取的 30-62 个随机字符:
func generatePaddingHeader() string {
paddingLen := rand.Intn(32) + 30
padding := make([]byte, paddingLen)
bits := rand.Uint64()
for i := 0; i < 16; i++ {
padding[i] = "!#$()+<>?@[]^`{}"[bits&15]
bits >>= 4
}
for i := 16; i < paddingLen; i++ {
padding[i] = '~'
}
return string(padding)
}线路格式(带 Padding 的帧)
前 8 帧中的每一帧编码如下:
+---------------+----------+------+---------+
| Data Length | Pad Size | Data | Padding |
| (2 bytes BE) | (1 byte) | (var)| (var) |
+---------------+----------+------+---------+func (p *paddingConn) writeWithPadding(writer io.Writer, data []byte) (n int, err error) {
if p.writePadding < paddingCount {
paddingSize := rand.Intn(256)
buffer := buf.NewSize(3 + len(data) + paddingSize)
header := buffer.Extend(3)
binary.BigEndian.PutUint16(header, uint16(len(data)))
header[2] = byte(paddingSize)
buffer.Write(data)
buffer.Extend(paddingSize) // 随机 padding 字节
_, err = writer.Write(buffer.Bytes())
p.writePadding++
return
}
// 8 帧之后直接写入
return writer.Write(data)
}读取带 Padding 的帧
func (p *paddingConn) readWithPadding(reader io.Reader, buffer []byte) (n int, err error) {
// 如果当前帧有剩余数据,读取它
if p.readRemaining > 0 { /* 读取剩余数据 */ }
// 跳过前一帧剩余的 padding
if p.paddingRemaining > 0 {
rw.SkipN(reader, p.paddingRemaining)
}
// 读取下一个带 padding 的帧头部(3 字节)
if p.readPadding < paddingCount {
io.ReadFull(reader, paddingHeader[:3])
originalDataSize := binary.BigEndian.Uint16(paddingHeader[:2])
paddingSize := int(paddingHeader[2])
n, _ = reader.Read(buffer[:originalDataSize])
p.readPadding++
p.readRemaining = originalDataSize - n
p.paddingRemaining = paddingSize
return
}
// 8 帧之后直接读取
return reader.Read(buffer)
}连接可替换性
padding 阶段(8 帧)结束后,padding 包装变为透明:
func (p *paddingConn) readerReplaceable() bool {
return p.readPadding == paddingCount
}
func (p *paddingConn) writerReplaceable() bool {
return p.writePadding == paddingCount
}两种连接类型
naiveConn:用于 HTTP/1.1 劫持的连接(包装net.Conn)naiveH2Conn:用于 HTTP/2 流(包装io.Reader+io.Writer+http.Flusher);每次写入后必须刷新
Outbound 架构(Cronet)
outbound 使用 Cronet 库(Chromium 的网络栈),使连接与真实的 Chrome 无法区分:
//go:build with_naive_outbound
type Outbound struct {
outbound.Adapter
ctx context.Context
logger logger.ContextLogger
client *cronet.NaiveClient
uotClient *uot.Client
}构建标签
outbound 需要 with_naive_outbound 构建标签。
TLS 限制
许多 TLS 选项不受支持,因为 Cronet 管理自己的 TLS:
if options.TLS.DisableSNI { return nil, E.New("not supported") }
if options.TLS.Insecure { return nil, E.New("not supported") }
if len(options.TLS.ALPN) > 0 { return nil, E.New("not supported") }
if options.TLS.UTLS != nil { return nil, E.New("not supported") }
if options.TLS.Reality != nil { return nil, E.New("not supported") }
// ... 更多限制客户端配置
client, _ := cronet.NewNaiveClient(cronet.NaiveClientOptions{
ServerAddress: serverAddress,
ServerName: serverName,
Username: options.Username,
Password: options.Password,
InsecureConcurrency: options.InsecureConcurrency,
ExtraHeaders: extraHeaders,
TrustedRootCertificates: trustedRootCertificates,
Dialer: outboundDialer,
DNSResolver: dnsResolver,
ECHEnabled: echEnabled,
QUIC: options.QUIC,
QUICCongestionControl: quicCongestionControl,
})QUIC 拥塞控制(Outbound)
outbound 支持多种 QUIC 拥塞控制算法:
switch options.QUICCongestionControl {
case "bbr": quicCongestionControl = cronet.QUICCongestionControlBBR
case "bbr2": quicCongestionControl = cronet.QUICCongestionControlBBRv2
case "cubic": quicCongestionControl = cronet.QUICCongestionControlCubic
case "reno": quicCongestionControl = cronet.QUICCongestionControlReno
}ECH 支持
outbound 支持加密客户端 Hello(Encrypted Client Hello):
if options.TLS.ECH != nil && options.TLS.ECH.Enabled {
echEnabled = true
echConfigList = block.Bytes // PEM 解码的 "ECH CONFIGS"
}DNS 集成
outbound 使用 sing-box DNS 路由器在 Cronet 内部进行名称解析:
dnsResolver = func(dnsContext context.Context, request *mDNS.Msg) *mDNS.Msg {
response, _ := dnsRouter.Exchange(dnsContext, request, outboundDialer.(dialer.ResolveDialer).QueryOptions())
return response
}通过 UoT 支持 UDP
UDP 仅通过 UDP-over-TCP 可用:
if uotOptions.Enabled {
outbound.uotClient = &uot.Client{
Dialer: &naiveDialer{client},
Version: uotOptions.Version,
}
}配置示例
Inbound
{
"type": "naive",
"tag": "naive-in",
"listen": "::",
"listen_port": 443,
"users": [
{ "username": "user1", "password": "pass1" }
],
"tls": {
"enabled": true,
"certificate_path": "/path/to/cert.pem",
"key_path": "/path/to/key.pem"
}
}Outbound
{
"type": "naive",
"tag": "naive-out",
"server": "example.com",
"server_port": 443,
"username": "user1",
"password": "pass1",
"tls": {
"enabled": true,
"server_name": "example.com"
},
"udp_over_tcp": {
"enabled": true,
"version": 2
}
}