Skip to content

Commit 2363ce5

Browse files
committed
refact: Trying to rewrite a better HTTP backend
1 parent 89283e9 commit 2363ce5

5 files changed

Lines changed: 310 additions & 47 deletions

File tree

REPO_SCHEMA.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ Go 语言编写的底层核心,负责高性能的网络处理:
5555
- [`libcore/nb4a.go`](libcore/nb4a.go): Go 核心的入口,导出 `InitCore` 等函数,供 Android 端通过 JNI 调用。
5656
- [`libcore/box.go`](libcore/box.go) / [`box_include.go`](libcore/box_include.go): 与 `sing-box` 核心的集成与初始化。
5757
- [`libcore/build.sh`](libcore/build.sh): 编译 Go 核心的本地脚本。
58-
- [`libcore/device/`](libcore/device/), [`ech/`](libcore/ech/), [`procfs/`](libcore/procfs/), [`protocol/`](libcore/protocol/), [`stun/`](libcore/stun/): Go 核心的子模块,处理设备、ECH、进程文件系统、自定义协议和 STUN 测试。
58+
- [`libcore/device/`](libcore/device/), [`ech/`](libcore/ech/), [`procfs/`](libcore/procfs/), [`stun/`](libcore/stun/): Go 核心的子模块,处理设备、ECH、进程文件系统和 STUN 测试。
59+
- [`libcore/protocol/`](libcore/protocol/): libcore 侧自定义/覆盖的 sing-box 协议实现,在 [`libcore/box_include.go`](libcore/box_include.go) 中注册。
60+
- `juicity/`: Juicity outbound。
61+
- `http/`: 对 sing-box `http` outbound 的**覆盖实现**(在 sing-box 自身注册之后再次注册同名 `"http"` 类型,registry 后注册生效)。行为差异:TLS 启用且用户未显式配置 ALPN 时默认提供 `["h2", "http/1.1"]`,TLS 握手后按 ALPN 协商结果分流——协商到 `h2` 走 HTTP/2 CONNECT(基于 `golang.org/x/net/http2`,上行流为 `io.Pipe` 请求体、响应体为下行流),否则保持原有 HTTP/1.1 CONNECT。用于兼容 h2-only 的 HTTPS 代理节点(对齐 v2ray 系核心行为);用户可在节点配置中显式填写 ALPN=`http/1.1` 回退旧行为。
5962

6063
---
6164

ROO_TODO.example.md

Lines changed: 0 additions & 45 deletions
This file was deleted.

libcore/box_include.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"github.com/sagernet/sing-box/protocol/vmess"
4141
"github.com/sagernet/sing-box/protocol/wireguard"
4242

43+
h2http "libcore/protocol/http"
4344
"libcore/protocol/juicity"
4445

4546
_ "github.com/sagernet/sing-box/experimental/clashapi"
@@ -74,6 +75,9 @@ func nekoboxAndroidOutboundRegistry() *outbound.Registry {
7475

7576
socks.RegisterOutbound(registry)
7677
http.RegisterOutbound(registry)
78+
// 覆盖 sing-box 的 http outbound:TLS 下默认 ALPN ["h2","http/1.1"],
79+
// 协商到 h2 时走 HTTP/2 CONNECT(兼容 h2-only HTTPS 代理节点)。
80+
h2http.RegisterOutbound(registry)
7781
shadowsocks.RegisterOutbound(registry)
7882
shadowsocksr.RegisterOutbound(registry)
7983
vmess.RegisterOutbound(registry)

libcore/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
github.com/sagernet/sing-tun v0.7.10
1717
github.com/ulikunitz/xz v0.5.15
1818
golang.org/x/mobile v0.0.0-20231108233038-35478a0c49da
19+
golang.org/x/net v0.49.0
1920
golang.org/x/sys v0.41.0
2021
)
2122

@@ -78,7 +79,6 @@ require (
7879
golang.org/x/crypto v0.48.0 // indirect
7980
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
8081
golang.org/x/mod v0.32.0 // indirect
81-
golang.org/x/net v0.49.0 // indirect
8282
golang.org/x/sync v0.19.0 // indirect
8383
golang.org/x/text v0.34.0 // indirect
8484
golang.org/x/time v0.9.0 // indirect

libcore/protocol/http/outbound.go

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
// Package http 提供 sing-box "http" outbound 的覆盖实现:
2+
// 在 TLS 启用时默认提供 ALPN ["h2", "http/1.1"],握手后按协商结果分流
3+
// CONNECT 协议(h2 → HTTP/2 CONNECT,否则 → 原有 HTTP/1.1 CONNECT)。
4+
// 用于兼容 h2-only 的 HTTPS 代理节点(对齐 v2ray 系核心的既有行为)。
5+
package http
6+
7+
import (
8+
std_bufio "bufio"
9+
"context"
10+
stdtls "crypto/tls"
11+
"encoding/base64"
12+
"io"
13+
"net"
14+
stdhttp "net/http"
15+
"net/url"
16+
"os"
17+
"strings"
18+
"sync"
19+
"time"
20+
21+
"github.com/sagernet/sing-box/adapter"
22+
"github.com/sagernet/sing-box/adapter/outbound"
23+
"github.com/sagernet/sing-box/common/dialer"
24+
"github.com/sagernet/sing-box/common/tls"
25+
C "github.com/sagernet/sing-box/constant"
26+
"github.com/sagernet/sing-box/log"
27+
"github.com/sagernet/sing-box/option"
28+
"github.com/sagernet/sing/common"
29+
"github.com/sagernet/sing/common/buf"
30+
"github.com/sagernet/sing/common/bufio"
31+
E "github.com/sagernet/sing/common/exceptions"
32+
"github.com/sagernet/sing/common/logger"
33+
M "github.com/sagernet/sing/common/metadata"
34+
N "github.com/sagernet/sing/common/network"
35+
sHTTP "github.com/sagernet/sing/protocol/http"
36+
"golang.org/x/net/http2"
37+
)
38+
39+
// RegisterOutbound 以相同的类型名 "http" 覆盖注册,
40+
// 须在 sing-box protocol/http.RegisterOutbound 之后调用。
41+
func RegisterOutbound(registry *outbound.Registry) {
42+
outbound.Register[option.HTTPOutboundOptions](registry, C.TypeHTTP, NewOutbound)
43+
}
44+
45+
var _ adapter.Outbound = (*Outbound)(nil)
46+
47+
type Outbound struct {
48+
outbound.Adapter
49+
logger logger.ContextLogger
50+
client *sHTTP.Client // 明文 HTTP 路径
51+
dialer N.Dialer // TLS detour dialer(TLS 启用时使用)
52+
serverAddr M.Socksaddr
53+
tlsEnabled bool
54+
username string
55+
password string
56+
host string
57+
path string
58+
headers stdhttp.Header
59+
}
60+
61+
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (adapter.Outbound, error) {
62+
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
63+
if err != nil {
64+
return nil, err
65+
}
66+
tlsEnabled := options.TLS != nil && options.TLS.Enabled
67+
if tlsEnabled && len(options.TLS.ALPN) == 0 {
68+
// 用户未显式配置 ALPN 时默认提供 ["h2", "http/1.1"],
69+
// 握手后按协商结果分流 CONNECT 协议。
70+
// 显式配置 ALPN 时尊重用户配置(例如填 http/1.1 可回退旧行为)。
71+
options.TLS.ALPN = []string{"h2", "http/1.1"}
72+
}
73+
detour, err := tls.NewDialerFromOptions(ctx, router, outboundDialer, options.Server, common.PtrValueOrDefault(options.TLS))
74+
if err != nil {
75+
return nil, err
76+
}
77+
headers := options.Headers.Build()
78+
var host string
79+
if headers != nil {
80+
host = headers.Get("Host")
81+
}
82+
// 注意:sHTTP.NewClient 内部会从 headers 中删除 Host(共享同一 map),
83+
// 之后 h.headers 即为不含 Host 的版本,可直接用于 TLS 分支。
84+
return &Outbound{
85+
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHTTP, tag, []string{N.NetworkTCP}, options.DialerOptions),
86+
logger: logger,
87+
client: sHTTP.NewClient(sHTTP.Options{
88+
Dialer: detour,
89+
Server: options.ServerOptions.Build(),
90+
Username: options.Username,
91+
Password: options.Password,
92+
Path: options.Path,
93+
Headers: headers,
94+
}),
95+
dialer: detour,
96+
serverAddr: options.ServerOptions.Build(),
97+
tlsEnabled: tlsEnabled,
98+
username: options.Username,
99+
password: options.Password,
100+
host: host,
101+
path: options.Path,
102+
headers: headers,
103+
}, nil
104+
}
105+
106+
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
107+
ctx, metadata := adapter.ExtendContext(ctx)
108+
metadata.Outbound = h.Tag()
109+
metadata.Destination = destination
110+
h.logger.InfoContext(ctx, "outbound connection to ", destination)
111+
if !h.tlsEnabled {
112+
return h.client.DialContext(ctx, network, destination)
113+
}
114+
network = N.NetworkName(network)
115+
switch network {
116+
case N.NetworkTCP:
117+
case N.NetworkUDP:
118+
return nil, os.ErrInvalid
119+
default:
120+
return nil, E.Extend(N.ErrUnknownNetwork, network)
121+
}
122+
conn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
123+
if err != nil {
124+
return nil, err
125+
}
126+
// TLS 握手完成后按 ALPN 协商结果分流:
127+
// 协商到 h2 → HTTP/2 CONNECT;否则保持原有 HTTP/1.1 CONNECT 行为。
128+
// 注意:std tls / uTLS / REALITY 包装连接均暴露
129+
// ConnectionState() stdtls.ConnectionState,这里不对具体类型断言。
130+
if stateConn, ok := conn.(interface {
131+
ConnectionState() stdtls.ConnectionState
132+
}); ok && stateConn.ConnectionState().NegotiatedProtocol == "h2" {
133+
return h.dialH2(ctx, conn, destination)
134+
}
135+
return h.dialHTTP1(conn, destination)
136+
}
137+
138+
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
139+
return nil, os.ErrInvalid
140+
}
141+
142+
// dialHTTP1 在已建立的 TLS 连接上执行 HTTP/1.1 CONNECT,
143+
// 行为与 sing protocol/http Client 保持一致。
144+
func (h *Outbound) dialHTTP1(conn net.Conn, destination M.Socksaddr) (net.Conn, error) {
145+
request := &stdhttp.Request{
146+
Method: stdhttp.MethodConnect,
147+
Header: stdhttp.Header{
148+
"Proxy-Connection": []string{"Keep-Alive"},
149+
},
150+
}
151+
if h.host != "" && h.host != destination.Fqdn {
152+
if h.path != "" {
153+
_ = conn.Close()
154+
return nil, E.New("Host header and path are not allowed at the same time")
155+
}
156+
request.Host = h.host
157+
request.URL = &url.URL{Opaque: destination.String()}
158+
} else {
159+
request.URL = &url.URL{Host: destination.String()}
160+
}
161+
if h.path != "" {
162+
err := sHTTP.URLSetPath(request.URL, h.path)
163+
if err != nil {
164+
_ = conn.Close()
165+
return nil, err
166+
}
167+
}
168+
for key, valueList := range h.headers {
169+
request.Header.Set(key, valueList[0])
170+
for _, value := range valueList[1:] {
171+
request.Header.Add(key, value)
172+
}
173+
}
174+
if h.username != "" {
175+
auth := h.username + ":" + h.password
176+
request.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
177+
}
178+
err := request.Write(conn)
179+
if err != nil {
180+
conn.Close()
181+
return nil, err
182+
}
183+
reader := std_bufio.NewReader(conn)
184+
response, err := stdhttp.ReadResponse(reader, request)
185+
if err != nil {
186+
conn.Close()
187+
return nil, err
188+
}
189+
if response.StatusCode == stdhttp.StatusOK {
190+
if reader.Buffered() > 0 {
191+
buffer := buf.NewSize(reader.Buffered())
192+
_, err = buffer.ReadFullFrom(reader, buffer.FreeLen())
193+
if err != nil {
194+
conn.Close()
195+
return nil, err
196+
}
197+
conn = bufio.NewCachedConn(conn, buffer)
198+
}
199+
return conn, nil
200+
}
201+
conn.Close()
202+
switch response.StatusCode {
203+
case stdhttp.StatusProxyAuthRequired:
204+
return nil, E.New("authentication required")
205+
case stdhttp.StatusMethodNotAllowed:
206+
return nil, E.New("method not allowed")
207+
default:
208+
return nil, E.New("unexpected status: ", response.Status)
209+
}
210+
}
211+
212+
// dialH2 在已协商 ALPN=h2 的 TLS 连接上执行 HTTP/2 CONNECT,
213+
// 上行流以 io.Pipe 作为请求 Body,响应 Body 即下行流。
214+
func (h *Outbound) dialH2(ctx context.Context, conn net.Conn, destination M.Socksaddr) (net.Conn, error) {
215+
transport := &http2.Transport{
216+
// 复用已握手完成的连接,不再让 Transport 自己拨号。
217+
// (设置 DialTLSContext 时 Transport 不再自行校验 ALPN。)
218+
DialTLSContext: func(ctx context.Context, network, addr string, cfg *stdtls.Config) (net.Conn, error) {
219+
return conn, nil
220+
},
221+
}
222+
pipeReader, pipeWriter := io.Pipe()
223+
request := &stdhttp.Request{
224+
Method: stdhttp.MethodConnect,
225+
URL: &url.URL{Scheme: "https", Host: destination.String()},
226+
Host: destination.String(),
227+
Header: make(stdhttp.Header),
228+
Body: pipeReader,
229+
}
230+
if h.host != "" && h.host != destination.Fqdn {
231+
request.Host = h.host
232+
}
233+
for key, valueList := range h.headers {
234+
// 过滤 HTTP/1.1 连接级头(RFC 7540 8.1.2.2),h2 中不合法。
235+
switch strings.ToLower(key) {
236+
case "connection", "proxy-connection", "keep-alive", "upgrade", "transfer-encoding":
237+
continue
238+
}
239+
request.Header.Set(key, valueList[0])
240+
for _, value := range valueList[1:] {
241+
request.Header.Add(key, value)
242+
}
243+
}
244+
if h.username != "" || h.password != "" {
245+
auth := h.username + ":" + h.password
246+
request.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
247+
}
248+
response, err := transport.RoundTrip(request.WithContext(ctx))
249+
if err != nil {
250+
pipeReader.Close()
251+
pipeWriter.Close()
252+
conn.Close()
253+
return nil, err
254+
}
255+
if response.StatusCode != stdhttp.StatusOK {
256+
response.Body.Close()
257+
pipeReader.Close()
258+
pipeWriter.Close()
259+
conn.Close()
260+
switch response.StatusCode {
261+
case stdhttp.StatusProxyAuthRequired:
262+
return nil, E.New("authentication required")
263+
case stdhttp.StatusMethodNotAllowed:
264+
return nil, E.New("method not allowed")
265+
default:
266+
return nil, E.New("unexpected status: ", response.Status)
267+
}
268+
}
269+
return &h2TunnelConn{
270+
Conn: conn,
271+
reader: response.Body,
272+
writer: pipeWriter,
273+
}, nil
274+
}
275+
276+
// h2TunnelConn 将 h2 CONNECT 流适配为 net.Conn:
277+
// 读自响应 Body,写入 io.Pipe(作为请求 Body 上行)。
278+
type h2TunnelConn struct {
279+
net.Conn // LocalAddr/RemoteAddr 委托给底层 TLS 连接
280+
reader io.ReadCloser
281+
writer *io.PipeWriter
282+
closeOnce sync.Once
283+
}
284+
285+
func (c *h2TunnelConn) Read(b []byte) (int, error) { return c.reader.Read(b) }
286+
func (c *h2TunnelConn) Write(b []byte) (int, error) { return c.writer.Write(b) }
287+
288+
func (c *h2TunnelConn) Close() error {
289+
// 幂等:写关 = 上行流结束;关 reader = 取消下行流;底层连接一并关闭。
290+
c.closeOnce.Do(func() {
291+
c.writer.Close()
292+
c.reader.Close()
293+
c.Conn.Close()
294+
})
295+
return nil
296+
}
297+
298+
// h2 流无截止时间语义,按需 no-op。
299+
func (c *h2TunnelConn) SetDeadline(t time.Time) error { return nil }
300+
func (c *h2TunnelConn) SetReadDeadline(t time.Time) error { return nil }
301+
func (c *h2TunnelConn) SetWriteDeadline(t time.Time) error { return nil }

0 commit comments

Comments
 (0)