mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-22 16:07:49 +08:00
Update On Sun Apr 12 21:03:21 CEST 2026
This commit is contained in:
@@ -1327,3 +1327,4 @@ Update On Wed Apr 8 21:30:11 CEST 2026
|
||||
Update On Thu Apr 9 21:21:01 CEST 2026
|
||||
Update On Fri Apr 10 21:07:29 CEST 2026
|
||||
Update On Sat Apr 11 20:57:57 CEST 2026
|
||||
Update On Sun Apr 12 21:03:12 CEST 2026
|
||||
|
||||
@@ -315,10 +315,8 @@ func (w *Masque) run(ctx context.Context) error {
|
||||
|
||||
go func() {
|
||||
defer runCancel()
|
||||
buf := pool.Get(pool.UDPBufferSize)
|
||||
defer pool.Put(buf)
|
||||
for runCtx.Err() == nil {
|
||||
n, err := ipConn.ReadPacket(buf)
|
||||
buf, err := ipConn.ReadPacket()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
log.Errorln("[Masque](%s) connection closed while writing to IP connection: %v", w.name, err)
|
||||
@@ -327,7 +325,7 @@ func (w *Masque) run(ctx context.Context) error {
|
||||
log.Warnln("[Masque](%s) error reading from IP connection: %v, continuing...", w.name, err)
|
||||
continue
|
||||
}
|
||||
if _, err := w.tunDevice.Write([][]byte{buf[:n]}, 0); err != nil {
|
||||
if _, err := w.tunDevice.Write([][]byte{buf}, 0); err != nil {
|
||||
log.Errorln("[Masque](%s) error writing to TUN device: %v", w.name, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -129,3 +129,52 @@ func TestConvertsV2RayMieruFragment(t *testing.T) {
|
||||
assert.Len(t, proxies, 1)
|
||||
assert.Equal(t, "myproxy:443/TCP", proxies[0]["name"])
|
||||
}
|
||||
|
||||
func TestConvertsV2RayVlessRealityVisionTCPWithoutHeaderType(t *testing.T) {
|
||||
vlessTest := "vless://a1b2c3d4-eacc-4433-981b-7e5f9a8b@142.98.76.54:34888?encryption=none&security=reality&type=tcp&sni=github.io&fp=chrome&pbk=TifX9kL2mPqRsTuVwXyZ_JdUWw&sid=6ba85179f3a2b4c5&flow=xtls-rprx-vision#My-VLESS-Reality-Vision"
|
||||
|
||||
proxies, err := ConvertsV2Ray([]byte(vlessTest))
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, proxies, 1)
|
||||
assert.Equal(t, "tcp", proxies[0]["network"])
|
||||
assert.Equal(t, "xtls-rprx-vision", proxies[0]["flow"])
|
||||
assert.Equal(t, "none", proxies[0]["encryption"])
|
||||
assert.Equal(t, "github.io", proxies[0]["servername"])
|
||||
assert.NotContains(t, proxies[0], "http-opts")
|
||||
assert.NotContains(t, proxies[0], "h2-opts")
|
||||
}
|
||||
|
||||
func TestConvertsV2RayVlessTCPHTTPHeaderType(t *testing.T) {
|
||||
vlessTest := "vless://uuid@example.com:443?security=tls&type=tcp&headerType=http&host=cdn.example.com&path=%2Fedge&method=POST#vless-http"
|
||||
|
||||
proxies, err := ConvertsV2Ray([]byte(vlessTest))
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, proxies, 1)
|
||||
assert.Equal(t, "http", proxies[0]["network"])
|
||||
assert.Equal(t, map[string]any{
|
||||
"method": "POST",
|
||||
"path": []string{"/edge"},
|
||||
"headers": map[string]any{
|
||||
"Host": []string{"cdn.example.com"},
|
||||
},
|
||||
}, proxies[0]["http-opts"])
|
||||
assert.NotContains(t, proxies[0], "h2-opts")
|
||||
}
|
||||
|
||||
func TestConvertsV2RayVlessHTTPTransportUsesH2Opts(t *testing.T) {
|
||||
vlessTest := "vless://uuid@example.com:443?security=tls&type=http&host=cdn.example.com&path=%2Fgrpc#vless-h2"
|
||||
|
||||
proxies, err := ConvertsV2Ray([]byte(vlessTest))
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, proxies, 1)
|
||||
assert.Equal(t, "h2", proxies[0]["network"])
|
||||
assert.Equal(t, map[string]any{
|
||||
"host": []string{"cdn.example.com"},
|
||||
"path": []string{"/grpc"},
|
||||
"headers": map[string]any{},
|
||||
}, proxies[0]["h2-opts"])
|
||||
assert.NotContains(t, proxies[0], "http-opts")
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m
|
||||
network = "tcp"
|
||||
}
|
||||
fakeType := strings.ToLower(query.Get("headerType"))
|
||||
if fakeType == "http" {
|
||||
if network == "tcp" && fakeType == "http" {
|
||||
network = "http"
|
||||
} else if network == "http" {
|
||||
network = "h2"
|
||||
@@ -71,27 +71,26 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m
|
||||
proxy["network"] = network
|
||||
switch network {
|
||||
case "tcp":
|
||||
if fakeType != "none" {
|
||||
headers := make(map[string]any)
|
||||
httpOpts := make(map[string]any)
|
||||
httpOpts["path"] = []string{"/"}
|
||||
case "http":
|
||||
headers := make(map[string]any)
|
||||
httpOpts := make(map[string]any)
|
||||
httpOpts["path"] = []string{"/"}
|
||||
|
||||
if host := query.Get("host"); host != "" {
|
||||
headers["Host"] = []string{host}
|
||||
}
|
||||
|
||||
if method := query.Get("method"); method != "" {
|
||||
httpOpts["method"] = method
|
||||
}
|
||||
|
||||
if path := query.Get("path"); path != "" {
|
||||
httpOpts["path"] = []string{path}
|
||||
}
|
||||
httpOpts["headers"] = headers
|
||||
proxy["http-opts"] = httpOpts
|
||||
if host := query.Get("host"); host != "" {
|
||||
headers["Host"] = []string{host}
|
||||
}
|
||||
|
||||
case "http":
|
||||
if method := query.Get("method"); method != "" {
|
||||
httpOpts["method"] = method
|
||||
}
|
||||
|
||||
if path := query.Get("path"); path != "" {
|
||||
httpOpts["path"] = []string{path}
|
||||
}
|
||||
httpOpts["headers"] = headers
|
||||
proxy["http-opts"] = httpOpts
|
||||
|
||||
case "h2":
|
||||
headers := make(map[string]any)
|
||||
h2Opts := make(map[string]any)
|
||||
h2Opts["path"] = []string{"/"}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ require (
|
||||
github.com/metacubex/blake3 v0.1.0
|
||||
github.com/metacubex/chacha v0.1.5
|
||||
github.com/metacubex/chi v0.1.0
|
||||
github.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727
|
||||
github.com/metacubex/connect-ip-go v0.0.0-20260412152424-e1625567920a
|
||||
github.com/metacubex/cpu v0.1.1
|
||||
github.com/metacubex/edwards25519 v1.2.0
|
||||
github.com/metacubex/fswatch v0.1.1
|
||||
|
||||
+2
-2
@@ -87,8 +87,8 @@ github.com/metacubex/chacha v0.1.5 h1:fKWMb/5c7ZrY8Uoqi79PPFxl+qwR7X/q0OrsAubyX2
|
||||
github.com/metacubex/chacha v0.1.5/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
github.com/metacubex/chi v0.1.0 h1:rjNDyDj50nRpicG43CNkIw4ssiCbmDL8d7wJXKlUCsg=
|
||||
github.com/metacubex/chi v0.1.0/go.mod h1:zM5u5oMQt8b2DjvDHvzadKrP6B2ztmasL1YHRMbVV+g=
|
||||
github.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727 h1:qbZQ0sO0bDBKPvTd/qNQK6513300WJ5GRsHnw3PO4Ho=
|
||||
github.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727/go.mod h1:xYC8Ik7/rN6no+vTRuWMEziGwm3brA0wNM/zZP9qhOQ=
|
||||
github.com/metacubex/connect-ip-go v0.0.0-20260412152424-e1625567920a h1:Ph5UfTWDsGruZ+v95Df1ycTflQFmpZBFg2LUvj2kx/M=
|
||||
github.com/metacubex/connect-ip-go v0.0.0-20260412152424-e1625567920a/go.mod h1:xYC8Ik7/rN6no+vTRuWMEziGwm3brA0wNM/zZP9qhOQ=
|
||||
github.com/metacubex/cpu v0.1.1 h1:rRV5HGmeuGzjiKI3hYbL0dCd0qGwM7VUtk4ICXD06mI=
|
||||
github.com/metacubex/cpu v0.1.1/go.mod h1:09VEt4dSRLR+bOA8l4w4NDuzGZ8n5dkMv7e8axgEeTU=
|
||||
github.com/metacubex/edwards25519 v1.2.0 h1:pIQZLBsjQgg3Nl/c86YYFEUAbL5qQRnPq4LrgIw0KK4=
|
||||
|
||||
@@ -150,7 +150,7 @@ type h2IpConn struct {
|
||||
closeErr error
|
||||
}
|
||||
|
||||
func (c *h2IpConn) ReadPacket(b []byte) (n int, err error) {
|
||||
func (c *h2IpConn) ReadPacket() (b []byte, err error) {
|
||||
start:
|
||||
data, err := c.str.ReceiveDatagram(context.Background())
|
||||
if err != nil {
|
||||
@@ -161,16 +161,16 @@ start:
|
||||
}()
|
||||
select {
|
||||
case <-c.closeChan:
|
||||
return 0, c.closeErr
|
||||
return nil, c.closeErr
|
||||
default:
|
||||
return 0, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := c.handleIncomingProxiedPacket(data); err != nil {
|
||||
log.Debugln("dropping proxied packet: %s", err)
|
||||
goto start
|
||||
}
|
||||
return copy(b, data), nil
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *h2IpConn) handleIncomingProxiedPacket(data []byte) error {
|
||||
|
||||
@@ -28,7 +28,7 @@ const (
|
||||
)
|
||||
|
||||
type IpConn interface {
|
||||
ReadPacket(b []byte) (n int, err error)
|
||||
ReadPacket() (b []byte, err error)
|
||||
WritePacket(b []byte) (icmp []byte, err error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ package vmess
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/textproto"
|
||||
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"net/url"
|
||||
|
||||
"github.com/metacubex/http"
|
||||
"github.com/metacubex/randv2"
|
||||
@@ -64,10 +63,12 @@ func (hc *httpConn) Write(b []byte) (int, error) {
|
||||
host = header[randv2.IntN(len(header))]
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("http://%s%s", net.JoinHostPort(host, "80"), path)
|
||||
req, err := http.NewRequest(utils.EmptyOr(hc.cfg.Method, http.MethodGet), u, bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
req := http.Request{
|
||||
Method: hc.cfg.Method, // default is GET
|
||||
Host: host,
|
||||
URL: &url.URL{Scheme: "http", Host: host, Path: path},
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(bytes.NewReader(b)),
|
||||
}
|
||||
for key, list := range hc.cfg.Headers {
|
||||
req.Header.Set(key, list[randv2.IntN(len(list))])
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 1,
|
||||
"latest": {
|
||||
"mihomo": "v1.19.23",
|
||||
"mihomo_alpha": "alpha-96a6962",
|
||||
"mihomo_alpha": "alpha-80072eb",
|
||||
"clash_rs": "v0.9.6",
|
||||
"clash_premium": "2023-09-05-gdcc8d87",
|
||||
"clash_rs_alpha": "0.9.6-alpha+sha.415b05e"
|
||||
@@ -69,5 +69,5 @@
|
||||
"linux-armv7hf": "clash-rs-armv7-unknown-linux-gnueabihf"
|
||||
}
|
||||
},
|
||||
"updated_at": "2026-04-10T22:24:00.490Z"
|
||||
"updated_at": "2026-04-11T22:23:29.929Z"
|
||||
}
|
||||
|
||||
@@ -315,10 +315,8 @@ func (w *Masque) run(ctx context.Context) error {
|
||||
|
||||
go func() {
|
||||
defer runCancel()
|
||||
buf := pool.Get(pool.UDPBufferSize)
|
||||
defer pool.Put(buf)
|
||||
for runCtx.Err() == nil {
|
||||
n, err := ipConn.ReadPacket(buf)
|
||||
buf, err := ipConn.ReadPacket()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
log.Errorln("[Masque](%s) connection closed while writing to IP connection: %v", w.name, err)
|
||||
@@ -327,7 +325,7 @@ func (w *Masque) run(ctx context.Context) error {
|
||||
log.Warnln("[Masque](%s) error reading from IP connection: %v, continuing...", w.name, err)
|
||||
continue
|
||||
}
|
||||
if _, err := w.tunDevice.Write([][]byte{buf[:n]}, 0); err != nil {
|
||||
if _, err := w.tunDevice.Write([][]byte{buf}, 0); err != nil {
|
||||
log.Errorln("[Masque](%s) error writing to TUN device: %v", w.name, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -129,3 +129,52 @@ func TestConvertsV2RayMieruFragment(t *testing.T) {
|
||||
assert.Len(t, proxies, 1)
|
||||
assert.Equal(t, "myproxy:443/TCP", proxies[0]["name"])
|
||||
}
|
||||
|
||||
func TestConvertsV2RayVlessRealityVisionTCPWithoutHeaderType(t *testing.T) {
|
||||
vlessTest := "vless://a1b2c3d4-eacc-4433-981b-7e5f9a8b@142.98.76.54:34888?encryption=none&security=reality&type=tcp&sni=github.io&fp=chrome&pbk=TifX9kL2mPqRsTuVwXyZ_JdUWw&sid=6ba85179f3a2b4c5&flow=xtls-rprx-vision#My-VLESS-Reality-Vision"
|
||||
|
||||
proxies, err := ConvertsV2Ray([]byte(vlessTest))
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, proxies, 1)
|
||||
assert.Equal(t, "tcp", proxies[0]["network"])
|
||||
assert.Equal(t, "xtls-rprx-vision", proxies[0]["flow"])
|
||||
assert.Equal(t, "none", proxies[0]["encryption"])
|
||||
assert.Equal(t, "github.io", proxies[0]["servername"])
|
||||
assert.NotContains(t, proxies[0], "http-opts")
|
||||
assert.NotContains(t, proxies[0], "h2-opts")
|
||||
}
|
||||
|
||||
func TestConvertsV2RayVlessTCPHTTPHeaderType(t *testing.T) {
|
||||
vlessTest := "vless://uuid@example.com:443?security=tls&type=tcp&headerType=http&host=cdn.example.com&path=%2Fedge&method=POST#vless-http"
|
||||
|
||||
proxies, err := ConvertsV2Ray([]byte(vlessTest))
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, proxies, 1)
|
||||
assert.Equal(t, "http", proxies[0]["network"])
|
||||
assert.Equal(t, map[string]any{
|
||||
"method": "POST",
|
||||
"path": []string{"/edge"},
|
||||
"headers": map[string]any{
|
||||
"Host": []string{"cdn.example.com"},
|
||||
},
|
||||
}, proxies[0]["http-opts"])
|
||||
assert.NotContains(t, proxies[0], "h2-opts")
|
||||
}
|
||||
|
||||
func TestConvertsV2RayVlessHTTPTransportUsesH2Opts(t *testing.T) {
|
||||
vlessTest := "vless://uuid@example.com:443?security=tls&type=http&host=cdn.example.com&path=%2Fgrpc#vless-h2"
|
||||
|
||||
proxies, err := ConvertsV2Ray([]byte(vlessTest))
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, proxies, 1)
|
||||
assert.Equal(t, "h2", proxies[0]["network"])
|
||||
assert.Equal(t, map[string]any{
|
||||
"host": []string{"cdn.example.com"},
|
||||
"path": []string{"/grpc"},
|
||||
"headers": map[string]any{},
|
||||
}, proxies[0]["h2-opts"])
|
||||
assert.NotContains(t, proxies[0], "http-opts")
|
||||
}
|
||||
|
||||
+18
-19
@@ -63,7 +63,7 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m
|
||||
network = "tcp"
|
||||
}
|
||||
fakeType := strings.ToLower(query.Get("headerType"))
|
||||
if fakeType == "http" {
|
||||
if network == "tcp" && fakeType == "http" {
|
||||
network = "http"
|
||||
} else if network == "http" {
|
||||
network = "h2"
|
||||
@@ -71,27 +71,26 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m
|
||||
proxy["network"] = network
|
||||
switch network {
|
||||
case "tcp":
|
||||
if fakeType != "none" {
|
||||
headers := make(map[string]any)
|
||||
httpOpts := make(map[string]any)
|
||||
httpOpts["path"] = []string{"/"}
|
||||
case "http":
|
||||
headers := make(map[string]any)
|
||||
httpOpts := make(map[string]any)
|
||||
httpOpts["path"] = []string{"/"}
|
||||
|
||||
if host := query.Get("host"); host != "" {
|
||||
headers["Host"] = []string{host}
|
||||
}
|
||||
|
||||
if method := query.Get("method"); method != "" {
|
||||
httpOpts["method"] = method
|
||||
}
|
||||
|
||||
if path := query.Get("path"); path != "" {
|
||||
httpOpts["path"] = []string{path}
|
||||
}
|
||||
httpOpts["headers"] = headers
|
||||
proxy["http-opts"] = httpOpts
|
||||
if host := query.Get("host"); host != "" {
|
||||
headers["Host"] = []string{host}
|
||||
}
|
||||
|
||||
case "http":
|
||||
if method := query.Get("method"); method != "" {
|
||||
httpOpts["method"] = method
|
||||
}
|
||||
|
||||
if path := query.Get("path"); path != "" {
|
||||
httpOpts["path"] = []string{path}
|
||||
}
|
||||
httpOpts["headers"] = headers
|
||||
proxy["http-opts"] = httpOpts
|
||||
|
||||
case "h2":
|
||||
headers := make(map[string]any)
|
||||
h2Opts := make(map[string]any)
|
||||
h2Opts["path"] = []string{"/"}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ require (
|
||||
github.com/metacubex/blake3 v0.1.0
|
||||
github.com/metacubex/chacha v0.1.5
|
||||
github.com/metacubex/chi v0.1.0
|
||||
github.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727
|
||||
github.com/metacubex/connect-ip-go v0.0.0-20260412152424-e1625567920a
|
||||
github.com/metacubex/cpu v0.1.1
|
||||
github.com/metacubex/edwards25519 v1.2.0
|
||||
github.com/metacubex/fswatch v0.1.1
|
||||
|
||||
+2
-2
@@ -87,8 +87,8 @@ github.com/metacubex/chacha v0.1.5 h1:fKWMb/5c7ZrY8Uoqi79PPFxl+qwR7X/q0OrsAubyX2
|
||||
github.com/metacubex/chacha v0.1.5/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
github.com/metacubex/chi v0.1.0 h1:rjNDyDj50nRpicG43CNkIw4ssiCbmDL8d7wJXKlUCsg=
|
||||
github.com/metacubex/chi v0.1.0/go.mod h1:zM5u5oMQt8b2DjvDHvzadKrP6B2ztmasL1YHRMbVV+g=
|
||||
github.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727 h1:qbZQ0sO0bDBKPvTd/qNQK6513300WJ5GRsHnw3PO4Ho=
|
||||
github.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727/go.mod h1:xYC8Ik7/rN6no+vTRuWMEziGwm3brA0wNM/zZP9qhOQ=
|
||||
github.com/metacubex/connect-ip-go v0.0.0-20260412152424-e1625567920a h1:Ph5UfTWDsGruZ+v95Df1ycTflQFmpZBFg2LUvj2kx/M=
|
||||
github.com/metacubex/connect-ip-go v0.0.0-20260412152424-e1625567920a/go.mod h1:xYC8Ik7/rN6no+vTRuWMEziGwm3brA0wNM/zZP9qhOQ=
|
||||
github.com/metacubex/cpu v0.1.1 h1:rRV5HGmeuGzjiKI3hYbL0dCd0qGwM7VUtk4ICXD06mI=
|
||||
github.com/metacubex/cpu v0.1.1/go.mod h1:09VEt4dSRLR+bOA8l4w4NDuzGZ8n5dkMv7e8axgEeTU=
|
||||
github.com/metacubex/edwards25519 v1.2.0 h1:pIQZLBsjQgg3Nl/c86YYFEUAbL5qQRnPq4LrgIw0KK4=
|
||||
|
||||
@@ -150,7 +150,7 @@ type h2IpConn struct {
|
||||
closeErr error
|
||||
}
|
||||
|
||||
func (c *h2IpConn) ReadPacket(b []byte) (n int, err error) {
|
||||
func (c *h2IpConn) ReadPacket() (b []byte, err error) {
|
||||
start:
|
||||
data, err := c.str.ReceiveDatagram(context.Background())
|
||||
if err != nil {
|
||||
@@ -161,16 +161,16 @@ start:
|
||||
}()
|
||||
select {
|
||||
case <-c.closeChan:
|
||||
return 0, c.closeErr
|
||||
return nil, c.closeErr
|
||||
default:
|
||||
return 0, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := c.handleIncomingProxiedPacket(data); err != nil {
|
||||
log.Debugln("dropping proxied packet: %s", err)
|
||||
goto start
|
||||
}
|
||||
return copy(b, data), nil
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *h2IpConn) handleIncomingProxiedPacket(data []byte) error {
|
||||
|
||||
@@ -28,7 +28,7 @@ const (
|
||||
)
|
||||
|
||||
type IpConn interface {
|
||||
ReadPacket(b []byte) (n int, err error)
|
||||
ReadPacket() (b []byte, err error)
|
||||
WritePacket(b []byte) (icmp []byte, err error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ package vmess
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/textproto"
|
||||
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"net/url"
|
||||
|
||||
"github.com/metacubex/http"
|
||||
"github.com/metacubex/randv2"
|
||||
@@ -64,10 +63,12 @@ func (hc *httpConn) Write(b []byte) (int, error) {
|
||||
host = header[randv2.IntN(len(header))]
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("http://%s%s", net.JoinHostPort(host, "80"), path)
|
||||
req, err := http.NewRequest(utils.EmptyOr(hc.cfg.Method, http.MethodGet), u, bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
req := http.Request{
|
||||
Method: hc.cfg.Method, // default is GET
|
||||
Host: host,
|
||||
URL: &url.URL{Scheme: "http", Host: host, Path: path},
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(bytes.NewReader(b)),
|
||||
}
|
||||
for key, list := range hc.cfg.Headers {
|
||||
req.Header.Set(key, list[randv2.IntN(len(list))])
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -257,6 +257,11 @@ select[multiple="multiple"] {
|
||||
margin: 5em auto;
|
||||
}
|
||||
|
||||
.alert-message {
|
||||
transform: none;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.alert-message>h4 {
|
||||
font-size: 110%;
|
||||
font-weight: bold;
|
||||
@@ -270,6 +275,11 @@ select[multiple="multiple"] {
|
||||
padding: .3rem .6rem;
|
||||
}
|
||||
|
||||
#maincontent > .alert-message {
|
||||
position: fixed;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.container .alert,
|
||||
.container .alert-message {
|
||||
margin-left: 0;
|
||||
@@ -1431,7 +1441,6 @@ body:not(.Interfaces) .cbi-rowstyle-2:first-child {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 0;
|
||||
color: #8898aa;
|
||||
background-color: currentColor;
|
||||
mask-image: var(--dropdown-arrow-icon);
|
||||
mask-repeat: no-repeat;
|
||||
@@ -1472,6 +1481,10 @@ body:not(.Interfaces) .cbi-rowstyle-2:first-child {
|
||||
border-left: thin solid #ccc;
|
||||
}
|
||||
|
||||
.cbi-dropdown>ul>li[display="0"] {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.cbi-dropdown[empty]>ul {
|
||||
max-width: 1px;
|
||||
}
|
||||
@@ -2493,7 +2506,6 @@ pre.command-output {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
.alert-message.spinning,
|
||||
.modal.alert-message.spinning {
|
||||
position: relative;
|
||||
|
||||
@@ -1069,10 +1069,10 @@ input,
|
||||
.developer-container {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.config-upload-content{
|
||||
background: #1e1e1e !important;
|
||||
.config-upload-content{
|
||||
background: #1e1e1e !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cbi-tabmenu::-webkit-scrollbar-thumb {
|
||||
@@ -1220,3 +1220,15 @@ input,
|
||||
background-color: rgb(112, 112, 112);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
[data-page="admin-system-admin-repokeys"] #key-input {
|
||||
background-color: #1e1e1e;
|
||||
color: #ccc;
|
||||
border: 1px solid #3c3c3c !important;
|
||||
}
|
||||
|
||||
[data-page="admin-system-package-manager"] #modal_overlay textarea{
|
||||
background-color: #1e1e1e;
|
||||
color: #ccc;
|
||||
border: 1px solid #3c3c3c !important;
|
||||
}
|
||||
@@ -760,6 +760,15 @@
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.oc .sub-card:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.oc .myip-ip-item:hover,
|
||||
.oc .myip-check-item:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.oc .main-cards-container {
|
||||
margin: 0 !important;
|
||||
gap: 0 !important;
|
||||
@@ -1022,4 +1031,4 @@ div[style*="display:grid;grid-template-columns:repeat"] {
|
||||
#mwan3-service-status > .alert-message {
|
||||
position: static;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
<!-- Security -->
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
<meta http-equiv="X-Frame-Options" content="SAMEORIGIN">
|
||||
<!-- <meta http-equiv="X-Frame-Options" content="SAMEORIGIN"> -->
|
||||
<meta http-equiv="X-XSS-Protection" content="1; mode=block">
|
||||
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
||||
|
||||
|
||||
@@ -823,7 +823,7 @@ for k, v in pairs(nodes_table) do
|
||||
socks:value(v.id, v["remark"])
|
||||
socks.group[#socks.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
else
|
||||
elseif v.protocol ~= "_shunt" then
|
||||
socks:value(v.id, v["remark"])
|
||||
socks.group[#socks.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
|
||||
+6
-4
@@ -102,10 +102,12 @@ o.widget = "checkbox"
|
||||
o.template = appname .. "/cbi/nodes_multivalue"
|
||||
o.group = {}
|
||||
for i, v in pairs(nodes_table) do
|
||||
o:value(v.id, v.remark)
|
||||
o.group[#o.group+1] = v.group or ""
|
||||
socks_node:value(v.id, v["remark"])
|
||||
socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
if v.protocol ~= "_shunt" then
|
||||
o:value(v.id, v.remark)
|
||||
o.group[#o.group+1] = v.group or ""
|
||||
socks_node:value(v.id, v["remark"])
|
||||
socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
end
|
||||
-- 读取旧 DynamicList
|
||||
function o.cfgvalue(self, section)
|
||||
|
||||
@@ -1521,14 +1521,28 @@ function gen_config(var)
|
||||
useSystemHosts = true
|
||||
}
|
||||
|
||||
local _direct_dns = {
|
||||
tag = "dns-global-direct",
|
||||
queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
|
||||
}
|
||||
local _direct_dns = {}
|
||||
|
||||
direct_dns_udp_server = (direct_dns_udp_server and direct_dns_udp_server ~= "") and direct_dns_udp_server or nil
|
||||
|
||||
if direct_dns_udp_server or direct_dns_tcp_server then
|
||||
_direct_dns.tag = "dns-global-direct"
|
||||
_direct_dns.queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
|
||||
|
||||
if direct_dns_udp_server then
|
||||
local port = tonumber(direct_dns_port) or 53
|
||||
_direct_dns.port = port
|
||||
_direct_dns.address = direct_dns_udp_server
|
||||
elseif direct_dns_tcp_server then
|
||||
local port = tonumber(direct_dns_port) or 53
|
||||
_direct_dns.address = "tcp://" .. direct_dns_tcp_server .. ":" .. port
|
||||
end
|
||||
|
||||
if COMMON.default_outbound_tag == "direct" then
|
||||
table.insert(dns.servers, _direct_dns)
|
||||
end
|
||||
end
|
||||
if dns_listen_port and next(_direct_dns) then
|
||||
local domain = {}
|
||||
local nodes_domain_text = sys.exec([[uci show passwall | sed -n "s/.*\.\(address\|download_address\)='\([^']*\)'/\2/p" | sort -u]])
|
||||
string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w)
|
||||
@@ -1543,19 +1557,6 @@ function gen_config(var)
|
||||
domain = domain
|
||||
})
|
||||
end
|
||||
|
||||
if direct_dns_udp_server then
|
||||
local port = tonumber(direct_dns_port) or 53
|
||||
_direct_dns.port = port
|
||||
_direct_dns.address = direct_dns_udp_server
|
||||
elseif direct_dns_tcp_server then
|
||||
local port = tonumber(direct_dns_port) or 53
|
||||
_direct_dns.address = "tcp://" .. direct_dns_tcp_server .. ":" .. port
|
||||
end
|
||||
|
||||
if COMMON.default_outbound_tag == "direct" then
|
||||
table.insert(dns.servers, _direct_dns)
|
||||
end
|
||||
end
|
||||
|
||||
local _remote_dns = {}
|
||||
@@ -1649,8 +1650,7 @@ function gen_config(var)
|
||||
protocol = "dokodemo-door",
|
||||
tag = "dns-in",
|
||||
settings = {
|
||||
address = remote_dns_udp_server or remote_dns_tcp_server,
|
||||
port = tonumber(remote_dns_udp_port) or tonumber(remote_dns_tcp_port),
|
||||
address = "0.0.0.0",
|
||||
network = "tcp,udp"
|
||||
}
|
||||
})
|
||||
@@ -1796,14 +1796,12 @@ function gen_config(var)
|
||||
for line, _ in pairs(GLOBAL.DNS_HOSTNAME) do
|
||||
table.insert(hostname, line)
|
||||
end
|
||||
table.insert(dns.servers, 2, {
|
||||
tag = "bootstrap",
|
||||
address = "223.5.5.5",
|
||||
queryStrategy = "UseIPv4",
|
||||
domains = hostname
|
||||
})
|
||||
local new_dns_server = next(_direct_dns) and api.clone(_direct_dns) or { address = "localhost" }
|
||||
new_dns_server.tag = "dns-in-bootstrap"
|
||||
new_dns_server.domains = hostname
|
||||
table.insert(dns.servers, 2, new_dns_server)
|
||||
table.insert(routing.rules, idx, {
|
||||
inboundTag = { "bootstrap" },
|
||||
inboundTag = { "dns-in-bootstrap" },
|
||||
outboundTag = "direct"
|
||||
})
|
||||
end
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
#!/bin/sh
|
||||
|
||||
[[ "$ACTION" == "ifup" && $(uci get "passwall.@global[0].enabled") == "1" ]] && [ -f /var/lock/passwall_ready.lock ] && {
|
||||
default_device=$(ip route | grep default | awk -F 'dev ' '{print $2}' | awk '{print $1}')
|
||||
[ "$default_device" == "$DEVICE" ] && {
|
||||
[ "$ACTION" = "ifup" ] || [ "$ACTION" = "ifupdate" ] || exit 0
|
||||
|
||||
[ "$(uci get passwall.@global[0].enabled 2>/dev/null)" = "1" ] && [ -f /var/lock/passwall_ready.lock ] && {
|
||||
|
||||
default_device=$(ip route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1)
|
||||
default6_device=$(ip -6 route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1)
|
||||
( [ "$default_device" = "$DEVICE" ] || [ "$default6_device" = "$DEVICE" ] ) && {
|
||||
|
||||
if [ "$ACTION" = "ifupdate" ]; then
|
||||
FWI=$(uci -q get firewall.passwall.path 2>/dev/null)
|
||||
[ -n "$FWI" ] && [ -f "$FWI" ] && sh "$FWI" >/dev/null 2>&1 &
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LOCK_FILE_DIR=/var/lock
|
||||
[ ! -d ${LOCK_FILE_DIR} ] && mkdir -p ${LOCK_FILE_DIR}
|
||||
LOCK_FILE="${LOCK_FILE_DIR}/passwall_ifup.lock"
|
||||
|
||||
@@ -2051,16 +2051,7 @@ get_config() {
|
||||
REMOTE_DNS_QUERY_STRATEGY="UseIP"
|
||||
[ "$FILTER_PROXY_IPV6" = "1" ] && REMOTE_DNS_QUERY_STRATEGY="UseIPv4"
|
||||
DNSMASQ_FILTER_PROXY_IPV6=${FILTER_PROXY_IPV6}
|
||||
|
||||
RESOLVFILE=/tmp/resolv.conf.d/resolv.conf.auto
|
||||
[ -f "${RESOLVFILE}" ] && [ -s "${RESOLVFILE}" ] || RESOLVFILE=/tmp/resolv.conf.auto
|
||||
|
||||
ISP_DNS=$(cat $RESOLVFILE 2>/dev/null | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | grep -v -E '^(0\.0\.0\.0|127\.0\.0\.1)$' | awk '!seen[$0]++')
|
||||
ISP_DNS6=$(cat $RESOLVFILE 2>/dev/null | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | awk -F % '{print $1}' | awk -F " " '{print $2}' | grep -v -Fx ::1 | grep -v -Fx :: | awk '!seen[$0]++')
|
||||
|
||||
DEFAULT_DNS=$(uci show dhcp.@dnsmasq[0] | grep "\.server=" | awk -F '=' '{print $2}' | sed "s/'//g" | tr ' ' '\n' | grep -v "\/" | sed ':label;N;s/\n/,/;b label')
|
||||
[ -z "${DEFAULT_DNS}" ] && [ "$(echo $ISP_DNS | tr ' ' '\n' | wc -l)" -ge 1 ] && DEFAULT_DNS=$(echo -n $ISP_DNS | tr ' ' '\n' | tr '\n' ',' | sed 's/,$//')
|
||||
LOCAL_DNS="${DEFAULT_DNS:-119.29.29.29,223.5.5.5}"
|
||||
IPT_APPEND_DNS=${LOCAL_DNS}
|
||||
|
||||
DNSMASQ_CONF_DIR=/tmp/dnsmasq.d
|
||||
@@ -2088,6 +2079,20 @@ get_config() {
|
||||
}
|
||||
}
|
||||
|
||||
get_local_dns() {
|
||||
RESOLVFILE=/tmp/resolv.conf.d/resolv.conf.auto
|
||||
[ -f "${RESOLVFILE}" ] && [ -s "${RESOLVFILE}" ] || RESOLVFILE=/tmp/resolv.conf.auto
|
||||
|
||||
ISP_DNS=$(cat $RESOLVFILE 2>/dev/null | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | grep -v -E '^(0\.0\.0\.0|127\.0\.0\.1)$' | awk '!seen[$0]++')
|
||||
ISP_DNS6=$(cat $RESOLVFILE 2>/dev/null | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | awk -F % '{print $1}' | awk -F " " '{print $2}' | grep -v -Fx ::1 | grep -v -Fx :: | awk '!seen[$0]++')
|
||||
|
||||
DEFAULT_DNS=$(uci show dhcp.@dnsmasq[0] | grep "\.server=" | awk -F '=' '{print $2}' | sed "s/'//g" | tr ' ' '\n' | grep -v "\/" | sed ':label;N;s/\n/,/;b label')
|
||||
[ -z "${DEFAULT_DNS}" ] && [ "$(echo $ISP_DNS | tr ' ' '\n' | wc -l)" -ge 1 ] && DEFAULT_DNS=$(echo -n $ISP_DNS | tr ' ' '\n' | tr '\n' ',' | sed 's/,$//')
|
||||
LOCAL_DNS="${DEFAULT_DNS:-119.29.29.29,223.5.5.5}"
|
||||
}
|
||||
|
||||
get_local_dns
|
||||
|
||||
arg1=$1
|
||||
shift
|
||||
case $arg1 in
|
||||
|
||||
@@ -50,8 +50,8 @@ test_node() {
|
||||
[ -n "${_type}" ] && {
|
||||
local _tmp_port=$(get_new_port 48800 tcp,udp)
|
||||
NO_REC_PROCESS=1 $APP_FILE run_socks flag="test_node_${node_id}" node=${node_id} bind=127.0.0.1 socks_port=${_tmp_port} config_file=test_node_${node_id}.json
|
||||
sleep 2s
|
||||
local curlx="socks5h://127.0.0.1:${_tmp_port}"
|
||||
sleep 1s
|
||||
local _proxy_status=$(test_url "${probe_url}" ${retry_num} ${connect_timeout} "-x $curlx")
|
||||
# 结束 SS 插件进程
|
||||
local pid_file="/tmp/etc/${CONFIG}/test_node_${node_id}_plugin.pid"
|
||||
|
||||
@@ -1867,7 +1867,7 @@ local function update_node(manual)
|
||||
if domain_strategy then
|
||||
if vvv == "sing-box" then
|
||||
local map = { UseIPv4v6 = "prefer_ipv4", UseIPv6v4 = "prefer_ipv6", UseIPv4 = "ipv4_only", UseIPv6 = "ipv6_only" }
|
||||
domain_strategy = map[domain_strategy or ""]
|
||||
domain_strategy = map[domain_strategy] or ""
|
||||
end
|
||||
uci:set(appname, cfgid, "domain_strategy", domain_strategy)
|
||||
end
|
||||
@@ -2087,7 +2087,7 @@ local execute = function()
|
||||
domain_resolver_dns = value.domain_resolver_dns
|
||||
domain_resolver_dns_https = value.domain_resolver_dns_https
|
||||
local map = { UseIPv4v6 = 1, UseIPv6v4 = 1, UseIPv4 = 1, UseIPv6 = 1 }
|
||||
domain_strategy = (not map[value.domain_strategy or ""]) and value.domain_strategy or nil
|
||||
domain_strategy = (value.domain_strategy and map[value.domain_strategy]) and value.domain_strategy or nil
|
||||
|
||||
-- 订阅组链式代理
|
||||
local function valid_chain_node(node)
|
||||
|
||||
@@ -60,7 +60,7 @@ url_test_node() {
|
||||
NO_REC_PROCESS=1 /usr/share/${CONFIG}/app.sh run_socks flag="url_test_${node_id}" node=${node_id} bind=127.0.0.1 socks_port=${_tmp_port} config_file=url_test_${node_id}.json
|
||||
local curlx="socks5h://127.0.0.1:${_tmp_port}"
|
||||
fi
|
||||
sleep 1s
|
||||
sleep 2s
|
||||
local probeUrl=$(config_t_get global_other url_test_url https://www.google.com/generate_204)
|
||||
result=$(curl --connect-timeout 3 --max-time 5 -o /dev/null -I -skL -w "%{http_code}:%{time_pretransfer}" -x ${curlx} "${probeUrl}")
|
||||
# 结束 SS 插件进程
|
||||
|
||||
@@ -357,6 +357,7 @@ set_cache_var() {
|
||||
shift 1
|
||||
local val="$@"
|
||||
[ -n "${key}" ] && [ -n "${val}" ] && {
|
||||
[ ! -d $TMP_PATH ] && mkdir -p $TMP_PATH
|
||||
sed -i "/${key}=/d" $TMP_PATH/var >/dev/null 2>&1
|
||||
echo "${key}=\"${val}\"" >> $TMP_PATH/var
|
||||
eval ${key}=\"${val}\"
|
||||
|
||||
+3
-3
@@ -131,7 +131,7 @@ jobs:
|
||||
./build-release -t ${{ matrix.platform.target }} $compile_features $compile_compress $compile_nightly $compile_cargo_flags
|
||||
|
||||
- name: Upload Github Assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
./build/build-host-release -t ${{ matrix.target }}
|
||||
|
||||
- name: Upload Github Assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -205,7 +205,7 @@ jobs:
|
||||
pwsh ./build/build-host-release.ps1 "full winservice"
|
||||
|
||||
- name: Upload Github Assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
Generated
+5
-5
@@ -2656,9 +2656,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
|
||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||
dependencies = [
|
||||
"chacha20 0.10.0",
|
||||
"getrandom 0.4.1",
|
||||
@@ -3197,7 +3197,7 @@ dependencies = [
|
||||
"notify",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"rand 0.10.0",
|
||||
"rand 0.10.1",
|
||||
"sealed",
|
||||
"sendfd",
|
||||
"serde",
|
||||
@@ -3263,7 +3263,7 @@ dependencies = [
|
||||
"log4rs",
|
||||
"mimalloc",
|
||||
"qrcode",
|
||||
"rand 0.10.0",
|
||||
"rand 0.10.1",
|
||||
"reqwest",
|
||||
"rpassword",
|
||||
"rpmalloc",
|
||||
@@ -3315,7 +3315,7 @@ dependencies = [
|
||||
"native-tls",
|
||||
"nix",
|
||||
"pin-project",
|
||||
"rand 0.10.0",
|
||||
"rand 0.10.1",
|
||||
"regex",
|
||||
"rocksdb",
|
||||
"rustls-native-certs",
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
//! HTTP CONNECT proxy client for tunneling through HTTP proxies
|
||||
|
||||
use std::io;
|
||||
|
||||
use base64::{Engine, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||
use shadowsocks::relay::socks5::Address;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
|
||||
/// HTTP CONNECT proxy client for tunneling through HTTP proxies
|
||||
pub struct HttpConnectClient;
|
||||
|
||||
impl HttpConnectClient {
|
||||
/// Perform HTTP CONNECT negotiation on an existing stream
|
||||
pub async fn conduct_connect<S>(
|
||||
stream: &mut S,
|
||||
target: &Address,
|
||||
proxy_auth: Option<(&str, &str)>,
|
||||
) -> io::Result<()>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let authority = match target {
|
||||
Address::SocketAddress(sa) => {
|
||||
if sa.is_ipv6() {
|
||||
format!("[{}]:{}", sa.ip(), sa.port())
|
||||
} else {
|
||||
sa.to_string()
|
||||
}
|
||||
}
|
||||
Address::DomainNameAddress(host, port) => format!("{host}:{port}"),
|
||||
};
|
||||
|
||||
let mut request = format!("CONNECT {authority} HTTP/1.1\r\nHost: {authority}\r\nProxy-Connection: Keep-Alive\r\n");
|
||||
|
||||
if let Some((username, password)) = proxy_auth {
|
||||
let encoded = BASE64_STANDARD.encode(format!("{username}:{password}"));
|
||||
request.push_str(&format!("Proxy-Authorization: Basic {encoded}\r\n"));
|
||||
}
|
||||
|
||||
request.push_str("\r\n");
|
||||
stream.write_all(request.as_bytes()).await?;
|
||||
|
||||
let mut response = Vec::with_capacity(1024);
|
||||
let mut buf = [0u8; 1024];
|
||||
|
||||
let header_end = loop {
|
||||
if response.len() > 16 * 1024 {
|
||||
return Err(io::Error::other("HTTP CONNECT response header is too large"));
|
||||
}
|
||||
|
||||
let n = stream.read(&mut buf).await?;
|
||||
if n == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"unexpected EOF while reading HTTP CONNECT response",
|
||||
));
|
||||
}
|
||||
|
||||
response.extend_from_slice(&buf[..n]);
|
||||
if let Some(pos) = response.windows(4).position(|w| w == b"\r\n\r\n") {
|
||||
break pos + 4;
|
||||
}
|
||||
};
|
||||
|
||||
let header = String::from_utf8_lossy(&response[..header_end]);
|
||||
let status_line = header.lines().next().unwrap_or_default();
|
||||
let status_code = status_line
|
||||
.split_whitespace()
|
||||
.nth(1)
|
||||
.and_then(|code| code.parse::<u16>().ok())
|
||||
.ok_or_else(|| io::Error::other(format!("invalid HTTP CONNECT response: {status_line}")))?;
|
||||
|
||||
if status_code != 200 {
|
||||
return Err(io::Error::other(format!(
|
||||
"HTTP CONNECT proxy rejected tunnel with status {status_code}: {status_line}"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,15 @@ pub use self::{
|
||||
};
|
||||
|
||||
pub mod flow;
|
||||
pub mod http_connect;
|
||||
pub mod http_stream;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod launch_activate_socket;
|
||||
pub mod mon_socket;
|
||||
pub mod mon_stream;
|
||||
pub mod outbound_proxy;
|
||||
pub mod packet_window;
|
||||
pub mod socks5_client;
|
||||
pub mod outbound_proxy;
|
||||
pub mod utils;
|
||||
|
||||
/// Packet size for all UDP associations' send queue
|
||||
|
||||
@@ -4,29 +4,16 @@ use std::io;
|
||||
|
||||
use crate::config::{OutboundProxy, OutboundProxyProtocol};
|
||||
use crate::net::{
|
||||
http_connect::HttpConnectClient,
|
||||
http_stream::ProxyHttpStream,
|
||||
socks5_client::{BoxProxyStream, Socks5TcpClient, socks5_command, socks5_handshake},
|
||||
socks5_client::{BoxProxyStream, Socks5TcpClient},
|
||||
};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||
use shadowsocks::{
|
||||
context::Context,
|
||||
net::{ConnectOpts, TcpStream as OutboundTcpStream},
|
||||
relay::socks5::{Address, Command},
|
||||
relay::socks5::Address,
|
||||
};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
|
||||
pub(crate) fn address_authority(addr: &Address) -> String {
|
||||
match addr {
|
||||
Address::SocketAddress(sa) => {
|
||||
if sa.is_ipv6() {
|
||||
format!("[{}]:{}", sa.ip(), sa.port())
|
||||
} else {
|
||||
sa.to_string()
|
||||
}
|
||||
}
|
||||
Address::DomainNameAddress(host, port) => format!("{host}:{port}"),
|
||||
}
|
||||
}
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
|
||||
#[cfg(any(feature = "local-http-native-tls", feature = "local-http-rustls"))]
|
||||
async fn tls_wrap(stream: BoxProxyStream, host: &str) -> io::Result<BoxProxyStream> {
|
||||
@@ -42,60 +29,6 @@ async fn tls_wrap(_stream: BoxProxyStream, _host: &str) -> io::Result<BoxProxySt
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn http_connect<S>(stream: &mut S, proxy: &OutboundProxy, target: &Address) -> io::Result<()>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let authority = address_authority(target);
|
||||
let mut request = format!("CONNECT {authority} HTTP/1.1\r\nHost: {authority}\r\nProxy-Connection: Keep-Alive\r\n");
|
||||
|
||||
if let Some(ref auth) = proxy.auth {
|
||||
let encoded = BASE64_STANDARD.encode(format!("{}:{}", auth.username, auth.password));
|
||||
request.push_str(&format!("Proxy-Authorization: Basic {encoded}\r\n"));
|
||||
}
|
||||
|
||||
request.push_str("\r\n");
|
||||
stream.write_all(request.as_bytes()).await?;
|
||||
|
||||
let mut response = Vec::with_capacity(1024);
|
||||
let mut buf = [0u8; 1024];
|
||||
|
||||
let header_end = loop {
|
||||
if response.len() > 16 * 1024 {
|
||||
return Err(io::Error::other("HTTP CONNECT response header is too large"));
|
||||
}
|
||||
|
||||
let n = stream.read(&mut buf).await?;
|
||||
if n == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"unexpected EOF while reading HTTP CONNECT response",
|
||||
));
|
||||
}
|
||||
|
||||
response.extend_from_slice(&buf[..n]);
|
||||
if let Some(pos) = response.windows(4).position(|w| w == b"\r\n\r\n") {
|
||||
break pos + 4;
|
||||
}
|
||||
};
|
||||
|
||||
let header = String::from_utf8_lossy(&response[..header_end]);
|
||||
let status_line = header.lines().next().unwrap_or_default();
|
||||
let status_code = status_line
|
||||
.split_whitespace()
|
||||
.nth(1)
|
||||
.and_then(|code| code.parse::<u16>().ok())
|
||||
.ok_or_else(|| io::Error::other(format!("invalid HTTP CONNECT response: {status_line}")))?;
|
||||
|
||||
if status_code != 200 {
|
||||
return Err(io::Error::other(format!(
|
||||
"HTTP CONNECT proxy rejected tunnel with status {status_code}: {status_line}"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn connect_via_proxy<S>(stream: &mut S, proxy: &OutboundProxy, target: &Address) -> io::Result<()>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
@@ -106,13 +39,17 @@ where
|
||||
.auth
|
||||
.as_ref()
|
||||
.map(|auth| (auth.username.as_bytes(), auth.password.as_bytes()));
|
||||
socks5_handshake(stream, auth).await.map_err(io::Error::other)?;
|
||||
socks5_command(stream, Command::TcpConnect, target.clone())
|
||||
Socks5TcpClient::conduct_handshake_and_connect(stream, target.clone(), auth)
|
||||
.await
|
||||
.map_err(io::Error::other)?;
|
||||
Ok(())
|
||||
.map_err(io::Error::other)
|
||||
}
|
||||
OutboundProxyProtocol::Http | OutboundProxyProtocol::Https => {
|
||||
let proxy_auth = proxy
|
||||
.auth
|
||||
.as_ref()
|
||||
.map(|auth| (auth.username.as_str(), auth.password.as_str()));
|
||||
HttpConnectClient::conduct_connect(stream, target, proxy_auth).await
|
||||
}
|
||||
OutboundProxyProtocol::Http | OutboundProxyProtocol::Https => http_connect(stream, proxy, target).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,21 @@ impl Socks5TcpClient {
|
||||
})
|
||||
}
|
||||
|
||||
/// Negotiate SOCKS5 handshake and connect to `addr` on an existing stream (for proxy chains)
|
||||
pub async fn conduct_handshake_and_connect<S, A>(
|
||||
stream: &mut S,
|
||||
addr: A,
|
||||
auth: Option<(&[u8], &[u8])>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
A: Into<Address>,
|
||||
{
|
||||
socks5_handshake(stream, auth).await?;
|
||||
socks5_command(stream, Command::TcpConnect, addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn from_stream(stream: BoxProxyStream, local_addr: SocketAddr) -> Self {
|
||||
Self { stream, local_addr }
|
||||
}
|
||||
|
||||
@@ -450,6 +450,14 @@ impl<S> CryptoStream<S> {
|
||||
self.dec.nonce()
|
||||
}
|
||||
|
||||
/// Get authenticated user key (AEAD2022).
|
||||
/// Returns the key of the user that was authenticated during the handshake,
|
||||
/// or `None` for single-user mode or non-AEAD2022 ciphers.
|
||||
#[inline]
|
||||
pub fn user_key(&self) -> Option<&[u8]> {
|
||||
self.dec.user_key()
|
||||
}
|
||||
|
||||
/// Get sent IV (Stream) or Salt (AEAD, AEAD2022)
|
||||
#[inline]
|
||||
pub fn sent_nonce(&self) -> &[u8] {
|
||||
|
||||
@@ -100,6 +100,12 @@ impl<S> ProxyServerStream<S> {
|
||||
pub fn into_inner(self) -> S {
|
||||
self.stream.into_inner()
|
||||
}
|
||||
|
||||
/// Get authenticated user key after handshake (AEAD2022 multi-user mode).
|
||||
/// Returns `None` in single-user mode or before the handshake completes.
|
||||
pub fn user_key(&self) -> Option<&[u8]> {
|
||||
self.stream.user_key()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> ProxyServerStream<S>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
type HTTPClientManager interface {
|
||||
ResolveTransport(logger logger.ContextLogger, options option.HTTPClientOptions) (http.RoundTripper, error)
|
||||
DefaultTransport() http.RoundTripper
|
||||
}
|
||||
@@ -2,17 +2,11 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-tun"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
|
||||
"go4.org/netipx"
|
||||
@@ -51,7 +45,7 @@ type ConnectionRouterEx interface {
|
||||
|
||||
type RuleSet interface {
|
||||
Name() string
|
||||
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
||||
StartContext(ctx context.Context) error
|
||||
PostStart() error
|
||||
Metadata() RuleSetMetadata
|
||||
ExtractIPSet() []*netipx.IPSet
|
||||
@@ -77,46 +71,3 @@ type RuleSetMetadata struct {
|
||||
ContainsIPCIDRRule bool
|
||||
ContainsDNSQueryTypeRule bool
|
||||
}
|
||||
type HTTPStartContext struct {
|
||||
ctx context.Context
|
||||
access sync.Mutex
|
||||
httpClientCache map[string]*http.Client
|
||||
}
|
||||
|
||||
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
||||
return &HTTPStartContext{
|
||||
ctx: ctx,
|
||||
httpClientCache: make(map[string]*http.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
if httpClient, loaded := c.httpClientCache[detour]; loaded {
|
||||
return httpClient
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: C.TCPTimeout,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
Time: ntp.TimeFuncFromContext(c.ctx),
|
||||
RootCAs: RootPoolFromContext(c.ctx),
|
||||
},
|
||||
},
|
||||
}
|
||||
c.httpClientCache[detour] = httpClient
|
||||
return httpClient
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) Close() {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
for _, client := range c.httpClientCache {
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
+31
-3
@@ -16,12 +16,14 @@ import (
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/certificate"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/httpclient"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/direct"
|
||||
@@ -50,6 +52,7 @@ type Box struct {
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
httpClientService adapter.LifecycleService
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
}
|
||||
@@ -169,6 +172,10 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
|
||||
var internalServices []adapter.LifecycleService
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
|
||||
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
|
||||
httpClientService := adapter.LifecycleService(httpClientManager)
|
||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||
len(certificateOptions.Certificate) > 0 ||
|
||||
@@ -181,8 +188,6 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||
internalServices = append(internalServices, certificateStore)
|
||||
}
|
||||
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||
@@ -368,6 +373,12 @@ func New(options Options) (*Box, error) {
|
||||
&option.LocalDNSServerOptions{},
|
||||
)
|
||||
})
|
||||
httpClientManager.Initialize(func() (*httpclient.Client, error) {
|
||||
deprecated.Report(ctx, deprecated.OptionImplicitDefaultHTTPClient)
|
||||
var httpClientOptions option.HTTPClientOptions
|
||||
httpClientOptions.DefaultOutbound = true
|
||||
return httpclient.NewClient(ctx, logFactory.NewLogger("httpclient"), "", httpClientOptions)
|
||||
})
|
||||
if platformInterface != nil {
|
||||
err = platformInterface.Initialize(networkManager)
|
||||
if err != nil {
|
||||
@@ -428,6 +439,7 @@ func New(options Options) (*Box, error) {
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
httpClientService: httpClientService,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
@@ -490,7 +502,15 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter)
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStateStart, []adapter.LifecycleService{s.httpClientService})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.router, s.dnsRouter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -567,6 +587,14 @@ func (s *Box) Close() error {
|
||||
})
|
||||
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
if s.httpClientService != nil {
|
||||
s.logger.Trace("close ", s.httpClientService.Name())
|
||||
startTime := time.Now()
|
||||
err = E.Append(err, s.httpClientService.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", s.httpClientService.Name())
|
||||
})
|
||||
s.logger.Trace("close ", s.httpClientService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
for _, lifecycleService := range s.internalService {
|
||||
s.logger.Trace("close ", lifecycleService.Name())
|
||||
startTime := time.Now()
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -35,21 +36,9 @@ func updateMozillaIncludedRootCAs() error {
|
||||
return err
|
||||
}
|
||||
geoIndex := slices.Index(header, "Geographic Focus")
|
||||
nameIndex := slices.Index(header, "Common Name or Certificate Name")
|
||||
certIndex := slices.Index(header, "PEM Info")
|
||||
|
||||
generated := strings.Builder{}
|
||||
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
var mozillaIncluded *x509.CertPool
|
||||
|
||||
func init() {
|
||||
mozillaIncluded = x509.NewCertPool()
|
||||
`)
|
||||
pemBundle := strings.Builder{}
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
@@ -60,18 +49,12 @@ func init() {
|
||||
if record[geoIndex] == "China" {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
generated.WriteString(record[nameIndex])
|
||||
generated.WriteString("\n")
|
||||
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
|
||||
cert := record[certIndex]
|
||||
// Remove single quotes
|
||||
cert = cert[1 : len(cert)-1]
|
||||
generated.WriteString(cert)
|
||||
generated.WriteString("`))\n")
|
||||
pemBundle.WriteString(cert)
|
||||
pemBundle.WriteString("\n")
|
||||
}
|
||||
generated.WriteString("}\n")
|
||||
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
||||
return writeGeneratedCertificateBundle("mozilla", "mozillaIncluded", pemBundle.String())
|
||||
}
|
||||
|
||||
func fetchChinaFingerprints() (map[string]bool, error) {
|
||||
@@ -119,23 +102,11 @@ func updateChromeIncludedRootCAs() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
subjectIndex := slices.Index(header, "Subject")
|
||||
statusIndex := slices.Index(header, "Google Chrome Status")
|
||||
certIndex := slices.Index(header, "X.509 Certificate (PEM)")
|
||||
fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint")
|
||||
|
||||
generated := strings.Builder{}
|
||||
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
var chromeIncluded *x509.CertPool
|
||||
|
||||
func init() {
|
||||
chromeIncluded = x509.NewCertPool()
|
||||
`)
|
||||
pemBundle := strings.Builder{}
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
@@ -149,18 +120,39 @@ func init() {
|
||||
if chinaFingerprints[record[fingerprintIndex]] {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
generated.WriteString(record[subjectIndex])
|
||||
generated.WriteString("\n")
|
||||
generated.WriteString(" chromeIncluded.AppendCertsFromPEM([]byte(`")
|
||||
cert := record[certIndex]
|
||||
// Remove single quotes if present
|
||||
if len(cert) > 0 && cert[0] == '\'' {
|
||||
cert = cert[1 : len(cert)-1]
|
||||
}
|
||||
generated.WriteString(cert)
|
||||
generated.WriteString("`))\n")
|
||||
pemBundle.WriteString(cert)
|
||||
pemBundle.WriteString("\n")
|
||||
}
|
||||
generated.WriteString("}\n")
|
||||
return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644)
|
||||
return writeGeneratedCertificateBundle("chrome", "chromeIncluded", pemBundle.String())
|
||||
}
|
||||
|
||||
func writeGeneratedCertificateBundle(name string, variableName string, pemBundle string) error {
|
||||
goSource := `// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed ` + name + `.pem
|
||||
var ` + variableName + `PEM string
|
||||
|
||||
var ` + variableName + ` *x509.CertPool
|
||||
|
||||
func init() {
|
||||
` + variableName + ` = x509.NewCertPool()
|
||||
` + variableName + `.AppendCertsFromPEM([]byte(` + variableName + `PEM))
|
||||
}
|
||||
`
|
||||
err := os.WriteFile(filepath.Join("common/certificate", name+".pem"), []byte(pemBundle), 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filepath.Join("common/certificate", name+".go"), []byte(goSource), 0o644)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -22,8 +22,10 @@ var _ adapter.CertificateStore = (*Store)(nil)
|
||||
|
||||
type Store struct {
|
||||
access sync.RWMutex
|
||||
store string
|
||||
systemPool *x509.CertPool
|
||||
currentPool *x509.CertPool
|
||||
currentPEM []string
|
||||
certificate string
|
||||
certificatePaths []string
|
||||
certificateDirectoryPaths []string
|
||||
@@ -61,6 +63,7 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
|
||||
return nil, E.New("unknown certificate store: ", options.Store)
|
||||
}
|
||||
store := &Store{
|
||||
store: options.Store,
|
||||
systemPool: systemPool,
|
||||
certificate: strings.Join(options.Certificate, "\n"),
|
||||
certificatePaths: options.CertificatePath,
|
||||
@@ -123,19 +126,37 @@ func (s *Store) Pool() *x509.CertPool {
|
||||
return s.currentPool
|
||||
}
|
||||
|
||||
func (s *Store) StoreKind() string {
|
||||
return s.store
|
||||
}
|
||||
|
||||
func (s *Store) CurrentPEM() []string {
|
||||
s.access.RLock()
|
||||
defer s.access.RUnlock()
|
||||
return append([]string(nil), s.currentPEM...)
|
||||
}
|
||||
|
||||
func (s *Store) update() error {
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
var currentPool *x509.CertPool
|
||||
var currentPEM []string
|
||||
if s.systemPool == nil {
|
||||
currentPool = x509.NewCertPool()
|
||||
} else {
|
||||
currentPool = s.systemPool.Clone()
|
||||
}
|
||||
switch s.store {
|
||||
case C.CertificateStoreMozilla:
|
||||
currentPEM = append(currentPEM, mozillaIncludedPEM)
|
||||
case C.CertificateStoreChrome:
|
||||
currentPEM = append(currentPEM, chromeIncludedPEM)
|
||||
}
|
||||
if s.certificate != "" {
|
||||
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
||||
return E.New("invalid certificate PEM strings")
|
||||
}
|
||||
currentPEM = append(currentPEM, s.certificate)
|
||||
}
|
||||
for _, path := range s.certificatePaths {
|
||||
pemContent, err := os.ReadFile(path)
|
||||
@@ -145,6 +166,7 @@ func (s *Store) update() error {
|
||||
if !currentPool.AppendCertsFromPEM(pemContent) {
|
||||
return E.New("invalid certificate PEM file: ", path)
|
||||
}
|
||||
currentPEM = append(currentPEM, string(pemContent))
|
||||
}
|
||||
var firstErr error
|
||||
for _, directoryPath := range s.certificateDirectoryPaths {
|
||||
@@ -157,8 +179,8 @@ func (s *Store) update() error {
|
||||
}
|
||||
for _, directoryEntry := range directoryEntries {
|
||||
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
||||
if err == nil {
|
||||
currentPool.AppendCertsFromPEM(pemContent)
|
||||
if err == nil && currentPool.AppendCertsFromPEM(pemContent) {
|
||||
currentPEM = append(currentPEM, string(pemContent))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,6 +188,7 @@ func (s *Store) update() error {
|
||||
return firstErr
|
||||
}
|
||||
s.currentPool = currentPool
|
||||
s.currentPEM = currentPEM
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ type DirectDialer interface {
|
||||
type DetourDialer struct {
|
||||
outboundManager adapter.OutboundManager
|
||||
detour string
|
||||
defaultOutbound bool
|
||||
legacyDNSDialer bool
|
||||
dialer N.Dialer
|
||||
initOnce sync.Once
|
||||
@@ -33,6 +34,13 @@ func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNS
|
||||
}
|
||||
}
|
||||
|
||||
func NewDefaultOutboundDetour(outboundManager adapter.OutboundManager) N.Dialer {
|
||||
return &DetourDialer{
|
||||
outboundManager: outboundManager,
|
||||
defaultOutbound: true,
|
||||
}
|
||||
}
|
||||
|
||||
func InitializeDetour(dialer N.Dialer) error {
|
||||
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
|
||||
if !isDetour {
|
||||
@@ -47,12 +55,18 @@ func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
||||
}
|
||||
|
||||
func (d *DetourDialer) init() {
|
||||
dialer, loaded := d.outboundManager.Outbound(d.detour)
|
||||
if !loaded {
|
||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||
return
|
||||
var dialer adapter.Outbound
|
||||
if d.detour != "" {
|
||||
var loaded bool
|
||||
dialer, loaded = d.outboundManager.Outbound(d.detour)
|
||||
if !loaded {
|
||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
dialer = d.outboundManager.Default()
|
||||
}
|
||||
if !d.legacyDNSDialer {
|
||||
if !d.defaultOutbound && !d.legacyDNSDialer {
|
||||
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
||||
if directDialer.IsEmpty() {
|
||||
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
||||
|
||||
@@ -25,6 +25,7 @@ type Options struct {
|
||||
NewDialer bool
|
||||
LegacyDNSDialer bool
|
||||
DirectOutbound bool
|
||||
DefaultOutbound bool
|
||||
}
|
||||
|
||||
// TODO: merge with NewWithOptions
|
||||
@@ -42,19 +43,26 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
dialer N.Dialer
|
||||
err error
|
||||
)
|
||||
hasDetour := dialOptions.Detour != "" || options.DefaultOutbound
|
||||
if dialOptions.Detour != "" {
|
||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
||||
if outboundManager == nil {
|
||||
return nil, E.New("missing outbound manager")
|
||||
}
|
||||
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
|
||||
} else if options.DefaultOutbound {
|
||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
||||
if outboundManager == nil {
|
||||
return nil, E.New("missing outbound manager")
|
||||
}
|
||||
dialer = NewDefaultOutboundDetour(outboundManager)
|
||||
} else {
|
||||
dialer, err = NewDefault(options.Context, dialOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||
if options.RemoteIsDomain && (!hasDetour || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
||||
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
||||
var defaultOptions adapter.NetworkOptions
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type httpTransport interface {
|
||||
http.RoundTripper
|
||||
CloseIdleConnections()
|
||||
Clone() httpTransport
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
transport httpTransport
|
||||
headers http.Header
|
||||
host string
|
||||
tag string
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, logger logger.ContextLogger, tag string, options option.HTTPClientOptions) (*Client, error) {
|
||||
rawDialer, err := dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
RemoteIsDomain: true,
|
||||
ResolverOnDetour: options.ResolveOnDetour,
|
||||
NewDialer: options.ResolveOnDetour,
|
||||
DefaultOutbound: options.DefaultOutbound,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
||||
tlsOptions.Enabled = true
|
||||
baseTLSConfig, err := tls.NewClientWithOptions(tls.ClientOptions{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Options: tlsOptions,
|
||||
AllowEmptyServerName: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewClientWithDialer(rawDialer, baseTLSConfig, tag, options)
|
||||
}
|
||||
|
||||
func NewClientWithDialer(rawDialer N.Dialer, baseTLSConfig tls.Config, tag string, options option.HTTPClientOptions) (*Client, error) {
|
||||
headers := options.Headers.Build()
|
||||
host := headers.Get("Host")
|
||||
headers.Del("Host")
|
||||
transport, err := newTransport(rawDialer, baseTLSConfig, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{
|
||||
transport: transport,
|
||||
headers: headers,
|
||||
host: host,
|
||||
tag: tag,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTPClientOptions) (httpTransport, error) {
|
||||
version := options.Version
|
||||
if version == 0 {
|
||||
version = 2
|
||||
}
|
||||
fallbackDelay := time.Duration(options.DialerOptions.FallbackDelay)
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = 300 * time.Millisecond
|
||||
}
|
||||
var transport httpTransport
|
||||
var err error
|
||||
switch version {
|
||||
case 1:
|
||||
transport = newHTTP1Transport(rawDialer, baseTLSConfig)
|
||||
case 2:
|
||||
if options.DisableVersionFallback {
|
||||
transport, err = newHTTP2Transport(rawDialer, baseTLSConfig, options.HTTP2Options)
|
||||
} else {
|
||||
transport, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options)
|
||||
}
|
||||
case 3:
|
||||
if baseTLSConfig != nil {
|
||||
_, err = baseTLSConfig.STDConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options.DisableVersionFallback {
|
||||
transport, err = newHTTP3Transport(rawDialer, baseTLSConfig, options.HTTP3Options)
|
||||
} else {
|
||||
var h2Fallback httpTransport
|
||||
h2Fallback, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport, err = newHTTP3FallbackTransport(rawDialer, baseTLSConfig, h2Fallback, options.HTTP3Options, fallbackDelay)
|
||||
}
|
||||
default:
|
||||
return nil, E.New("unknown HTTP version: ", version)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transport, nil
|
||||
}
|
||||
|
||||
func (c *Client) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if c.tag == "" && len(c.headers) == 0 && c.host == "" {
|
||||
return c.transport.RoundTrip(request)
|
||||
}
|
||||
if c.tag != "" {
|
||||
if transportTag, loaded := transportTagFromContext(request.Context()); loaded && transportTag == c.tag {
|
||||
return nil, E.New("HTTP request loopback in transport[", c.tag, "]")
|
||||
}
|
||||
request = request.Clone(contextWithTransportTag(request.Context(), c.tag))
|
||||
} else {
|
||||
request = request.Clone(request.Context())
|
||||
}
|
||||
applyHeaders(request, c.headers, c.host)
|
||||
return c.transport.RoundTrip(request)
|
||||
}
|
||||
|
||||
func (c *Client) CloseIdleConnections() {
|
||||
c.transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (c *Client) Clone() *Client {
|
||||
return &Client{
|
||||
transport: c.transport.Clone(),
|
||||
headers: c.headers.Clone(),
|
||||
host: c.host,
|
||||
tag: c.tag,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
c.CloseIdleConnections()
|
||||
if closer, isCloser := c.transport.(io.Closer); isCloser {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package httpclient
|
||||
|
||||
import "context"
|
||||
|
||||
type transportKey struct{}
|
||||
|
||||
func contextWithTransportTag(ctx context.Context, transportTag string) context.Context {
|
||||
return context.WithValue(ctx, transportKey{}, transportTag)
|
||||
}
|
||||
|
||||
func transportTagFromContext(ctx context.Context) (string, bool) {
|
||||
value, loaded := ctx.Value(transportKey{}).(string)
|
||||
return value, loaded
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdTLS "crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func dialTLS(ctx context.Context, rawDialer N.Dialer, baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string, expectProto string) (net.Conn, error) {
|
||||
if baseTLSConfig == nil {
|
||||
return nil, E.New("TLS transport unavailable")
|
||||
}
|
||||
tlsConfig := baseTLSConfig.Clone()
|
||||
if tlsConfig.ServerName() == "" && destination.IsValid() {
|
||||
tlsConfig.SetServerName(destination.AddrString())
|
||||
}
|
||||
tlsConfig.SetNextProtos(nextProtos)
|
||||
conn, err := rawDialer.DialContext(ctx, N.NetworkTCP, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConn, err := tls.ClientHandshake(ctx, conn, tlsConfig)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if expectProto != "" && tlsConn.ConnectionState().NegotiatedProtocol != expectProto {
|
||||
tlsConn.Close()
|
||||
return nil, errHTTP2Fallback
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
func applyHeaders(request *http.Request, headers http.Header, host string) {
|
||||
for header, values := range headers {
|
||||
request.Header[header] = append([]string(nil), values...)
|
||||
}
|
||||
if host != "" {
|
||||
request.Host = host
|
||||
}
|
||||
}
|
||||
|
||||
func requestRequiresHTTP1(request *http.Request) bool {
|
||||
return strings.Contains(strings.ToLower(request.Header.Get("Connection")), "upgrade") &&
|
||||
strings.EqualFold(request.Header.Get("Upgrade"), "websocket")
|
||||
}
|
||||
|
||||
func requestReplayable(request *http.Request) bool {
|
||||
return request.Body == nil || request.Body == http.NoBody || request.GetBody != nil
|
||||
}
|
||||
|
||||
func cloneRequestForRetry(request *http.Request) *http.Request {
|
||||
cloned := request.Clone(request.Context())
|
||||
if request.Body != nil && request.Body != http.NoBody && request.GetBody != nil {
|
||||
cloned.Body = mustGetBody(request)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func mustGetBody(request *http.Request) io.ReadCloser {
|
||||
body, err := request.GetBody()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func buildSTDTLSConfig(baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string) (*stdTLS.Config, error) {
|
||||
if baseTLSConfig == nil {
|
||||
return nil, nil
|
||||
}
|
||||
tlsConfig := baseTLSConfig.Clone()
|
||||
if tlsConfig.ServerName() == "" && destination.IsValid() {
|
||||
tlsConfig.SetServerName(destination.AddrString())
|
||||
}
|
||||
tlsConfig.SetNextProtos(nextProtos)
|
||||
return tlsConfig.STDConfig()
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type http1Transport struct {
|
||||
transport *http.Transport
|
||||
}
|
||||
|
||||
func newHTTP1Transport(rawDialer N.Dialer, baseTLSConfig tls.Config) *http1Transport {
|
||||
transport := &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return rawDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
}
|
||||
if baseTLSConfig != nil {
|
||||
transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{"http/1.1"}, "")
|
||||
}
|
||||
}
|
||||
return &http1Transport{transport: transport}
|
||||
}
|
||||
|
||||
func (t *http1Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return t.transport.RoundTrip(request)
|
||||
}
|
||||
|
||||
func (t *http1Transport) CloseIdleConnections() {
|
||||
t.transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (t *http1Transport) Clone() httpTransport {
|
||||
return &http1Transport{transport: t.transport.Clone()}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
stdTLS "crypto/tls"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
func CloneHTTP2Transport(transport *http2.Transport) *http2.Transport {
|
||||
return &http2.Transport{
|
||||
ReadIdleTimeout: transport.ReadIdleTimeout,
|
||||
PingTimeout: transport.PingTimeout,
|
||||
DialTLSContext: transport.DialTLSContext,
|
||||
}
|
||||
}
|
||||
|
||||
func ConfigureHTTP2Transport(options option.HTTP2Options) (*http2.Transport, error) {
|
||||
stdTransport := &http.Transport{
|
||||
TLSClientConfig: &stdTLS.Config{},
|
||||
HTTP2: &http.HTTP2Config{
|
||||
MaxReceiveBufferPerStream: int(options.StreamReceiveWindow.Value()),
|
||||
MaxReceiveBufferPerConnection: int(options.ConnectionReceiveWindow.Value()),
|
||||
MaxConcurrentStreams: options.MaxConcurrentStreams,
|
||||
SendPingTimeout: time.Duration(options.KeepAlivePeriod),
|
||||
PingTimeout: time.Duration(options.IdleTimeout),
|
||||
},
|
||||
}
|
||||
h2Transport, err := http2.ConfigureTransports(stdTransport)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "configure HTTP/2 transport")
|
||||
}
|
||||
// ConfigureTransports binds ConnPool to the throwaway http.Transport; sever it so DialTLSContext is used directly.
|
||||
h2Transport.ConnPool = nil
|
||||
h2Transport.ReadIdleTimeout = time.Duration(options.KeepAlivePeriod)
|
||||
h2Transport.PingTimeout = time.Duration(options.IdleTimeout)
|
||||
return h2Transport, nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdTLS "crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var errHTTP2Fallback = E.New("fallback to HTTP/1.1")
|
||||
|
||||
type http2FallbackTransport struct {
|
||||
h2Transport *http2.Transport
|
||||
h1Transport *http1Transport
|
||||
h2Fallback *atomic.Bool
|
||||
}
|
||||
|
||||
func newHTTP2FallbackTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2FallbackTransport, error) {
|
||||
h1 := newHTTP1Transport(rawDialer, baseTLSConfig)
|
||||
var fallback atomic.Bool
|
||||
h2Transport, err := ConfigureHTTP2Transport(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) {
|
||||
conn, dialErr := dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS, "http/1.1"}, http2.NextProtoTLS)
|
||||
if dialErr != nil {
|
||||
if errors.Is(dialErr, errHTTP2Fallback) {
|
||||
fallback.Store(true)
|
||||
}
|
||||
return nil, dialErr
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
return &http2FallbackTransport{
|
||||
h2Transport: h2Transport,
|
||||
h1Transport: h1,
|
||||
h2Fallback: &fallback,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *http2FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return t.roundTrip(request, true)
|
||||
}
|
||||
|
||||
func (t *http2FallbackTransport) roundTrip(request *http.Request, allowHTTP1Fallback bool) (*http.Response, error) {
|
||||
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
|
||||
return t.h1Transport.RoundTrip(request)
|
||||
}
|
||||
if t.h2Fallback.Load() {
|
||||
if !allowHTTP1Fallback {
|
||||
return nil, errHTTP2Fallback
|
||||
}
|
||||
return t.h1Transport.RoundTrip(request)
|
||||
}
|
||||
response, err := t.h2Transport.RoundTrip(request)
|
||||
if err == nil {
|
||||
return response, nil
|
||||
}
|
||||
if !errors.Is(err, errHTTP2Fallback) || !allowHTTP1Fallback {
|
||||
return nil, err
|
||||
}
|
||||
return t.h1Transport.RoundTrip(cloneRequestForRetry(request))
|
||||
}
|
||||
|
||||
func (t *http2FallbackTransport) CloseIdleConnections() {
|
||||
t.h1Transport.CloseIdleConnections()
|
||||
t.h2Transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (t *http2FallbackTransport) Clone() httpTransport {
|
||||
return &http2FallbackTransport{
|
||||
h2Transport: CloneHTTP2Transport(t.h2Transport),
|
||||
h1Transport: t.h1Transport.Clone().(*http1Transport),
|
||||
h2Fallback: t.h2Fallback,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdTLS "crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type http2Transport struct {
|
||||
h2Transport *http2.Transport
|
||||
h1Transport *http1Transport
|
||||
}
|
||||
|
||||
func newHTTP2Transport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2Transport, error) {
|
||||
h1 := newHTTP1Transport(rawDialer, baseTLSConfig)
|
||||
h2Transport, err := ConfigureHTTP2Transport(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) {
|
||||
return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS}, http2.NextProtoTLS)
|
||||
}
|
||||
return &http2Transport{
|
||||
h2Transport: h2Transport,
|
||||
h1Transport: h1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *http2Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
|
||||
return t.h1Transport.RoundTrip(request)
|
||||
}
|
||||
return t.h2Transport.RoundTrip(request)
|
||||
}
|
||||
|
||||
func (t *http2Transport) CloseIdleConnections() {
|
||||
t.h1Transport.CloseIdleConnections()
|
||||
t.h2Transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (t *http2Transport) Clone() httpTransport {
|
||||
return &http2Transport{
|
||||
h2Transport: CloneHTTP2Transport(t.h2Transport),
|
||||
h1Transport: t.h1Transport.Clone().(*http1Transport),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
//go:build with_quic
|
||||
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdTLS "crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type http3Transport struct {
|
||||
h3Transport *http3.Transport
|
||||
}
|
||||
|
||||
type http3FallbackTransport struct {
|
||||
h3Transport *http3.Transport
|
||||
h2Fallback httpTransport
|
||||
fallbackDelay time.Duration
|
||||
brokenAccess sync.Mutex
|
||||
brokenUntil time.Time
|
||||
brokenBackoff time.Duration
|
||||
}
|
||||
|
||||
func newHTTP3RoundTripper(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
options option.QUICOptions,
|
||||
) *http3.Transport {
|
||||
var handshakeTimeout time.Duration
|
||||
if baseTLSConfig != nil {
|
||||
handshakeTimeout = baseTLSConfig.HandshakeTimeout()
|
||||
}
|
||||
quicConfig := &quic.Config{
|
||||
InitialStreamReceiveWindow: options.StreamReceiveWindow.Value(),
|
||||
MaxStreamReceiveWindow: options.StreamReceiveWindow.Value(),
|
||||
InitialConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
|
||||
MaxConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
|
||||
KeepAlivePeriod: time.Duration(options.KeepAlivePeriod),
|
||||
MaxIdleTimeout: time.Duration(options.IdleTimeout),
|
||||
DisablePathMTUDiscovery: options.DisablePathMTUDiscovery,
|
||||
}
|
||||
if options.InitialPacketSize > 0 {
|
||||
quicConfig.InitialPacketSize = uint16(options.InitialPacketSize)
|
||||
}
|
||||
if options.MaxConcurrentStreams > 0 {
|
||||
quicConfig.MaxIncomingStreams = int64(options.MaxConcurrentStreams)
|
||||
}
|
||||
if handshakeTimeout > 0 {
|
||||
quicConfig.HandshakeIdleTimeout = handshakeTimeout
|
||||
}
|
||||
h3Transport := &http3.Transport{
|
||||
TLSClientConfig: &stdTLS.Config{},
|
||||
QUICConfig: quicConfig,
|
||||
Dial: func(ctx context.Context, addr string, tlsConfig *stdTLS.Config, quicConfig *quic.Config) (*quic.Conn, error) {
|
||||
if handshakeTimeout > 0 && quicConfig.HandshakeIdleTimeout == 0 {
|
||||
quicConfig = quicConfig.Clone()
|
||||
quicConfig.HandshakeIdleTimeout = handshakeTimeout
|
||||
}
|
||||
if baseTLSConfig != nil {
|
||||
var err error
|
||||
tlsConfig, err = buildSTDTLSConfig(baseTLSConfig, M.ParseSocksaddr(addr), []string{http3.NextProtoH3})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
tlsConfig = tlsConfig.Clone()
|
||||
tlsConfig.NextProtos = []string{http3.NextProtoH3}
|
||||
}
|
||||
conn, err := rawDialer.DialContext(ctx, N.NetworkUDP, M.ParseSocksaddr(addr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quicConn, err := quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsConfig, quicConfig)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return quicConn, nil
|
||||
},
|
||||
}
|
||||
return h3Transport
|
||||
}
|
||||
|
||||
func newHTTP3Transport(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
options option.QUICOptions,
|
||||
) (httpTransport, error) {
|
||||
return &http3Transport{
|
||||
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newHTTP3FallbackTransport(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
h2Fallback httpTransport,
|
||||
options option.QUICOptions,
|
||||
fallbackDelay time.Duration,
|
||||
) (httpTransport, error) {
|
||||
return &http3FallbackTransport{
|
||||
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
|
||||
h2Fallback: h2Fallback,
|
||||
fallbackDelay: fallbackDelay,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *http3Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return t.h3Transport.RoundTrip(request)
|
||||
}
|
||||
|
||||
func (t *http3Transport) CloseIdleConnections() {
|
||||
t.h3Transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (t *http3Transport) Close() error {
|
||||
t.CloseIdleConnections()
|
||||
return t.h3Transport.Close()
|
||||
}
|
||||
|
||||
func (t *http3Transport) Clone() httpTransport {
|
||||
return &http3Transport{
|
||||
h3Transport: t.h3Transport,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
|
||||
return t.h2Fallback.RoundTrip(request)
|
||||
}
|
||||
return t.roundTripHTTP3(request)
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) roundTripHTTP3(request *http.Request) (*http.Response, error) {
|
||||
if t.h3Broken() {
|
||||
return t.h2FallbackRoundTrip(request)
|
||||
}
|
||||
response, err := t.h3Transport.RoundTripOpt(request, http3.RoundTripOpt{OnlyCachedConn: true})
|
||||
if err == nil {
|
||||
t.clearH3Broken()
|
||||
return response, nil
|
||||
}
|
||||
if !errors.Is(err, http3.ErrNoCachedConn) {
|
||||
t.markH3Broken()
|
||||
return t.h2FallbackRoundTrip(cloneRequestForRetry(request))
|
||||
}
|
||||
if !requestReplayable(request) {
|
||||
response, err = t.h3Transport.RoundTrip(request)
|
||||
if err == nil {
|
||||
t.clearH3Broken()
|
||||
return response, nil
|
||||
}
|
||||
t.markH3Broken()
|
||||
return nil, err
|
||||
}
|
||||
return t.roundTripHTTP3Race(request)
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request) (*http.Response, error) {
|
||||
ctx, cancel := context.WithCancel(request.Context())
|
||||
defer cancel()
|
||||
type result struct {
|
||||
response *http.Response
|
||||
err error
|
||||
h3 bool
|
||||
}
|
||||
results := make(chan result, 2)
|
||||
startRoundTrip := func(request *http.Request, useH3 bool) {
|
||||
request = request.WithContext(ctx)
|
||||
var (
|
||||
response *http.Response
|
||||
err error
|
||||
)
|
||||
if useH3 {
|
||||
response, err = t.h3Transport.RoundTrip(request)
|
||||
} else {
|
||||
response, err = t.h2FallbackRoundTrip(request)
|
||||
}
|
||||
results <- result{response: response, err: err, h3: useH3}
|
||||
}
|
||||
goroutines := 1
|
||||
received := 0
|
||||
drainRemaining := func() {
|
||||
cancel()
|
||||
for range goroutines - received {
|
||||
go func() {
|
||||
loser := <-results
|
||||
if loser.response != nil && loser.response.Body != nil {
|
||||
loser.response.Body.Close()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
go startRoundTrip(cloneRequestForRetry(request), true)
|
||||
timer := time.NewTimer(t.fallbackDelay)
|
||||
defer timer.Stop()
|
||||
var (
|
||||
h3Err error
|
||||
fallbackErr error
|
||||
)
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
if goroutines == 1 {
|
||||
goroutines++
|
||||
go startRoundTrip(cloneRequestForRetry(request), false)
|
||||
}
|
||||
case raceResult := <-results:
|
||||
received++
|
||||
if raceResult.err == nil {
|
||||
if raceResult.h3 {
|
||||
t.clearH3Broken()
|
||||
}
|
||||
drainRemaining()
|
||||
return raceResult.response, nil
|
||||
}
|
||||
if raceResult.h3 {
|
||||
t.markH3Broken()
|
||||
h3Err = raceResult.err
|
||||
if goroutines == 1 {
|
||||
goroutines++
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
go startRoundTrip(cloneRequestForRetry(request), false)
|
||||
}
|
||||
} else {
|
||||
fallbackErr = raceResult.err
|
||||
}
|
||||
if received < goroutines {
|
||||
continue
|
||||
}
|
||||
drainRemaining()
|
||||
switch {
|
||||
case h3Err != nil && fallbackErr != nil:
|
||||
return nil, E.Errors(h3Err, fallbackErr)
|
||||
case fallbackErr != nil:
|
||||
return nil, fallbackErr
|
||||
default:
|
||||
return nil, h3Err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) h2FallbackRoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if fallback, isFallback := t.h2Fallback.(*http2FallbackTransport); isFallback {
|
||||
return fallback.roundTrip(request, true)
|
||||
}
|
||||
return t.h2Fallback.RoundTrip(request)
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) CloseIdleConnections() {
|
||||
t.h3Transport.CloseIdleConnections()
|
||||
t.h2Fallback.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) Close() error {
|
||||
t.CloseIdleConnections()
|
||||
return t.h3Transport.Close()
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) Clone() httpTransport {
|
||||
return &http3FallbackTransport{
|
||||
h3Transport: t.h3Transport,
|
||||
h2Fallback: t.h2Fallback.Clone(),
|
||||
fallbackDelay: t.fallbackDelay,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) h3Broken() bool {
|
||||
t.brokenAccess.Lock()
|
||||
defer t.brokenAccess.Unlock()
|
||||
return !t.brokenUntil.IsZero() && time.Now().Before(t.brokenUntil)
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) clearH3Broken() {
|
||||
t.brokenAccess.Lock()
|
||||
t.brokenUntil = time.Time{}
|
||||
t.brokenBackoff = 0
|
||||
t.brokenAccess.Unlock()
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) markH3Broken() {
|
||||
t.brokenAccess.Lock()
|
||||
defer t.brokenAccess.Unlock()
|
||||
if t.brokenBackoff == 0 {
|
||||
t.brokenBackoff = 5 * time.Minute
|
||||
} else {
|
||||
t.brokenBackoff *= 2
|
||||
if t.brokenBackoff > 48*time.Hour {
|
||||
t.brokenBackoff = 48 * time.Hour
|
||||
}
|
||||
}
|
||||
t.brokenUntil = time.Now().Add(t.brokenBackoff)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//go:build !with_quic
|
||||
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func newHTTP3FallbackTransport(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
h2Fallback httpTransport,
|
||||
options option.QUICOptions,
|
||||
fallbackDelay time.Duration,
|
||||
) (httpTransport, error) {
|
||||
return nil, E.New("HTTP/3 requires building with the with_quic tag")
|
||||
}
|
||||
|
||||
func newHTTP3Transport(
|
||||
rawDialer N.Dialer,
|
||||
baseTLSConfig tls.Config,
|
||||
options option.QUICOptions,
|
||||
) (httpTransport, error) {
|
||||
return nil, E.New("HTTP/3 requires building with the with_quic tag")
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
_ adapter.HTTPClientManager = (*Manager)(nil)
|
||||
_ adapter.LifecycleService = (*Manager)(nil)
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
access sync.Mutex
|
||||
defines map[string]option.HTTPClient
|
||||
clients map[string]*Client
|
||||
defaultTag string
|
||||
defaultTransport http.RoundTripper
|
||||
defaultTransportFallback func() (*Client, error)
|
||||
fallbackClient *Client
|
||||
}
|
||||
|
||||
func NewManager(ctx context.Context, logger log.ContextLogger, clients []option.HTTPClient, defaultHTTPClient string) *Manager {
|
||||
defines := make(map[string]option.HTTPClient, len(clients))
|
||||
for _, client := range clients {
|
||||
defines[client.Tag] = client
|
||||
}
|
||||
defaultTag := defaultHTTPClient
|
||||
if defaultTag == "" && len(clients) > 0 {
|
||||
defaultTag = clients[0].Tag
|
||||
}
|
||||
return &Manager{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
defines: defines,
|
||||
clients: make(map[string]*Client),
|
||||
defaultTag: defaultTag,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Initialize(defaultTransportFallback func() (*Client, error)) {
|
||||
m.defaultTransportFallback = defaultTransportFallback
|
||||
}
|
||||
|
||||
func (m *Manager) Name() string {
|
||||
return "http-client"
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
if m.defaultTag != "" {
|
||||
transport, err := m.resolveShared(m.defaultTag)
|
||||
if err != nil {
|
||||
return E.Cause(err, "resolve default http client")
|
||||
}
|
||||
m.defaultTransport = transport
|
||||
} else if m.defaultTransportFallback != nil {
|
||||
client, err := m.defaultTransportFallback()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create default http client")
|
||||
}
|
||||
m.defaultTransport = client
|
||||
m.fallbackClient = client
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) DefaultTransport() http.RoundTripper {
|
||||
return m.defaultTransport
|
||||
}
|
||||
|
||||
func (m *Manager) ResolveTransport(logger logger.ContextLogger, options option.HTTPClientOptions) (http.RoundTripper, error) {
|
||||
if options.Tag != "" {
|
||||
if options.ResolveOnDetour {
|
||||
define, loaded := m.defines[options.Tag]
|
||||
if !loaded {
|
||||
return nil, E.New("http_client not found: ", options.Tag)
|
||||
}
|
||||
resolvedOptions := define.Options()
|
||||
resolvedOptions.ResolveOnDetour = true
|
||||
return NewClient(m.ctx, logger, options.Tag, resolvedOptions)
|
||||
}
|
||||
return m.resolveShared(options.Tag)
|
||||
}
|
||||
return NewClient(m.ctx, logger, "", options)
|
||||
}
|
||||
|
||||
func (m *Manager) resolveShared(tag string) (http.RoundTripper, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if client, loaded := m.clients[tag]; loaded {
|
||||
return client, nil
|
||||
}
|
||||
define, loaded := m.defines[tag]
|
||||
if !loaded {
|
||||
return nil, E.New("http_client not found: ", tag)
|
||||
}
|
||||
client, err := NewClient(m.ctx, m.logger, tag, define.Options())
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create shared http_client[", tag, "]")
|
||||
}
|
||||
m.clients[tag] = client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.clients == nil {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
for _, client := range m.clients {
|
||||
err = E.Append(err, client.Close(), func(err error) error {
|
||||
return E.Cause(err, "close http client")
|
||||
})
|
||||
}
|
||||
if m.fallbackClient != nil {
|
||||
err = E.Append(err, m.fallbackClient.Close(), func(err error) error {
|
||||
return E.Cause(err, "close default http client")
|
||||
})
|
||||
}
|
||||
m.clients = nil
|
||||
return err
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,205 @@
|
||||
//go:build darwin && cgo
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdtls "crypto/tls"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
const appleTLSTestTimeout = 5 * time.Second
|
||||
|
||||
type appleTLSServerResult struct {
|
||||
state stdtls.ConnectionState
|
||||
err error
|
||||
}
|
||||
|
||||
func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) {
|
||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
||||
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
||||
Certificates: []stdtls.Certificate{serverCertificate},
|
||||
MinVersion: stdtls.VersionTLS12,
|
||||
MaxVersion: stdtls.VersionTLS12,
|
||||
NextProtos: []string{"h2"},
|
||||
})
|
||||
|
||||
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Engine: "apple",
|
||||
ServerName: "localhost",
|
||||
MinVersion: "1.2",
|
||||
MaxVersion: "1.2",
|
||||
ALPN: badoption.Listable[string]{"h2"},
|
||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer clientConn.Close()
|
||||
|
||||
clientState := clientConn.ConnectionState()
|
||||
if clientState.Version != stdtls.VersionTLS12 {
|
||||
t.Fatalf("unexpected negotiated version: %x", clientState.Version)
|
||||
}
|
||||
if clientState.NegotiatedProtocol != "h2" {
|
||||
t.Fatalf("unexpected negotiated protocol: %q", clientState.NegotiatedProtocol)
|
||||
}
|
||||
|
||||
result := <-serverResult
|
||||
if result.err != nil {
|
||||
t.Fatal(result.err)
|
||||
}
|
||||
if result.state.Version != stdtls.VersionTLS12 {
|
||||
t.Fatalf("server negotiated unexpected version: %x", result.state.Version)
|
||||
}
|
||||
if result.state.NegotiatedProtocol != "h2" {
|
||||
t.Fatalf("server negotiated unexpected protocol: %q", result.state.NegotiatedProtocol)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppleClientHandshakeRejectsVersionMismatch(t *testing.T) {
|
||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
||||
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
||||
Certificates: []stdtls.Certificate{serverCertificate},
|
||||
MinVersion: stdtls.VersionTLS13,
|
||||
MaxVersion: stdtls.VersionTLS13,
|
||||
})
|
||||
|
||||
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Engine: "apple",
|
||||
ServerName: "localhost",
|
||||
MaxVersion: "1.2",
|
||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||
})
|
||||
if err == nil {
|
||||
clientConn.Close()
|
||||
t.Fatal("expected version mismatch handshake to fail")
|
||||
}
|
||||
|
||||
if result := <-serverResult; result.err == nil {
|
||||
t.Fatal("expected server handshake to fail on version mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppleClientHandshakeRejectsServerNameMismatch(t *testing.T) {
|
||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
||||
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
||||
Certificates: []stdtls.Certificate{serverCertificate},
|
||||
})
|
||||
|
||||
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
Engine: "apple",
|
||||
ServerName: "example.com",
|
||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
||||
})
|
||||
if err == nil {
|
||||
clientConn.Close()
|
||||
t.Fatal("expected server name mismatch handshake to fail")
|
||||
}
|
||||
|
||||
if result := <-serverResult; result.err == nil {
|
||||
t.Fatal("expected server handshake to fail on server name mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func newAppleTestCertificate(t *testing.T, serverName string) (stdtls.Certificate, string) {
|
||||
t.Helper()
|
||||
|
||||
privateKeyPEM, certificatePEM, err := GenerateCertificate(nil, nil, time.Now, serverName, time.Now().Add(time.Hour))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
certificate, err := stdtls.X509KeyPair(certificatePEM, privateKeyPEM)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return certificate, string(certificatePEM)
|
||||
}
|
||||
|
||||
func startAppleTLSTestServer(t *testing.T, tlsConfig *stdtls.Config) (<-chan appleTLSServerResult, string) {
|
||||
t.Helper()
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
listener.Close()
|
||||
})
|
||||
|
||||
if tcpListener, isTCP := listener.(*net.TCPListener); isTCP {
|
||||
err = tcpListener.SetDeadline(time.Now().Add(appleTLSTestTimeout))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
result := make(chan appleTLSServerResult, 1)
|
||||
go func() {
|
||||
defer close(result)
|
||||
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
result <- appleTLSServerResult{err: err}
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
err = conn.SetDeadline(time.Now().Add(appleTLSTestTimeout))
|
||||
if err != nil {
|
||||
result <- appleTLSServerResult{err: err}
|
||||
return
|
||||
}
|
||||
|
||||
tlsConn := stdtls.Server(conn, tlsConfig)
|
||||
defer tlsConn.Close()
|
||||
|
||||
err = tlsConn.Handshake()
|
||||
if err != nil {
|
||||
result <- appleTLSServerResult{err: err}
|
||||
return
|
||||
}
|
||||
|
||||
result <- appleTLSServerResult{state: tlsConn.ConnectionState()}
|
||||
}()
|
||||
|
||||
return result, listener.Addr().String()
|
||||
}
|
||||
|
||||
func newAppleTestClientConn(t *testing.T, serverAddress string, options option.OutboundTLSOptions) (Conn, error) {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), appleTLSTestTimeout)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clientConfig, err := NewClientWithOptions(ClientOptions{
|
||||
Context: ctx,
|
||||
Logger: logger.NOP(),
|
||||
ServerAddress: "",
|
||||
Options: options,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", serverAddress, appleTLSTestTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tlsConn, err := ClientHandshake(ctx, conn, clientConfig)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build !darwin || !cgo
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
func newAppleClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
return nil, E.New("Apple TLS engine is not available on non-Apple platforms")
|
||||
}
|
||||
@@ -8,14 +8,16 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/common/badtls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
var errMissingServerName = E.New("missing server_name or insecure=true")
|
||||
|
||||
func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
||||
if !options.Enabled {
|
||||
return dialer, nil
|
||||
@@ -42,11 +44,12 @@ func NewClient(ctx context.Context, logger logger.ContextLogger, serverAddress s
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
ServerAddress string
|
||||
Options option.OutboundTLSOptions
|
||||
KTLSCompatible bool
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
ServerAddress string
|
||||
Options option.OutboundTLSOptions
|
||||
AllowEmptyServerName bool
|
||||
KTLSCompatible bool
|
||||
}
|
||||
|
||||
func NewClientWithOptions(options ClientOptions) (Config, error) {
|
||||
@@ -61,17 +64,22 @@ func NewClientWithOptions(options ClientOptions) (Config, error) {
|
||||
if options.Options.KernelRx {
|
||||
options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx")
|
||||
}
|
||||
if options.Options.Reality != nil && options.Options.Reality.Enabled {
|
||||
return NewRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
} else if options.Options.UTLS != nil && options.Options.UTLS.Enabled {
|
||||
return NewUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
switch options.Options.Engine {
|
||||
case "", "go":
|
||||
case "apple":
|
||||
return newAppleClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
default:
|
||||
return nil, E.New("unknown tls engine: ", options.Options.Engine)
|
||||
}
|
||||
return NewSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
if options.Options.Reality != nil && options.Options.Reality.Enabled {
|
||||
return newRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
} else if options.Options.UTLS != nil && options.Options.UTLS.Enabled {
|
||||
return newUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
}
|
||||
return newSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
||||
}
|
||||
|
||||
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||
defer cancel()
|
||||
tlsConn, err := aTLS.ClientHandshake(ctx, conn, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -52,11 +52,15 @@ type RealityClientConfig struct {
|
||||
}
|
||||
|
||||
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newRealityClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
if options.UTLS == nil || !options.UTLS.Enabled {
|
||||
return nil, E.New("uTLS is required by reality client")
|
||||
}
|
||||
|
||||
uClient, err := NewUTLSClient(ctx, logger, serverAddress, options)
|
||||
uClient, err := newUTLSClient(ctx, logger, serverAddress, options, allowEmptyServerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -108,6 +112,14 @@ func (e *RealityClientConfig) SetNextProtos(nextProto []string) {
|
||||
e.uClient.SetNextProtos(nextProto)
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) HandshakeTimeout() time.Duration {
|
||||
return e.uClient.HandshakeTimeout()
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
e.uClient.SetHandshakeTimeout(timeout)
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return nil, E.New("unsupported usage for reality")
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ import (
|
||||
var _ ServerConfigCompat = (*RealityServerConfig)(nil)
|
||||
|
||||
type RealityServerConfig struct {
|
||||
config *utls.RealityConfig
|
||||
config *utls.RealityConfig
|
||||
handshakeTimeout time.Duration
|
||||
}
|
||||
|
||||
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
@@ -130,7 +131,16 @@ func NewRealityServer(ctx context.Context, logger log.ContextLogger, options opt
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
return nil, E.New("Reality is conflict with ECH")
|
||||
}
|
||||
var config ServerConfig = &RealityServerConfig{&tlsConfig}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
var config ServerConfig = &RealityServerConfig{
|
||||
config: &tlsConfig,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
}
|
||||
if options.KernelTx || options.KernelRx {
|
||||
if !C.IsLinux {
|
||||
return nil, E.New("kTLS is only supported on Linux")
|
||||
@@ -161,6 +171,14 @@ func (c *RealityServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *RealityServerConfig) HandshakeTimeout() time.Duration {
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *RealityServerConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *RealityServerConfig) STDConfig() (*tls.Config, error) {
|
||||
return nil, E.New("unsupported usage for reality")
|
||||
}
|
||||
@@ -191,7 +209,8 @@ func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn
|
||||
|
||||
func (c *RealityServerConfig) Clone() Config {
|
||||
return &RealityServerConfig{
|
||||
config: c.config.Clone(),
|
||||
config: c.config.Clone(),
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,8 +46,11 @@ func NewServerWithOptions(options ServerOptions) (ServerConfig, error) {
|
||||
}
|
||||
|
||||
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||
defer cancel()
|
||||
if config.HandshakeTimeout() == 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, C.TCPTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
tlsConn, err := aTLS.ServerHandshake(ctx, conn, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -24,16 +24,30 @@ import (
|
||||
type STDClientConfig struct {
|
||||
ctx context.Context
|
||||
config *tls.Config
|
||||
serverName string
|
||||
disableSNI bool
|
||||
verifyServerName bool
|
||||
handshakeTimeout time.Duration
|
||||
fragment bool
|
||||
fragmentFallbackDelay time.Duration
|
||||
recordFragment bool
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) ServerName() string {
|
||||
return c.config.ServerName
|
||||
return c.serverName
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) SetServerName(serverName string) {
|
||||
c.serverName = serverName
|
||||
if c.disableSNI {
|
||||
c.config.ServerName = ""
|
||||
if c.verifyServerName {
|
||||
c.config.VerifyConnection = verifyConnection(c.config.RootCAs, c.config.Time, serverName)
|
||||
} else {
|
||||
c.config.VerifyConnection = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
c.config.ServerName = serverName
|
||||
}
|
||||
|
||||
@@ -45,6 +59,14 @@ func (c *STDClientConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) HandshakeTimeout() time.Duration {
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return c.config, nil
|
||||
}
|
||||
@@ -57,13 +79,19 @@ func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) {
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) Clone() Config {
|
||||
return &STDClientConfig{
|
||||
cloned := &STDClientConfig{
|
||||
ctx: c.ctx,
|
||||
config: c.config.Clone(),
|
||||
serverName: c.serverName,
|
||||
disableSNI: c.disableSNI,
|
||||
verifyServerName: c.verifyServerName,
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
fragment: c.fragment,
|
||||
fragmentFallbackDelay: c.fragmentFallbackDelay,
|
||||
recordFragment: c.recordFragment,
|
||||
}
|
||||
cloned.SetServerName(cloned.serverName)
|
||||
return cloned
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) ECHConfigList() []byte {
|
||||
@@ -75,41 +103,27 @@ func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte
|
||||
}
|
||||
|
||||
func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newSTDClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
} else if serverAddress != "" {
|
||||
serverName = serverAddress
|
||||
}
|
||||
if serverName == "" && !options.Insecure {
|
||||
return nil, E.New("missing server_name or insecure=true")
|
||||
if serverName == "" && !options.Insecure && !allowEmptyServerName {
|
||||
return nil, errMissingServerName
|
||||
}
|
||||
|
||||
var tlsConfig tls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if !options.DisableSNI {
|
||||
tlsConfig.ServerName = serverName
|
||||
}
|
||||
if options.Insecure {
|
||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
||||
} else if options.DisableSNI {
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
|
||||
verifyOptions := x509.VerifyOptions{
|
||||
Roots: tlsConfig.RootCAs,
|
||||
DNSName: serverName,
|
||||
Intermediates: x509.NewCertPool(),
|
||||
}
|
||||
for _, cert := range state.PeerCertificates[1:] {
|
||||
verifyOptions.Intermediates.AddCert(cert)
|
||||
}
|
||||
if tlsConfig.Time != nil {
|
||||
verifyOptions.CurrentTime = tlsConfig.Time()
|
||||
}
|
||||
_, err := state.PeerCertificates[0].Verify(verifyOptions)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(options.CertificatePublicKeySHA256) > 0 {
|
||||
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
||||
@@ -198,7 +212,24 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
|
||||
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
|
||||
return nil, E.New("client certificate and client key must be provided together")
|
||||
}
|
||||
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
var config Config = &STDClientConfig{
|
||||
ctx: ctx,
|
||||
config: &tlsConfig,
|
||||
serverName: serverName,
|
||||
disableSNI: options.DisableSNI,
|
||||
verifyServerName: options.DisableSNI && !options.Insecure,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
fragment: options.Fragment,
|
||||
fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay),
|
||||
recordFragment: options.RecordFragment,
|
||||
}
|
||||
config.SetServerName(serverName)
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
var err error
|
||||
config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options)
|
||||
@@ -220,6 +251,27 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func verifyConnection(rootCAs *x509.CertPool, timeFunc func() time.Time, serverName string) func(state tls.ConnectionState) error {
|
||||
return func(state tls.ConnectionState) error {
|
||||
if serverName == "" {
|
||||
return errMissingServerName
|
||||
}
|
||||
verifyOptions := x509.VerifyOptions{
|
||||
Roots: rootCAs,
|
||||
DNSName: serverName,
|
||||
Intermediates: x509.NewCertPool(),
|
||||
}
|
||||
for _, cert := range state.PeerCertificates[1:] {
|
||||
verifyOptions.Intermediates.AddCert(cert)
|
||||
}
|
||||
if timeFunc != nil {
|
||||
verifyOptions.CurrentTime = timeFunc()
|
||||
}
|
||||
_, err := state.PeerCertificates[0].Verify(verifyOptions)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) error {
|
||||
leafCertificate, err := x509.ParseCertificate(rawCerts[0])
|
||||
if err != nil {
|
||||
|
||||
@@ -92,6 +92,7 @@ func getACMENextProtos(provider adapter.CertificateProvider) []string {
|
||||
type STDServerConfig struct {
|
||||
access sync.RWMutex
|
||||
config *tls.Config
|
||||
handshakeTimeout time.Duration
|
||||
logger log.Logger
|
||||
certificateProvider managedCertificateProvider
|
||||
acmeService adapter.SimpleLifecycle
|
||||
@@ -139,6 +140,18 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.config = config
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) HandshakeTimeout() time.Duration {
|
||||
c.access.RLock()
|
||||
defer c.access.RUnlock()
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) hasACMEALPN() bool {
|
||||
if c.acmeService != nil {
|
||||
return true
|
||||
@@ -165,7 +178,8 @@ func (c *STDServerConfig) Server(conn net.Conn) (Conn, error) {
|
||||
|
||||
func (c *STDServerConfig) Clone() Config {
|
||||
return &STDServerConfig{
|
||||
config: c.config.Clone(),
|
||||
config: c.config.Clone(),
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,8 +485,15 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
serverConfig := &STDServerConfig{
|
||||
config: tlsConfig,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
logger: logger,
|
||||
certificateProvider: certificateProvider,
|
||||
acmeService: acmeService,
|
||||
|
||||
@@ -28,6 +28,10 @@ import (
|
||||
type UTLSClientConfig struct {
|
||||
ctx context.Context
|
||||
config *utls.Config
|
||||
serverName string
|
||||
disableSNI bool
|
||||
verifyServerName bool
|
||||
handshakeTimeout time.Duration
|
||||
id utls.ClientHelloID
|
||||
fragment bool
|
||||
fragmentFallbackDelay time.Duration
|
||||
@@ -35,10 +39,20 @@ type UTLSClientConfig struct {
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) ServerName() string {
|
||||
return c.config.ServerName
|
||||
return c.serverName
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) SetServerName(serverName string) {
|
||||
c.serverName = serverName
|
||||
if c.disableSNI {
|
||||
c.config.ServerName = ""
|
||||
if c.verifyServerName {
|
||||
c.config.InsecureServerNameToVerify = serverName
|
||||
} else {
|
||||
c.config.InsecureServerNameToVerify = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
c.config.ServerName = serverName
|
||||
}
|
||||
|
||||
@@ -53,6 +67,14 @@ func (c *UTLSClientConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) HandshakeTimeout() time.Duration {
|
||||
return c.handshakeTimeout
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
||||
c.handshakeTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return nil, E.New("unsupported usage for uTLS")
|
||||
}
|
||||
@@ -69,9 +91,20 @@ func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []by
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) Clone() Config {
|
||||
return &UTLSClientConfig{
|
||||
c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment,
|
||||
cloned := &UTLSClientConfig{
|
||||
ctx: c.ctx,
|
||||
config: c.config.Clone(),
|
||||
serverName: c.serverName,
|
||||
disableSNI: c.disableSNI,
|
||||
verifyServerName: c.verifyServerName,
|
||||
handshakeTimeout: c.handshakeTimeout,
|
||||
id: c.id,
|
||||
fragment: c.fragment,
|
||||
fragmentFallbackDelay: c.fragmentFallbackDelay,
|
||||
recordFragment: c.recordFragment,
|
||||
}
|
||||
cloned.SetServerName(cloned.serverName)
|
||||
return cloned
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) ECHConfigList() []byte {
|
||||
@@ -143,29 +176,29 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newUTLSClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
} else if serverAddress != "" {
|
||||
serverName = serverAddress
|
||||
}
|
||||
if serverName == "" && !options.Insecure {
|
||||
return nil, E.New("missing server_name or insecure=true")
|
||||
if serverName == "" && !options.Insecure && !allowEmptyServerName {
|
||||
return nil, errMissingServerName
|
||||
}
|
||||
|
||||
var tlsConfig utls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if !options.DisableSNI {
|
||||
tlsConfig.ServerName = serverName
|
||||
}
|
||||
if options.Insecure {
|
||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
||||
} else if options.DisableSNI {
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return nil, E.New("disable_sni is unsupported in reality")
|
||||
}
|
||||
tlsConfig.InsecureServerNameToVerify = serverName
|
||||
}
|
||||
if len(options.CertificatePublicKeySHA256) > 0 {
|
||||
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
||||
@@ -251,11 +284,29 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre
|
||||
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
|
||||
return nil, E.New("client certificate and client key must be provided together")
|
||||
}
|
||||
var handshakeTimeout time.Duration
|
||||
if options.HandshakeTimeout > 0 {
|
||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
||||
} else {
|
||||
handshakeTimeout = C.TCPTimeout
|
||||
}
|
||||
id, err := uTLSClientHelloID(options.UTLS.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var config Config = &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
var config Config = &UTLSClientConfig{
|
||||
ctx: ctx,
|
||||
config: &tlsConfig,
|
||||
serverName: serverName,
|
||||
disableSNI: options.DisableSNI,
|
||||
verifyServerName: options.DisableSNI && !options.Insecure,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
id: id,
|
||||
fragment: options.Fragment,
|
||||
fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay),
|
||||
recordFragment: options.RecordFragment,
|
||||
}
|
||||
config.SetServerName(serverName)
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return nil, E.New("Reality is conflict with ECH")
|
||||
|
||||
@@ -12,10 +12,18 @@ import (
|
||||
)
|
||||
|
||||
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newUTLSClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
|
||||
}
|
||||
|
||||
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return newRealityClient(ctx, logger, serverAddress, options, false)
|
||||
}
|
||||
|
||||
func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
||||
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
|
||||
}
|
||||
|
||||
|
||||
@@ -139,9 +139,9 @@ type fakeRuleSet struct {
|
||||
beforeDecrementReference func()
|
||||
}
|
||||
|
||||
func (s *fakeRuleSet) Name() string { return "fake-rule-set" }
|
||||
func (s *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error { return nil }
|
||||
func (s *fakeRuleSet) PostStart() error { return nil }
|
||||
func (s *fakeRuleSet) Name() string { return "fake-rule-set" }
|
||||
func (s *fakeRuleSet) StartContext(context.Context) error { return nil }
|
||||
func (s *fakeRuleSet) PostStart() error { return nil }
|
||||
func (s *fakeRuleSet) Metadata() adapter.RuleSetMetadata {
|
||||
s.access.Lock()
|
||||
metadata := s.metadata
|
||||
|
||||
@@ -3,17 +3,18 @@ package transport
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/httpclient"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
@@ -44,14 +45,20 @@ type HTTPSTransport struct {
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
method string
|
||||
host string
|
||||
queryHeaders http.Header
|
||||
transportAccess sync.Mutex
|
||||
transport *HTTPSTransportWrapper
|
||||
transport *httpclient.Client
|
||||
transportResetAt time.Time
|
||||
}
|
||||
|
||||
func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||
remoteOptions := option.RemoteDNSServerOptions{
|
||||
DNSServerAddressOptions: options.DNSServerAddressOptions,
|
||||
}
|
||||
remoteOptions.DialerOptions = options.DialerOptions
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -62,28 +69,21 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
return nil, err
|
||||
}
|
||||
if len(tlsConfig.NextProtos()) == 0 {
|
||||
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
|
||||
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
|
||||
} else if !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||
tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, tlsConfig.NextProtos()...))
|
||||
}
|
||||
headers := options.Headers.Build()
|
||||
host := headers.Get("Host")
|
||||
if host != "" {
|
||||
headers.Del("Host")
|
||||
} else {
|
||||
if tlsConfig.ServerName() != "" {
|
||||
host = tlsConfig.ServerName()
|
||||
} else {
|
||||
host = options.Server
|
||||
}
|
||||
serverAddr := options.DNSServerAddressOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 443
|
||||
}
|
||||
if !serverAddr.IsValid() {
|
||||
return nil, E.New("invalid server address: ", serverAddr)
|
||||
}
|
||||
destinationURL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: host,
|
||||
}
|
||||
if destinationURL.Host == "" {
|
||||
destinationURL.Host = options.Server
|
||||
}
|
||||
if options.ServerPort != 0 && options.ServerPort != 443 {
|
||||
destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort)))
|
||||
Host: doHURLHost(serverAddr, 443),
|
||||
}
|
||||
path := options.Path
|
||||
if path == "" {
|
||||
@@ -93,41 +93,67 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverAddr := options.DNSServerAddressOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 443
|
||||
method := strings.ToUpper(options.Method)
|
||||
if method == "" {
|
||||
method = http.MethodPost
|
||||
}
|
||||
if !serverAddr.IsValid() {
|
||||
return nil, E.New("invalid server address: ", serverAddr)
|
||||
switch method {
|
||||
case http.MethodGet, http.MethodPost:
|
||||
default:
|
||||
return nil, E.New("unsupported HTTPS DNS method: ", options.Method)
|
||||
}
|
||||
return NewHTTPSRaw(
|
||||
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
|
||||
httpClientOptions := options.HTTPClientOptions
|
||||
return NewHTTPRaw(
|
||||
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, remoteOptions),
|
||||
logger,
|
||||
transportDialer,
|
||||
&destinationURL,
|
||||
headers,
|
||||
serverAddr,
|
||||
tlsConfig,
|
||||
), nil
|
||||
httpClientOptions,
|
||||
method,
|
||||
)
|
||||
}
|
||||
|
||||
func NewHTTPSRaw(
|
||||
func NewHTTPRaw(
|
||||
adapter dns.TransportAdapter,
|
||||
logger log.ContextLogger,
|
||||
logger logger.ContextLogger,
|
||||
dialer N.Dialer,
|
||||
destination *url.URL,
|
||||
headers http.Header,
|
||||
serverAddr M.Socksaddr,
|
||||
tlsConfig tls.Config,
|
||||
) *HTTPSTransport {
|
||||
httpClientOptions option.HTTPClientOptions,
|
||||
method string,
|
||||
) (*HTTPSTransport, error) {
|
||||
if destination.Scheme == "https" && tlsConfig == nil {
|
||||
return nil, E.New("TLS transport unavailable")
|
||||
}
|
||||
queryHeaders := headers.Clone()
|
||||
if queryHeaders == nil {
|
||||
queryHeaders = make(http.Header)
|
||||
}
|
||||
host := queryHeaders.Get("Host")
|
||||
queryHeaders.Del("Host")
|
||||
queryHeaders.Set("Accept", MimeType)
|
||||
if method == http.MethodPost {
|
||||
queryHeaders.Set("Content-Type", MimeType)
|
||||
}
|
||||
httpClientOptions.Tag = ""
|
||||
httpClientOptions.Headers = nil
|
||||
currentTransport, err := httpclient.NewClientWithDialer(dialer, tlsConfig, "", httpClientOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &HTTPSTransport{
|
||||
TransportAdapter: adapter,
|
||||
logger: logger,
|
||||
dialer: dialer,
|
||||
destination: destination,
|
||||
headers: headers,
|
||||
transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr),
|
||||
}
|
||||
method: method,
|
||||
host: host,
|
||||
queryHeaders: queryHeaders,
|
||||
transport: currentTransport,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
|
||||
@@ -181,14 +207,25 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
|
||||
requestBuffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
|
||||
requestURL := *t.destination
|
||||
var request *http.Request
|
||||
switch t.method {
|
||||
case http.MethodGet:
|
||||
query := requestURL.Query()
|
||||
query.Set("dns", base64.RawURLEncoding.EncodeToString(rawMessage))
|
||||
requestURL.RawQuery = query.Encode()
|
||||
request, err = http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), nil)
|
||||
default:
|
||||
request, err = http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(rawMessage))
|
||||
}
|
||||
if err != nil {
|
||||
requestBuffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
request.Header = t.headers.Clone()
|
||||
request.Header.Set("Content-Type", MimeType)
|
||||
request.Header.Set("Accept", MimeType)
|
||||
request.Header = t.queryHeaders.Clone()
|
||||
if t.host != "" {
|
||||
request.Host = t.host
|
||||
}
|
||||
t.transportAccess.Lock()
|
||||
currentTransport := t.transport
|
||||
t.transportAccess.Unlock()
|
||||
@@ -222,3 +259,13 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
|
||||
}
|
||||
return &responseMessage, nil
|
||||
}
|
||||
|
||||
func doHURLHost(serverAddr M.Socksaddr, defaultPort uint16) string {
|
||||
if serverAddr.Port != defaultPort {
|
||||
return serverAddr.String()
|
||||
}
|
||||
if serverAddr.IsIPv6() {
|
||||
return "[" + serverAddr.AddrString() + "]"
|
||||
}
|
||||
return serverAddr.AddrString()
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var errFallback = E.New("fallback to HTTP/1.1")
|
||||
|
||||
type HTTPSTransportWrapper struct {
|
||||
http2Transport *http2.Transport
|
||||
httpTransport *http.Transport
|
||||
fallback *atomic.Bool
|
||||
}
|
||||
|
||||
func NewHTTPSTransportWrapper(dialer tls.Dialer, serverAddr M.Socksaddr) *HTTPSTransportWrapper {
|
||||
var fallback atomic.Bool
|
||||
return &HTTPSTransportWrapper{
|
||||
http2Transport: &http2.Transport{
|
||||
DialTLSContext: func(ctx context.Context, _, _ string, _ *tls.STDConfig) (net.Conn, error) {
|
||||
tlsConn, err := dialer.DialTLSContext(ctx, serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state := tlsConn.ConnectionState()
|
||||
if state.NegotiatedProtocol == http2.NextProtoTLS {
|
||||
return tlsConn, nil
|
||||
}
|
||||
tlsConn.Close()
|
||||
fallback.Store(true)
|
||||
return nil, errFallback
|
||||
},
|
||||
},
|
||||
httpTransport: &http.Transport{
|
||||
DialTLSContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return dialer.DialTLSContext(ctx, serverAddr)
|
||||
},
|
||||
},
|
||||
fallback: &fallback,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTTPSTransportWrapper) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if h.fallback.Load() {
|
||||
return h.httpTransport.RoundTrip(request)
|
||||
} else {
|
||||
response, err := h.http2Transport.RoundTrip(request)
|
||||
if err != nil {
|
||||
if errors.Is(err, errFallback) {
|
||||
return h.httpTransport.RoundTrip(request)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTTPSTransportWrapper) CloseIdleConnections() {
|
||||
h.http2Transport.CloseIdleConnections()
|
||||
h.httpTransport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (h *HTTPSTransportWrapper) Clone() *HTTPSTransportWrapper {
|
||||
return &HTTPSTransportWrapper{
|
||||
httpTransport: h.httpTransport,
|
||||
http2Transport: &http2.Transport{
|
||||
DialTLSContext: h.http2Transport.DialTLSContext,
|
||||
},
|
||||
fallback: h.fallback,
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
@@ -40,18 +41,23 @@ func RegisterHTTP3Transport(registry *dns.TransportRegistry) {
|
||||
|
||||
type HTTP3Transport struct {
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
serverAddr M.Socksaddr
|
||||
tlsConfig *tls.STDConfig
|
||||
transportAccess sync.Mutex
|
||||
transport *http3.Transport
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
handshakeTimeout time.Duration
|
||||
serverAddr M.Socksaddr
|
||||
tlsConfig *tls.STDConfig
|
||||
transportAccess sync.Mutex
|
||||
transport *http3.Transport
|
||||
}
|
||||
|
||||
func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||
remoteOptions := option.RemoteDNSServerOptions{
|
||||
DNSServerAddressOptions: options.DNSServerAddressOptions,
|
||||
}
|
||||
remoteOptions.DialerOptions = options.DialerOptions
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -61,6 +67,7 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handshakeTimeout := tlsConfig.HandshakeTimeout()
|
||||
stdConfig, err := tlsConfig.STDConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -102,11 +109,12 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
return nil, E.New("invalid server address: ", serverAddr)
|
||||
}
|
||||
t := &HTTP3Transport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, remoteOptions),
|
||||
logger: logger,
|
||||
dialer: transportDialer,
|
||||
destination: &destinationURL,
|
||||
headers: headers,
|
||||
handshakeTimeout: handshakeTimeout,
|
||||
serverAddr: serverAddr,
|
||||
tlsConfig: stdConfig,
|
||||
}
|
||||
@@ -115,8 +123,17 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
}
|
||||
|
||||
func (t *HTTP3Transport) newTransport() *http3.Transport {
|
||||
quicConfig := &quic.Config{}
|
||||
if t.handshakeTimeout > 0 {
|
||||
quicConfig.HandshakeIdleTimeout = t.handshakeTimeout
|
||||
}
|
||||
return &http3.Transport{
|
||||
QUICConfig: quicConfig,
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (*quic.Conn, error) {
|
||||
if t.handshakeTimeout > 0 && cfg.HandshakeIdleTimeout == 0 {
|
||||
cfg = cfg.Clone()
|
||||
cfg.HandshakeIdleTimeout = t.handshakeTimeout
|
||||
}
|
||||
conn, dialErr := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
|
||||
if dialErr != nil {
|
||||
return nil, dialErr
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-alert: `headers`, `tls`, Dial Fields moved to [HTTP Client Fields](#http-client-fields)
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
# DNS over HTTP3 (DoH3)
|
||||
@@ -15,27 +19,20 @@ icon: material/new-box
|
||||
{
|
||||
"type": "h3",
|
||||
"tag": "",
|
||||
|
||||
|
||||
"server": "",
|
||||
"server_port": 443,
|
||||
|
||||
"server_port": 0,
|
||||
|
||||
"path": "",
|
||||
"headers": {},
|
||||
|
||||
"tls": {},
|
||||
|
||||
// Dial Fields
|
||||
"method": "",
|
||||
|
||||
... // HTTP Client Fields
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "Difference from legacy H3 server"
|
||||
|
||||
* The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
|
||||
* The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead.
|
||||
|
||||
### Fields
|
||||
|
||||
#### server
|
||||
@@ -58,14 +55,14 @@ The path of the DNS server.
|
||||
|
||||
`/dns-query` will be used by default.
|
||||
|
||||
#### headers
|
||||
#### method
|
||||
|
||||
Additional headers to be sent to the DNS server.
|
||||
HTTP request method.
|
||||
|
||||
#### tls
|
||||
Available values: `GET`, `POST`.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
`POST` will be used by default.
|
||||
|
||||
### Dial Fields
|
||||
### HTTP Client Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-alert: `headers`、`tls`、拨号字段已移至 [HTTP 客户端字段](#http-客户端字段)
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
# DNS over HTTP3 (DoH3)
|
||||
@@ -17,25 +21,18 @@ icon: material/new-box
|
||||
"tag": "",
|
||||
|
||||
"server": "",
|
||||
"server_port": 443,
|
||||
"server_port": 0,
|
||||
|
||||
"path": "",
|
||||
"headers": {},
|
||||
"method": "",
|
||||
|
||||
"tls": {},
|
||||
|
||||
// 拨号字段
|
||||
... // HTTP 客户端字段
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "与旧版 H3 服务器的区别"
|
||||
|
||||
* 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。
|
||||
* 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
@@ -58,14 +55,14 @@ DNS 服务器的路径。
|
||||
|
||||
默认使用 `/dns-query`。
|
||||
|
||||
#### headers
|
||||
#### method
|
||||
|
||||
发送到 DNS 服务器的额外标头。
|
||||
HTTP 请求方法。
|
||||
|
||||
#### tls
|
||||
可用值:`GET`、`POST`。
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
默认使用 `POST`。
|
||||
|
||||
### 拨号字段
|
||||
### HTTP 客户端字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-alert: `headers`, `tls`, Dial Fields moved to [HTTP Client Fields](#http-client-fields)
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
# DNS over HTTPS (DoH)
|
||||
@@ -15,27 +19,20 @@ icon: material/new-box
|
||||
{
|
||||
"type": "https",
|
||||
"tag": "",
|
||||
|
||||
|
||||
"server": "",
|
||||
"server_port": 443,
|
||||
|
||||
"server_port": 0,
|
||||
|
||||
"path": "",
|
||||
"headers": {},
|
||||
|
||||
"tls": {},
|
||||
|
||||
// Dial Fields
|
||||
"method": "",
|
||||
|
||||
... // HTTP Client Fields
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "Difference from legacy HTTPS server"
|
||||
|
||||
* The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
|
||||
* The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead.
|
||||
|
||||
### Fields
|
||||
|
||||
#### server
|
||||
@@ -58,14 +55,14 @@ The path of the DNS server.
|
||||
|
||||
`/dns-query` will be used by default.
|
||||
|
||||
#### headers
|
||||
#### method
|
||||
|
||||
Additional headers to be sent to the DNS server.
|
||||
HTTP request method.
|
||||
|
||||
#### tls
|
||||
Available values: `GET`, `POST`.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
`POST` will be used by default.
|
||||
|
||||
### Dial Fields
|
||||
### HTTP Client Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-alert: `headers`、`tls`、拨号字段已移至 [HTTP 客户端字段](#http-客户端字段)
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
# DNS over HTTPS (DoH)
|
||||
@@ -17,25 +21,18 @@ icon: material/new-box
|
||||
"tag": "",
|
||||
|
||||
"server": "",
|
||||
"server_port": 443,
|
||||
"server_port": 0,
|
||||
|
||||
"path": "",
|
||||
"headers": {},
|
||||
"method": "",
|
||||
|
||||
"tls": {},
|
||||
|
||||
// 拨号字段
|
||||
... // HTTP 客户端字段
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "与旧版 HTTPS 服务器的区别"
|
||||
|
||||
* 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。
|
||||
* 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
@@ -58,14 +55,14 @@ DNS 服务器的路径。
|
||||
|
||||
默认使用 `/dns-query`。
|
||||
|
||||
#### headers
|
||||
#### method
|
||||
|
||||
发送到 DNS 服务器的额外标头。
|
||||
HTTP 请求方法。
|
||||
|
||||
#### tls
|
||||
可用值:`GET`、`POST`。
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
默认使用 `POST`。
|
||||
|
||||
### 拨号字段
|
||||
### HTTP 客户端字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [control_http_client](#control_http_client)
|
||||
:material-delete-clock: [Dial Fields](#dial-fields)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [relay_server_port](#relay_server_port)
|
||||
@@ -22,6 +27,7 @@ icon: material/new-box
|
||||
"state_directory": "",
|
||||
"auth_key": "",
|
||||
"control_url": "",
|
||||
"control_http_client": {}, // or ""
|
||||
"ephemeral": false,
|
||||
"hostname": "",
|
||||
"accept_routes": false,
|
||||
@@ -148,10 +154,18 @@ UDP NAT expiration time.
|
||||
|
||||
`5m` will be used by default.
|
||||
|
||||
#### control_http_client
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
HTTP Client for connecting to the Tailscale control plane.
|
||||
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
!!! note
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Dial Fields in Tailscale endpoints only control how it connects to the control plane and have nothing to do with actual connections.
|
||||
Dial Fields in Tailscale endpoints are deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, use `control_http_client` instead.
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [control_http_client](#control_http_client)
|
||||
:material-delete-clock: [拨号字段](#拨号字段)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [relay_server_port](#relay_server_port)
|
||||
@@ -22,6 +27,7 @@ icon: material/new-box
|
||||
"state_directory": "",
|
||||
"auth_key": "",
|
||||
"control_url": "",
|
||||
"control_http_client": {}, // 或 ""
|
||||
"ephemeral": false,
|
||||
"hostname": "",
|
||||
"accept_routes": false,
|
||||
@@ -147,10 +153,18 @@ UDP NAT 过期时间。
|
||||
|
||||
默认使用 `5m`。
|
||||
|
||||
#### control_http_client
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
用于连接 Tailscale 控制平面的 HTTP 客户端。
|
||||
|
||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
!!! note
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
Tailscale 端点中的拨号字段仅控制它如何连接到控制平面,与实际连接无关。
|
||||
Tailscale 端点中的拨号字段已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,请使用 `control_http_client` 代替。
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||
|
||||
@@ -21,11 +21,16 @@
|
||||
}
|
||||
],
|
||||
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
// Deprecated
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window_client": 0,
|
||||
"max_conn_client": 0,
|
||||
"disable_mtu_discovery": false,
|
||||
"tls": {}
|
||||
"disable_mtu_discovery": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -76,32 +81,38 @@ Authentication password, in base64.
|
||||
|
||||
Authentication password.
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
The QUIC stream-level flow control window for receiving data.
|
||||
|
||||
`15728640 (15 MB/s)` will be used if empty.
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
The QUIC connection-level flow control window for receiving data.
|
||||
|
||||
`67108864 (64 MB/s)` will be used if empty.
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open.
|
||||
|
||||
`1024` will be used if empty.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
|
||||
|
||||
Force enabled on for systems other than Linux and Windows (according to upstream).
|
||||
|
||||
#### tls
|
||||
|
||||
==Required==
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
### Deprecated Fields
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `stream_receive_window` instead.
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `connection_receive_window` instead.
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `max_concurrent_streams` instead.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `disable_path_mtu_discovery` instead.
|
||||
@@ -21,11 +21,16 @@
|
||||
}
|
||||
],
|
||||
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
// 废弃的
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window_client": 0,
|
||||
"max_conn_client": 0,
|
||||
"disable_mtu_discovery": false,
|
||||
"tls": {}
|
||||
"disable_mtu_discovery": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -76,32 +81,38 @@ base64 编码的认证密码。
|
||||
|
||||
认证密码。
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
用于接收数据的 QUIC 流级流控制窗口。
|
||||
|
||||
默认 `15728640 (15 MB/s)`。
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
用于接收数据的 QUIC 连接级流控制窗口。
|
||||
|
||||
默认 `67108864 (64 MB/s)`。
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
允许对等点打开的 QUIC 并发双向流的最大数量。
|
||||
|
||||
默认 `1024`。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。
|
||||
|
||||
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
|
||||
|
||||
#### tls
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### 废弃字段
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `stream_receive_window` 代替。
|
||||
|
||||
#### recv_window_client
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `connection_receive_window` 代替。
|
||||
|
||||
#### max_conn_client
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `max_concurrent_streams` 代替。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `disable_path_mtu_discovery` 代替。
|
||||
@@ -34,6 +34,9 @@ icon: material/alert-decagram
|
||||
],
|
||||
"ignore_client_bandwidth": false,
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
"masquerade": "", // or {}
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false
|
||||
@@ -95,6 +98,10 @@ Deny clients to use the BBR CC.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
#### masquerade
|
||||
|
||||
HTTP3 server behavior (URL string configuration) when authentication fails.
|
||||
|
||||
@@ -34,6 +34,9 @@ icon: material/alert-decagram
|
||||
],
|
||||
"ignore_client_bandwidth": false,
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
"masquerade": "", // 或 {}
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false
|
||||
@@ -92,6 +95,10 @@ Hysteria 用户
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
#### masquerade
|
||||
|
||||
HTTP3 服务器认证失败时的行为 (URL 字符串配置)。
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
"auth_timeout": "3s",
|
||||
"zero_rtt_handshake": false,
|
||||
"heartbeat": "10s",
|
||||
"tls": {}
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,4 +77,8 @@ Interval for sending heartbeat packets for keeping the connection alive
|
||||
|
||||
==Required==
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
@@ -18,7 +18,9 @@
|
||||
"auth_timeout": "3s",
|
||||
"zero_rtt_handshake": false,
|
||||
"heartbeat": "10s",
|
||||
"tls": {}
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,4 +77,8 @@ QUIC 拥塞控制算法
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
@@ -10,6 +10,7 @@ sing-box uses JSON for configuration files.
|
||||
"ntp": {},
|
||||
"certificate": {},
|
||||
"certificate_providers": [],
|
||||
"http_clients": [],
|
||||
"endpoints": [],
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
@@ -28,6 +29,7 @@ sing-box uses JSON for configuration files.
|
||||
| `ntp` | [NTP](./ntp/) |
|
||||
| `certificate` | [Certificate](./certificate/) |
|
||||
| `certificate_providers` | [Certificate Provider](./shared/certificate-provider/) |
|
||||
| `http_clients` | [HTTP Client](./shared/http-client/) |
|
||||
| `endpoints` | [Endpoint](./endpoint/) |
|
||||
| `inbounds` | [Inbound](./inbound/) |
|
||||
| `outbounds` | [Outbound](./outbound/) |
|
||||
|
||||
@@ -10,6 +10,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||
"ntp": {},
|
||||
"certificate": {},
|
||||
"certificate_providers": [],
|
||||
"http_clients": [],
|
||||
"endpoints": [],
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
@@ -28,6 +29,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||
| `ntp` | [NTP](./ntp/) |
|
||||
| `certificate` | [证书](./certificate/) |
|
||||
| `certificate_providers` | [证书提供者](./shared/certificate-provider/) |
|
||||
| `http_clients` | [HTTP 客户端](./shared/http-client/) |
|
||||
| `endpoints` | [端点](./endpoint/) |
|
||||
| `inbounds` | [入站](./inbound/) |
|
||||
| `outbounds` | [出站](./outbound/) |
|
||||
|
||||
@@ -27,13 +27,18 @@ icon: material/new-box
|
||||
"obfs": "fuck me till the daylight",
|
||||
"auth": "",
|
||||
"auth_str": "password",
|
||||
"network": "",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
... // Dial Fields
|
||||
|
||||
// Deprecated
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window": 0,
|
||||
"disable_mtu_discovery": false,
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // Dial Fields
|
||||
"disable_mtu_discovery": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -104,24 +109,6 @@ Authentication password, in base64.
|
||||
|
||||
Authentication password.
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
The QUIC stream-level flow control window for receiving data.
|
||||
|
||||
`15728640 (15 MB/s)` will be used if empty.
|
||||
|
||||
#### recv_window
|
||||
|
||||
The QUIC connection-level flow control window for receiving data.
|
||||
|
||||
`67108864 (64 MB/s)` will be used if empty.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
|
||||
|
||||
Force enabled on for systems other than Linux and Windows (according to upstream).
|
||||
|
||||
#### network
|
||||
|
||||
Enabled network
|
||||
@@ -136,6 +123,30 @@ Both is enabled by default.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
### Deprecated Fields
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `stream_receive_window` instead.
|
||||
|
||||
#### recv_window
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `connection_receive_window` instead.
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Use QUIC fields `disable_path_mtu_discovery` instead.
|
||||
|
||||
@@ -27,13 +27,18 @@ icon: material/new-box
|
||||
"obfs": "fuck me till the daylight",
|
||||
"auth": "",
|
||||
"auth_str": "password",
|
||||
"network": "",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
... // 拨号字段
|
||||
|
||||
// 废弃的
|
||||
|
||||
"recv_window_conn": 0,
|
||||
"recv_window": 0,
|
||||
"disable_mtu_discovery": false,
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // 拨号字段
|
||||
"disable_mtu_discovery": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -104,24 +109,6 @@ base64 编码的认证密码。
|
||||
|
||||
认证密码。
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
用于接收数据的 QUIC 流级流控制窗口。
|
||||
|
||||
默认 `15728640 (15 MB/s)`。
|
||||
|
||||
#### recv_window
|
||||
|
||||
用于接收数据的 QUIC 连接级流控制窗口。
|
||||
|
||||
默认 `67108864 (64 MB/s)`。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。
|
||||
|
||||
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
|
||||
|
||||
#### network
|
||||
|
||||
启用的网络协议。
|
||||
@@ -136,7 +123,30 @@ base64 编码的认证密码。
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||
|
||||
### 废弃字段
|
||||
|
||||
#### recv_window_conn
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `stream_receive_window` 代替。
|
||||
|
||||
#### recv_window
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `connection_receive_window` 代替。
|
||||
|
||||
#### disable_mtu_discovery
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
请使用 QUIC 字段 `disable_path_mtu_discovery` 代替。
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
"password": "goofy_ahh_password",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false,
|
||||
|
||||
@@ -124,6 +127,10 @@ Both is enabled by default.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
#### bbr_profile
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
"password": "goofy_ahh_password",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false,
|
||||
|
||||
@@ -122,6 +125,10 @@ QUIC 流量混淆器密码.
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
#### bbr_profile
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
"heartbeat": "10s",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
|
||||
... // QUIC Fields
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
```
|
||||
@@ -91,6 +93,10 @@ Both is enabled by default.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
### QUIC Fields
|
||||
|
||||
See [QUIC Fields](/configuration/shared/quic/) for details.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
"heartbeat": "10s",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
|
||||
|
||||
... // QUIC 字段
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
```
|
||||
@@ -99,6 +101,10 @@ UDP 包中继模式
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### QUIC 字段
|
||||
|
||||
参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||
|
||||
@@ -6,6 +6,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [default_http_client](#default_http_client)
|
||||
:material-plus: [find_neighbor](#find_neighbor)
|
||||
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
|
||||
|
||||
@@ -43,6 +44,7 @@ icon: material/alert-decagram
|
||||
"find_process": false,
|
||||
"find_neighbor": false,
|
||||
"dhcp_lease_files": [],
|
||||
"default_http_client": "",
|
||||
"default_domain_resolver": "", // or {}
|
||||
"default_network_strategy": "",
|
||||
"default_network_type": [],
|
||||
@@ -147,6 +149,14 @@ Custom DHCP lease file paths for hostname and MAC address resolution.
|
||||
|
||||
Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea) if empty.
|
||||
|
||||
#### default_http_client
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Tag of the default [HTTP Client](/configuration/shared/http-client/) used by remote rule-sets.
|
||||
|
||||
If empty and `http_clients` is defined, the first HTTP client is used.
|
||||
|
||||
#### default_domain_resolver
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
@@ -6,6 +6,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [default_http_client](#default_http_client)
|
||||
:material-plus: [find_neighbor](#find_neighbor)
|
||||
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
|
||||
|
||||
@@ -45,6 +46,7 @@ icon: material/alert-decagram
|
||||
"find_process": false,
|
||||
"find_neighbor": false,
|
||||
"dhcp_lease_files": [],
|
||||
"default_http_client": "",
|
||||
"default_network_strategy": "",
|
||||
"default_fallback_delay": ""
|
||||
}
|
||||
@@ -146,6 +148,14 @@ icon: material/alert-decagram
|
||||
|
||||
为空时自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。
|
||||
|
||||
#### default_http_client
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
远程规则集使用的默认 [HTTP 客户端](/zh/configuration/shared/http-client/) 的标签。
|
||||
|
||||
如果为空且 `http_clients` 已定义,将使用第一个 HTTP 客户端。
|
||||
|
||||
#### default_domain_resolver
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [http_client](#http_client)
|
||||
:material-delete-clock: [download_detour](#download_detour)
|
||||
|
||||
!!! quote "Changes in sing-box 1.10.0"
|
||||
|
||||
:material-plus: `type: inline`
|
||||
@@ -43,8 +48,12 @@
|
||||
"tag": "",
|
||||
"format": "source", // or binary
|
||||
"url": "",
|
||||
"download_detour": "", // optional
|
||||
"update_interval": "" // optional
|
||||
"http_client": "", // or {}
|
||||
"update_interval": "",
|
||||
|
||||
// Deprecated
|
||||
|
||||
"download_detour": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -102,14 +111,26 @@ File path of rule-set.
|
||||
|
||||
Download URL of rule-set.
|
||||
|
||||
#### download_detour
|
||||
#### http_client
|
||||
|
||||
Tag of the outbound to download rule-set.
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Default outbound will be used if empty.
|
||||
HTTP Client for downloading rule-set.
|
||||
|
||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
||||
|
||||
Default transport will be used if empty.
|
||||
|
||||
#### update_interval
|
||||
|
||||
Update interval of rule-set.
|
||||
|
||||
`1d` will be used if empty.
|
||||
|
||||
#### download_detour
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`download_detour` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, use `http_client` instead.
|
||||
|
||||
Tag of the outbound to download rule-set.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user