From 41452efebbe2cae08ec494fafb18377b97b590bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 9 Nov 2023 16:59:44 +0800 Subject: [PATCH 001/103] Migrate to gobwas/ws --- common/dialer/detour.go | 10 ++- experimental/clashapi/api_meta.go | 17 ++-- experimental/clashapi/connections.go | 9 +- experimental/clashapi/server.go | 38 ++++----- go.mod | 4 +- go.sum | 9 +- transport/v2ray/transport.go | 2 +- transport/v2rayhttp/client.go | 2 +- transport/v2raywebsocket/client.go | 94 +++++++++++---------- transport/v2raywebsocket/conn.go | 120 +++++++++++++++++---------- transport/v2raywebsocket/mask.go | 6 -- transport/v2raywebsocket/server.go | 13 +-- transport/v2raywebsocket/writer.go | 27 ++---- 13 files changed, 191 insertions(+), 160 deletions(-) delete mode 100644 transport/v2raywebsocket/mask.go diff --git a/common/dialer/detour.go b/common/dialer/detour.go index 81600913d4..ff484da29e 100644 --- a/common/dialer/detour.go +++ b/common/dialer/detour.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/bufio/deadline" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -44,7 +45,14 @@ func (d *DetourDialer) DialContext(ctx context.Context, network string, destinat if err != nil { return nil, err } - return dialer.DialContext(ctx, network, destination) + conn, err := dialer.DialContext(ctx, network, destination) + if err != nil { + return nil, err + } + if deadline.NeedAdditionalReadDeadline(conn) { + conn = deadline.NewConn(conn) + } + return conn, nil } func (d *DetourDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { diff --git a/experimental/clashapi/api_meta.go b/experimental/clashapi/api_meta.go index bfdee1b85b..876f98695d 100644 --- a/experimental/clashapi/api_meta.go +++ b/experimental/clashapi/api_meta.go @@ -2,12 +2,14 @@ package clashapi import ( "bytes" + "net" "net/http" "time" "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" - "github.com/sagernet/websocket" + "github.com/sagernet/ws" + "github.com/sagernet/ws/wsutil" "github.com/go-chi/chi/v5" "github.com/go-chi/render" @@ -27,16 +29,16 @@ type Memory struct { func memory(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - var wsConn *websocket.Conn - if websocket.IsWebSocketUpgrade(r) { + var conn net.Conn + if r.Header.Get("Upgrade") == "websocket" { var err error - wsConn, err = upgrader.Upgrade(w, r, nil) + conn, _, _, err = ws.UpgradeHTTP(r, w) if err != nil { return } } - if wsConn == nil { + if conn == nil { w.Header().Set("Content-Type", "application/json") render.Status(r, http.StatusOK) } @@ -63,13 +65,12 @@ func memory(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r }); err != nil { break } - if wsConn == nil { + if conn == nil { _, err = w.Write(buf.Bytes()) w.(http.Flusher).Flush() } else { - err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()) + err = wsutil.WriteServerText(conn, buf.Bytes()) } - if err != nil { break } diff --git a/experimental/clashapi/connections.go b/experimental/clashapi/connections.go index 94cfb9a37a..042bdd3645 100644 --- a/experimental/clashapi/connections.go +++ b/experimental/clashapi/connections.go @@ -9,7 +9,8 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" - "github.com/sagernet/websocket" + "github.com/sagernet/ws" + "github.com/sagernet/ws/wsutil" "github.com/go-chi/chi/v5" "github.com/go-chi/render" @@ -25,13 +26,13 @@ func connectionRouter(router adapter.Router, trafficManager *trafficontrol.Manag func getConnections(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if !websocket.IsWebSocketUpgrade(r) { + if r.Header.Get("Upgrade") != "websocket" { snapshot := trafficManager.Snapshot() render.JSON(w, r, snapshot) return } - conn, err := upgrader.Upgrade(w, r, nil) + conn, _, _, err := ws.UpgradeHTTP(r, w) if err != nil { return } @@ -56,7 +57,7 @@ func getConnections(trafficManager *trafficontrol.Manager) func(w http.ResponseW if err := json.NewEncoder(buf).Encode(snapshot); err != nil { return err } - return conn.WriteMessage(websocket.TextMessage, buf.Bytes()) + return wsutil.WriteServerText(conn, buf.Bytes()) } if err = sendSnapshot(); err != nil { diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 9f4b0f7cc3..6a3d6f66f7 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -25,7 +25,8 @@ import ( N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/filemanager" - "github.com/sagernet/websocket" + "github.com/sagernet/ws" + "github.com/sagernet/ws/wsutil" "github.com/go-chi/chi/v5" "github.com/go-chi/cors" @@ -314,7 +315,7 @@ func authentication(serverSecret string) func(next http.Handler) http.Handler { } // Browser websocket not support custom header - if websocket.IsWebSocketUpgrade(r) && r.URL.Query().Get("token") != "" { + if r.Header.Get("Upgrade") == "websocket" && r.URL.Query().Get("token") != "" { token := r.URL.Query().Get("token") if token != serverSecret { render.Status(r, http.StatusUnauthorized) @@ -351,12 +352,6 @@ func hello(redirect bool) func(w http.ResponseWriter, r *http.Request) { } } -var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, -} - type Traffic struct { Up int64 `json:"up"` Down int64 `json:"down"` @@ -364,16 +359,17 @@ type Traffic struct { func traffic(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - var wsConn *websocket.Conn - if websocket.IsWebSocketUpgrade(r) { + var conn net.Conn + if r.Header.Get("Upgrade") == "websocket" { var err error - wsConn, err = upgrader.Upgrade(w, r, nil) + conn, _, _, err = ws.UpgradeHTTP(r, w) if err != nil { return } + defer conn.Close() } - if wsConn == nil { + if conn == nil { w.Header().Set("Content-Type", "application/json") render.Status(r, http.StatusOK) } @@ -392,11 +388,11 @@ func traffic(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, break } - if wsConn == nil { + if conn == nil { _, err = w.Write(buf.Bytes()) w.(http.Flusher).Flush() } else { - err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()) + err = wsutil.WriteServerText(conn, buf.Bytes()) } if err != nil { @@ -432,16 +428,16 @@ func getLogs(logFactory log.ObservableFactory) func(w http.ResponseWriter, r *ht } defer logFactory.UnSubscribe(subscription) - var wsConn *websocket.Conn - if websocket.IsWebSocketUpgrade(r) { - var err error - wsConn, err = upgrader.Upgrade(w, r, nil) + var conn net.Conn + if r.Header.Get("Upgrade") == "websocket" { + conn, _, _, err = ws.UpgradeHTTP(r, w) if err != nil { return } + defer conn.Close() } - if wsConn == nil { + if conn == nil { w.Header().Set("Content-Type", "application/json") render.Status(r, http.StatusOK) } @@ -465,11 +461,11 @@ func getLogs(logFactory log.ObservableFactory) func(w http.ResponseWriter, r *ht if err != nil { break } - if wsConn == nil { + if conn == nil { _, err = w.Write(buf.Bytes()) w.(http.Flusher).Flush() } else { - err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()) + err = wsutil.WriteServerText(conn, buf.Bytes()) } if err != nil { diff --git a/go.mod b/go.mod index 0106737a6a..f26202f9e2 100644 --- a/go.mod +++ b/go.mod @@ -38,8 +38,8 @@ require ( github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 - github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f + github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.26.0 @@ -61,6 +61,8 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect diff --git a/go.sum b/go.sum index 10b96b00e0..d3c7797438 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,10 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -134,10 +138,10 @@ github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 h1:Px+hN4Vzgx+iCGV github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6/go.mod h1:zovq6vTvEM6ECiqE3Eeb9rpIylPpamPcmrJ9tv0Bt0M= github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 h1:kDUqhc9Vsk5HJuhfIATJ8oQwBmpOZJuozQG7Vk88lL4= github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM= -github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e h1:7uw2njHFGE+VpWamge6o56j2RWk4omF6uLKKxMmcWvs= -github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e/go.mod h1:45TUl8+gH4SIKr4ykREbxKWTxkDlSzFENzctB1dVRRY= github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f h1:Kvo8w8Y9lzFGB/7z09MJ3TR99TFtfI/IuY87Ygcycho= github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f/go.mod h1:mySs0abhpc/gLlvhoq7HP1RzOaRmIXVeZGCh++zoApk= +github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed h1:90a510OeE9siSJoYsI8nSjPmA+u5ROMDts/ZkdNsuXY= +github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= @@ -189,6 +193,7 @@ golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/transport/v2ray/transport.go b/transport/v2ray/transport.go index 3481c852c6..9dfee28119 100644 --- a/transport/v2ray/transport.go +++ b/transport/v2ray/transport.go @@ -50,7 +50,7 @@ func NewClientTransport(ctx context.Context, dialer N.Dialer, serverAddr M.Socks case C.V2RayTransportTypeGRPC: return NewGRPCClient(ctx, dialer, serverAddr, options.GRPCOptions, tlsConfig) case C.V2RayTransportTypeWebsocket: - return v2raywebsocket.NewClient(ctx, dialer, serverAddr, options.WebsocketOptions, tlsConfig), nil + return v2raywebsocket.NewClient(ctx, dialer, serverAddr, options.WebsocketOptions, tlsConfig) case C.V2RayTransportTypeQUIC: if tlsConfig == nil { return nil, C.ErrTLSRequired diff --git a/transport/v2rayhttp/client.go b/transport/v2rayhttp/client.go index d5e8a1f670..f280eeef55 100644 --- a/transport/v2rayhttp/client.go +++ b/transport/v2rayhttp/client.go @@ -81,7 +81,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt uri.Path = options.Path err := sHTTP.URLSetPath(&uri, options.Path) if err != nil { - return nil, E.New("failed to set path: " + err.Error()) + return nil, E.Cause(err, "parse path") } client.url = &uri return client, nil diff --git a/transport/v2raywebsocket/client.go b/transport/v2raywebsocket/client.go index 9f14f676e6..54c4df0b6e 100644 --- a/transport/v2raywebsocket/client.go +++ b/transport/v2raywebsocket/client.go @@ -5,58 +5,37 @@ import ( "net" "net/http" "net/url" + "strings" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" "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" sHTTP "github.com/sagernet/sing/protocol/http" - "github.com/sagernet/websocket" + "github.com/sagernet/ws" ) var _ adapter.V2RayClientTransport = (*Client)(nil) type Client struct { - dialer *websocket.Dialer + dialer N.Dialer + tlsConfig tls.Config + serverAddr M.Socksaddr requestURL url.URL - requestURLString string headers http.Header maxEarlyData uint32 earlyDataHeaderName string } -func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayWebsocketOptions, tlsConfig tls.Config) adapter.V2RayClientTransport { - wsDialer := &websocket.Dialer{ - ReadBufferSize: 4 * 1024, - WriteBufferSize: 4 * 1024, - HandshakeTimeout: time.Second * 8, - } +func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayWebsocketOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { if tlsConfig != nil { if len(tlsConfig.NextProtos()) == 0 { tlsConfig.SetNextProtos([]string{"http/1.1"}) } - wsDialer.NetDialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - conn, err := dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) - if err != nil { - return nil, err - } - tlsConn, err := tls.ClientHandshake(ctx, conn, tlsConfig) - if err != nil { - return nil, err - } - return &deadConn{tlsConn}, nil - } - } else { - wsDialer.NetDialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - conn, err := dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) - if err != nil { - return nil, err - } - return &deadConn{conn}, nil - } } var requestURL url.URL if tlsConfig == nil { @@ -68,37 +47,68 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt requestURL.Path = options.Path err := sHTTP.URLSetPath(&requestURL, options.Path) if err != nil { - return nil + return nil, E.Cause(err, "parse path") + } + if !strings.HasPrefix(requestURL.Path, "/") { + requestURL.Path = "/" + requestURL.Path } headers := make(http.Header) for key, value := range options.Headers { headers[key] = value + if key == "Host" { + if len(value) > 1 { + return nil, E.New("multiple Host headers") + } + requestURL.Host = value[0] + } + } + if headers.Get("User-Agent") == "" { + headers.Set("User-Agent", "Go-http-client/1.1") } return &Client{ - wsDialer, + dialer, + tlsConfig, + serverAddr, requestURL, - requestURL.String(), headers, options.MaxEarlyData, options.EarlyDataHeaderName, + }, nil +} + +func (c *Client) dialContext(ctx context.Context, requestURL *url.URL, headers http.Header) (*WebsocketConn, error) { + conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.serverAddr) + if err != nil { + return nil, err + } + if c.tlsConfig != nil { + conn, err = tls.ClientHandshake(ctx, conn, c.tlsConfig) + if err != nil { + return nil, err + } + } + conn.SetDeadline(time.Now().Add(C.TCPTimeout)) + var protocols []string + if protocolHeader := headers.Get("Sec-WebSocket-Protocol"); protocolHeader != "" { + protocols = []string{protocolHeader} + headers.Del("Sec-WebSocket-Protocol") } + reader, _, err := ws.Dialer{Header: ws.HandshakeHeaderHTTP(headers), Protocols: protocols}.Upgrade(conn, requestURL) + conn.SetDeadline(time.Time{}) + if err != nil { + return nil, err + } + return NewConn(conn, reader, nil, ws.StateClientSide), nil } func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { if c.maxEarlyData <= 0 { - conn, response, err := c.dialer.DialContext(ctx, c.requestURLString, c.headers) - if err == nil { - return &WebsocketConn{Conn: conn, Writer: NewWriter(conn, false)}, nil + conn, err := c.dialContext(ctx, &c.requestURL, c.headers) + if err != nil { + return nil, err } - return nil, wrapDialError(response, err) + return conn, nil } else { return &EarlyWebsocketConn{Client: c, ctx: ctx, create: make(chan struct{})}, nil } } - -func wrapDialError(response *http.Response, err error) error { - if response == nil { - return err - } - return E.Extend(err, "HTTP ", response.StatusCode, " ", response.Status) -} diff --git a/transport/v2raywebsocket/conn.go b/transport/v2raywebsocket/conn.go index 6400b118d9..5b51e5c528 100644 --- a/transport/v2raywebsocket/conn.go +++ b/transport/v2raywebsocket/conn.go @@ -1,11 +1,11 @@ package v2raywebsocket import ( + "bufio" "context" "encoding/base64" "io" "net" - "net/http" "os" "sync" "time" @@ -13,50 +13,96 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/debug" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/websocket" + "github.com/sagernet/ws" + "github.com/sagernet/ws/wsutil" ) type WebsocketConn struct { - *websocket.Conn + net.Conn *Writer - remoteAddr net.Addr - reader io.Reader + state ws.State + reader *wsutil.Reader + controlHandler wsutil.FrameHandlerFunc + remoteAddr net.Addr } -func NewServerConn(wsConn *websocket.Conn, remoteAddr net.Addr) *WebsocketConn { +func NewConn(conn net.Conn, br *bufio.Reader, remoteAddr net.Addr, state ws.State) *WebsocketConn { + controlHandler := wsutil.ControlFrameHandler(conn, state) + var reader io.Reader + if br != nil && br.Buffered() > 0 { + reader = br + } else { + reader = conn + } return &WebsocketConn{ - Conn: wsConn, - remoteAddr: remoteAddr, - Writer: NewWriter(wsConn, true), + Conn: conn, + state: state, + reader: &wsutil.Reader{ + Source: reader, + State: state, + SkipHeaderCheck: !debug.Enabled, + OnIntermediate: controlHandler, + }, + controlHandler: controlHandler, + remoteAddr: remoteAddr, + Writer: NewWriter(conn, state), } } func (c *WebsocketConn) Close() error { - err := c.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(C.TCPTimeout)) - if err != nil { - return c.Conn.Close() + c.Conn.SetWriteDeadline(time.Now().Add(C.TCPTimeout)) + frame := ws.NewCloseFrame(ws.NewCloseFrameBody( + ws.StatusNormalClosure, "", + )) + if c.state == ws.StateClientSide { + frame = ws.MaskFrameInPlace(frame) } + ws.WriteFrame(c.Conn, frame) + c.Conn.Close() return nil } func (c *WebsocketConn) Read(b []byte) (n int, err error) { + var header ws.Header for { - if c.reader == nil { - _, c.reader, err = c.NextReader() + n, err = c.reader.Read(b) + if n > 0 { + err = nil + return + } + if !E.IsMulti(err, io.EOF, wsutil.ErrNoFrameAdvance) { + return + } + header, err = c.reader.NextFrame() + if err != nil { + return + } + if header.OpCode.IsControl() { + err = c.controlHandler(header, c.reader) if err != nil { - err = wrapError(err) return } + continue } - n, err = c.reader.Read(b) - if E.IsMulti(err, io.EOF) { - c.reader = nil + if header.OpCode&ws.OpBinary == 0 { + err = c.reader.Discard() + if err != nil { + return + } continue } - err = wrapError(err) + } +} + +func (c *WebsocketConn) Write(p []byte) (n int, err error) { + err = wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, p) + if err != nil { return } + n = len(p) + return } func (c *WebsocketConn) RemoteAddr() net.Addr { @@ -83,11 +129,7 @@ func (c *WebsocketConn) NeedAdditionalReadDeadline() bool { } func (c *WebsocketConn) Upstream() any { - return c.Conn.NetConn() -} - -func (c *WebsocketConn) UpstreamWriter() any { - return c.Writer + return c.Conn } type EarlyWebsocketConn struct { @@ -113,8 +155,7 @@ func (c *EarlyWebsocketConn) writeRequest(content []byte) error { var ( earlyData []byte lateData []byte - conn *websocket.Conn - response *http.Response + conn *WebsocketConn err error ) if len(content) > int(c.maxEarlyData) { @@ -128,23 +169,26 @@ func (c *EarlyWebsocketConn) writeRequest(content []byte) error { if c.earlyDataHeaderName == "" { requestURL := c.requestURL requestURL.Path += earlyDataString - conn, response, err = c.dialer.DialContext(c.ctx, requestURL.String(), c.headers) + conn, err = c.dialContext(c.ctx, &requestURL, c.headers) } else { headers := c.headers.Clone() headers.Set(c.earlyDataHeaderName, earlyDataString) - conn, response, err = c.dialer.DialContext(c.ctx, c.requestURLString, headers) + conn, err = c.dialContext(c.ctx, &c.requestURL, headers) } } else { - conn, response, err = c.dialer.DialContext(c.ctx, c.requestURLString, c.headers) + conn, err = c.dialContext(c.ctx, &c.requestURL, c.headers) } if err != nil { - return wrapDialError(response, err) + return err } - c.conn = &WebsocketConn{Conn: conn, Writer: NewWriter(conn, false)} if len(lateData) > 0 { - _, err = c.conn.Write(lateData) + _, err = conn.Write(lateData) + if err != nil { + return err + } } - return err + c.conn = conn + return nil } func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) { @@ -230,13 +274,3 @@ func (c *EarlyWebsocketConn) Upstream() any { func (c *EarlyWebsocketConn) LazyHeadroom() bool { return c.conn == nil } - -func wrapError(err error) error { - if websocket.IsCloseError(err, websocket.CloseNormalClosure) { - return io.EOF - } - if websocket.IsCloseError(err, websocket.CloseAbnormalClosure) { - return net.ErrClosed - } - return err -} diff --git a/transport/v2raywebsocket/mask.go b/transport/v2raywebsocket/mask.go deleted file mode 100644 index 01ea8437a1..0000000000 --- a/transport/v2raywebsocket/mask.go +++ /dev/null @@ -1,6 +0,0 @@ -package v2raywebsocket - -import _ "unsafe" - -//go:linkname maskBytes github.com/sagernet/websocket.maskBytes -func maskBytes(key [4]byte, pos int, b []byte) int diff --git a/transport/v2raywebsocket/server.go b/transport/v2raywebsocket/server.go index 9d8bc69af7..ae6e15f376 100644 --- a/transport/v2raywebsocket/server.go +++ b/transport/v2raywebsocket/server.go @@ -20,7 +20,7 @@ import ( N "github.com/sagernet/sing/common/network" aTLS "github.com/sagernet/sing/common/tls" sHttp "github.com/sagernet/sing/protocol/http" - "github.com/sagernet/websocket" + "github.com/sagernet/ws" ) var _ adapter.V2RayServerTransport = (*Server)(nil) @@ -58,13 +58,6 @@ func NewServer(ctx context.Context, options option.V2RayWebsocketOptions, tlsCon return server, nil } -var upgrader = websocket.Upgrader{ - HandshakeTimeout: C.TCPTimeout, - CheckOrigin: func(r *http.Request) bool { - return true - }, -} - func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { if s.maxEarlyData == 0 || s.earlyDataHeaderName != "" { if request.URL.Path != s.path { @@ -95,14 +88,14 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { s.invalidRequest(writer, request, http.StatusBadRequest, E.Cause(err, "decode early data")) return } - wsConn, err := upgrader.Upgrade(writer, request, nil) + wsConn, reader, _, err := ws.UpgradeHTTP(request, writer) if err != nil { s.invalidRequest(writer, request, 0, E.Cause(err, "upgrade websocket connection")) return } var metadata M.Metadata metadata.Source = sHttp.SourceAddress(request) - conn = NewServerConn(wsConn, metadata.Source.TCPAddr()) + conn = NewConn(wsConn, reader.Reader, metadata.Source.TCPAddr(), ws.StateServerSide) if len(earlyData) > 0 { conn = bufio.NewCachedConn(conn, buf.As(earlyData)) } diff --git a/transport/v2raywebsocket/writer.go b/transport/v2raywebsocket/writer.go index fbb61f0f4d..5bd0d0a14f 100644 --- a/transport/v2raywebsocket/writer.go +++ b/transport/v2raywebsocket/writer.go @@ -2,36 +2,27 @@ package v2raywebsocket import ( "encoding/binary" + "io" "math/rand" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/websocket" + "github.com/sagernet/ws" ) type Writer struct { - *websocket.Conn writer N.ExtendedWriter isServer bool } -func NewWriter(conn *websocket.Conn, isServer bool) *Writer { +func NewWriter(writer io.Writer, state ws.State) *Writer { return &Writer{ - conn, - bufio.NewExtendedWriter(conn.NetConn()), - isServer, + bufio.NewExtendedWriter(writer), + state == ws.StateServerSide, } } -func (w *Writer) Write(p []byte) (n int, err error) { - err = w.Conn.WriteMessage(websocket.BinaryMessage, p) - if err != nil { - return - } - return len(p), nil -} - func (w *Writer) WriteBuffer(buffer *buf.Buffer) error { var payloadBitLength int dataLen := buffer.Len() @@ -52,7 +43,7 @@ func (w *Writer) WriteBuffer(buffer *buf.Buffer) error { } header := buffer.ExtendHeader(headerLen) - header[0] = websocket.BinaryMessage | 1<<7 + header[0] = byte(ws.OpBinary) | 0x80 if w.isServer { header[1] = 0 } else { @@ -72,16 +63,12 @@ func (w *Writer) WriteBuffer(buffer *buf.Buffer) error { if !w.isServer { maskKey := rand.Uint32() binary.BigEndian.PutUint32(header[1+payloadBitLength:], maskKey) - maskBytes(*(*[4]byte)(header[1+payloadBitLength:]), 0, data) + ws.Cipher(data, *(*[4]byte)(header[1+payloadBitLength:]), 0) } return w.writer.WriteBuffer(buffer) } -func (w *Writer) Upstream() any { - return w.Conn.NetConn() -} - func (w *Writer) FrontHeadroom() int { return 14 } From a19fbbabc08223c6d434def5601bacc1e530fc1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 30 Oct 2023 12:00:00 +0800 Subject: [PATCH 002/103] Add udp_disable_domain_unmapping inbound listen option --- docs/configuration/shared/listen.md | 54 +++++++++++++------------- docs/configuration/shared/listen.zh.md | 38 ++++++++---------- go.mod | 2 +- go.sum | 4 +- option/inbound.go | 9 +++-- outbound/default.go | 10 +++-- 6 files changed, 58 insertions(+), 59 deletions(-) diff --git a/docs/configuration/shared/listen.md b/docs/configuration/shared/listen.md index d4a0e58ee3..c1b1ed3382 100644 --- a/docs/configuration/shared/listen.md +++ b/docs/configuration/shared/listen.md @@ -7,28 +7,26 @@ "tcp_fast_open": false, "tcp_multi_path": false, "udp_fragment": false, + "udp_timeout": 300, + "detour": "another-in", "sniff": false, "sniff_override_destination": false, "sniff_timeout": "300ms", "domain_strategy": "prefer_ipv6", - "udp_timeout": 300, - "proxy_protocol": false, - "proxy_protocol_accept_no_header": false, - "detour": "another-in" + "udp_disable_domain_unmapping": false } ``` ### Fields -| Field | Available Context | -|-----------------------------------|-------------------------------------------------------------------| -| `listen` | Needs to listen on TCP or UDP. | -| `listen_port` | Needs to listen on TCP or UDP. | -| `tcp_fast_open` | Needs to listen on TCP. | -| `tcp_multi_path` | Needs to listen on TCP. | -| `udp_timeout` | Needs to assemble UDP connections, currently Tun and Shadowsocks. | -| `proxy_protocol` | Needs to listen on TCP. | -| `proxy_protocol_accept_no_header` | When `proxy_protocol` enabled | +| Field | Available Context | +|--------------------------------|-------------------------------------------------------------------| +| `listen` | Needs to listen on TCP or UDP. | +| `listen_port` | Needs to listen on TCP or UDP. | +| `tcp_fast_open` | Needs to listen on TCP. | +| `tcp_multi_path` | Needs to listen on TCP. | +| `udp_timeout` | Needs to assemble UDP connections, currently Tun and Shadowsocks. | +| `udp_disable_domain_unmapping` | Needs to listen on UDP and accept domain UDP addresses. | #### listen @@ -56,6 +54,16 @@ Enable TCP Multi Path. Enable UDP fragmentation. +#### udp_timeout + +UDP NAT expiration time in seconds, default is 300 (5 minutes). + +#### detour + +If set, connections will be forwarded to the specified inbound. + +Requires target inbound support, see [Injectable](/configuration/inbound/#fields). + #### sniff Enable sniffing. @@ -82,20 +90,10 @@ If set, the requested domain name will be resolved to IP before routing. If `sniff_override_destination` is in effect, its value will be taken as a fallback. -#### udp_timeout - -UDP NAT expiration time in seconds, default is 300 (5 minutes). - -#### proxy_protocol - -Parse [Proxy Protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) in the connection header. +#### udp_disable_domain_unmapping -#### proxy_protocol_accept_no_header - -Accept connections without Proxy Protocol header. - -#### detour - -If set, connections will be forwarded to the specified inbound. +If enabled, for UDP proxy requests addressed to a domain, +the original packet address will be sent in the response instead of the mapped domain. -Requires target inbound support, see [Injectable](/configuration/inbound/#fields). \ No newline at end of file +This option is used for compatibility with clients that +do not support receiving UDP packets with domain addresses, such as Surge. diff --git a/docs/configuration/shared/listen.zh.md b/docs/configuration/shared/listen.zh.md index b25ce295c0..b7fd74870a 100644 --- a/docs/configuration/shared/listen.zh.md +++ b/docs/configuration/shared/listen.zh.md @@ -7,14 +7,13 @@ "tcp_fast_open": false, "tcp_multi_path": false, "udp_fragment": false, + "udp_timeout": 300, + "detour": "another-in", "sniff": false, "sniff_override_destination": false, "sniff_timeout": "300ms", "domain_strategy": "prefer_ipv6", - "udp_timeout": 300, - "proxy_protocol": false, - "proxy_protocol_accept_no_header": false, - "detour": "another-in" + "udp_disable_domain_unmapping": false } ``` @@ -26,8 +25,7 @@ | `tcp_fast_open` | 需要监听 TCP。 | | `tcp_multi_path` | 需要监听 TCP。 | | `udp_timeout` | 需要组装 UDP 连接, 当前为 Tun 和 Shadowsocks。 | -| `proxy_protocol` | 需要监听 TCP。 | -| `proxy_protocol_accept_no_header` | `proxy_protocol` 启用时 | +| ### 字段 @@ -57,6 +55,16 @@ 启用 UDP 分段。 +#### udp_timeout + +UDP NAT 过期时间,以秒为单位,默认为 300(5 分钟)。 + +#### detour + +如果设置,连接将被转发到指定的入站。 + +需要目标入站支持,参阅 [注入支持](/zh/configuration/inbound/#_3)。 + #### sniff 启用协议探测。 @@ -83,20 +91,8 @@ 如果 `sniff_override_destination` 生效,它的值将作为后备。 -#### udp_timeout - -UDP NAT 过期时间,以秒为单位,默认为 300(5 分钟)。 - -#### proxy_protocol - -解析连接头中的 [代理协议](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)。 +#### udp_disable_domain_unmapping -#### proxy_protocol_accept_no_header - -接受没有代理协议标头的连接。 - -#### detour - -如果设置,连接将被转发到指定的入站。 +如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。 -需要目标入站支持,参阅 [注入支持](/zh/configuration/inbound/#_3)。 \ No newline at end of file +此选项用于兼容不支持接收带有域地址的 UDP 包的客户端,如 Surge。 diff --git a/go.mod b/go.mod index f26202f9e2..4af1845147 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.2.18-0.20231124115745-e50e7ae2d3e4 + github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc github.com/sagernet/sing-dns v0.1.11 github.com/sagernet/sing-mux v0.1.4 github.com/sagernet/sing-quic v0.1.5-0.20231123150204-077075e9b6ad diff --git a/go.sum b/go.sum index d3c7797438..9ed673394e 100644 --- a/go.sum +++ b/go.sum @@ -114,8 +114,8 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= -github.com/sagernet/sing v0.2.18-0.20231124115745-e50e7ae2d3e4 h1:Pew0S+oj/RLZ8zaxFGykDyQ6Pu5yiKAMDF/kqJcFAB8= -github.com/sagernet/sing v0.2.18-0.20231124115745-e50e7ae2d3e4/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= +github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc h1:vESVuxHgbd2EzHxd+TYTpNACIEGBOhp5n3KG7bgbcws= +github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE= github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE= github.com/sagernet/sing-mux v0.1.4 h1:BPNPOQr6HkXG3iY/BrfvUKUl+A7gYsGKVSxvoR3PO50= diff --git a/option/inbound.go b/option/inbound.go index 06408f58b2..c61428adb5 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -120,10 +120,11 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error { } type InboundOptions struct { - SniffEnabled bool `json:"sniff,omitempty"` - SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"` - SniffTimeout Duration `json:"sniff_timeout,omitempty"` - DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` + SniffEnabled bool `json:"sniff,omitempty"` + SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"` + SniffTimeout Duration `json:"sniff_timeout,omitempty"` + DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` + UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` } type ListenOptions struct { diff --git a/outbound/default.go b/outbound/default.go index 8067b6db5d..972aca9479 100644 --- a/outbound/default.go +++ b/outbound/default.go @@ -124,9 +124,13 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, } if destinationAddress.IsValid() { if metadata.Destination.IsFqdn() { - outConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) + if metadata.InboundOptions.UDPDisableDomainUnmapping { + outConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) + } else { + outConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) + } } - if natConn, loaded := common.Cast[*bufio.NATPacketConn](conn); loaded { + if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded { natConn.UpdateDestination(destinationAddress) } } @@ -170,7 +174,7 @@ func NewDirectPacketConnection(ctx context.Context, router adapter.Router, this if metadata.Destination.IsFqdn() { outConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) } - if natConn, loaded := common.Cast[*bufio.NATPacketConn](conn); loaded { + if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded { natConn.UpdateDestination(destinationAddress) } } From 39517da2c9847d58627ad18dc0f3417eebbe2ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 30 Oct 2023 12:00:00 +0800 Subject: [PATCH 003/103] Add exclude route support for tun & Update gVisor to 20231113.0 --- docs/configuration/inbound/tun.md | 14 +++++++++ docs/configuration/inbound/tun.zh.md | 14 +++++++++ experimental/libbox/service.go | 6 +++- experimental/libbox/tun.go | 9 ++++-- go.mod | 6 ++-- go.sum | 12 ++++---- inbound/tun.go | 36 ++++++++++++----------- option/tun.go | 44 +++++++++++++++------------- 8 files changed, 91 insertions(+), 50 deletions(-) diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 4c9670a27d..e6c52c54c1 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -22,6 +22,12 @@ "::/1", "8000::/1" ], + "inet4_route_exclude_address": [ + "192.168.0.0/16" + ], + "inet6_route_exclude_address": [ + "fc00::/7" + ], "endpoint_independent_nat": false, "stack": "system", "include_interface": [ @@ -130,6 +136,14 @@ Use custom routes instead of default when `auto_route` is enabled. Use custom routes instead of default when `auto_route` is enabled. +#### inet4_route_exclude_address + +Exclude custom routes when `auto_route` is enabled. + +#### inet6_route_exclude_address + +Exclude custom routes when `auto_route` is enabled. + #### endpoint_independent_nat !!! info "" diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index fbd10abf7e..8f246c042b 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -22,6 +22,12 @@ "::/1", "8000::/1" ], + "inet4_route_exclude_address": [ + "192.168.0.0/16" + ], + "inet6_route_exclude_address": [ + "fc00::/7" + ], "endpoint_independent_nat": false, "stack": "system", "include_interface": [ @@ -131,6 +137,14 @@ tun 接口的 IPv6 前缀。 启用 `auto_route` 时使用自定义路由而不是默认路由。 +#### inet4_route_exclude_address + +启用 `auto_route` 时排除自定义路由。 + +#### inet6_route_exclude_address + +启用 `auto_route` 时排除自定义路由。 + #### endpoint_independent_nat 启用独立于端点的 NAT。 diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 67b57d7da5..ce4d6d2aad 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -122,7 +122,11 @@ func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions if len(options.IncludeAndroidUser) > 0 { return nil, E.New("android: unsupported android_user option") } - tunFd, err := w.iif.OpenTun(&tunOptions{options, platformOptions}) + routeRanges, err := options.BuildAutoRouteRanges(true) + if err != nil { + return nil, err + } + tunFd, err := w.iif.OpenTun(&tunOptions{options, routeRanges, platformOptions}) if err != nil { return nil, err } diff --git a/experimental/libbox/tun.go b/experimental/libbox/tun.go index e692a5d651..e40ad58b46 100644 --- a/experimental/libbox/tun.go +++ b/experimental/libbox/tun.go @@ -60,6 +60,7 @@ var _ TunOptions = (*tunOptions)(nil) type tunOptions struct { *tun.Options + routeRanges []netip.Prefix option.TunPlatformOptions } @@ -91,11 +92,15 @@ func (o *tunOptions) GetStrictRoute() bool { } func (o *tunOptions) GetInet4RouteAddress() RoutePrefixIterator { - return mapRoutePrefix(o.Inet4RouteAddress) + return mapRoutePrefix(common.Filter(o.routeRanges, func(it netip.Prefix) bool { + return it.Addr().Is4() + })) } func (o *tunOptions) GetInet6RouteAddress() RoutePrefixIterator { - return mapRoutePrefix(o.Inet6RouteAddress) + return mapRoutePrefix(common.Filter(o.routeRanges, func(it netip.Prefix) bool { + return it.Addr().Is6() + })) } func (o *tunOptions) GetIncludePackage() StringIterator { diff --git a/go.mod b/go.mod index 4af1845147..e61889528a 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a github.com/sagernet/gomobile v0.0.0-20230915142329-c6740b6d2950 - github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab + github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc @@ -33,7 +33,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.5 github.com/sagernet/sing-shadowsocks2 v0.1.5 github.com/sagernet/sing-shadowtls v0.1.4 - github.com/sagernet/sing-tun v0.1.20 + github.com/sagernet/sing-tun v0.1.21-0.20231119035513-f6ea97c5af71 github.com/sagernet/sing-vmess v0.1.8 github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 @@ -89,7 +89,7 @@ require ( golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.3.0 // indirect + golang.org/x/time v0.4.0 // indirect golang.org/x/tools v0.15.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect diff --git a/go.sum b/go.sum index 9ed673394e..cf42420837 100644 --- a/go.sum +++ b/go.sum @@ -104,8 +104,8 @@ github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms= github.com/sagernet/gomobile v0.0.0-20230915142329-c6740b6d2950 h1:hUz/2mJLgi7l2H36JGpDY+jou9FmI6kAm0ZkU+xPpgE= github.com/sagernet/gomobile v0.0.0-20230915142329-c6740b6d2950/go.mod h1:5YE39YkJkCcMsfq1jMKkjsrM2GfBoF9JVWnvU89hmvU= -github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab h1:u+xQoi/Yc6bNUvTfrDD6HhGRybn2lzrhf5vmS+wb4Ho= -github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab/go.mod h1:3akUhSHSVtLuJaYcW5JPepUraBOW06Ibz2HKwaK5rOk= +github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 h1:dSPgjIw0CT6ISLeEh8Q20dZMBMFCcEceo23+LncRcNQ= +github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930/go.mod h1:JpKHkOYgh4wLwrX2BhH3ZIvCvazCkTnPeEcmigZJfHY= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460 h1:dAe4OIJAtE0nHOzTHhAReQteh3+sa63rvXbuIpbeOTY= @@ -128,8 +128,8 @@ github.com/sagernet/sing-shadowsocks2 v0.1.5 h1:JDeAJ4ZWlYZ7F6qEVdDKPhQEangxKw/J github.com/sagernet/sing-shadowsocks2 v0.1.5/go.mod h1:KF65y8lI5PGHyMgRZGYXYsH9ilgRc/yr+NYbSNGuBm4= github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= -github.com/sagernet/sing-tun v0.1.20 h1:vYWo/w6fkKc8I1WP/IB8eBWZVsGIC6eoEoNR6XqEDlY= -github.com/sagernet/sing-tun v0.1.20/go.mod h1:6kkPL/u9tWcLFfu55VbwMDnO++17cUihSmImkZjdZro= +github.com/sagernet/sing-tun v0.1.21-0.20231119035513-f6ea97c5af71 h1:WQi0TwhjbSNFFbxybIgAUSjVvo7uWSsLD28ldoM2avY= +github.com/sagernet/sing-tun v0.1.21-0.20231119035513-f6ea97c5af71/go.mod h1:hyzA4gDWbeg2SXklqPDswBKa//QcjlZqKw9aPcNdQ9A= github.com/sagernet/sing-vmess v0.1.8 h1:XVWad1RpTy9b5tPxdm5MCU8cGfrTGdR8qCq6HV2aCNc= github.com/sagernet/sing-vmess v0.1.8/go.mod h1:vhx32UNzTDUkNwOyIjcZQohre1CaytquC5mPplId8uA= github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 h1:HuE6xSwco/Xed8ajZ+coeYLmioq0Qp1/Z2zczFaV8as= @@ -202,8 +202,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= +golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= diff --git a/inbound/tun.go b/inbound/tun.go index 0b57482d39..7d1f519934 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -71,23 +71,25 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger logger: logger, inboundOptions: options.InboundOptions, tunOptions: tun.Options{ - Name: options.InterfaceName, - MTU: tunMTU, - Inet4Address: options.Inet4Address, - Inet6Address: options.Inet6Address, - AutoRoute: options.AutoRoute, - StrictRoute: options.StrictRoute, - IncludeInterface: options.IncludeInterface, - ExcludeInterface: options.ExcludeInterface, - Inet4RouteAddress: options.Inet4RouteAddress, - Inet6RouteAddress: options.Inet6RouteAddress, - IncludeUID: includeUID, - ExcludeUID: excludeUID, - IncludeAndroidUser: options.IncludeAndroidUser, - IncludePackage: options.IncludePackage, - ExcludePackage: options.ExcludePackage, - InterfaceMonitor: router.InterfaceMonitor(), - TableIndex: 2022, + Name: options.InterfaceName, + MTU: tunMTU, + Inet4Address: options.Inet4Address, + Inet6Address: options.Inet6Address, + AutoRoute: options.AutoRoute, + StrictRoute: options.StrictRoute, + IncludeInterface: options.IncludeInterface, + ExcludeInterface: options.ExcludeInterface, + Inet4RouteAddress: options.Inet4RouteAddress, + Inet6RouteAddress: options.Inet6RouteAddress, + Inet4RouteExcludeAddress: options.Inet4RouteExcludeAddress, + Inet6RouteExcludeAddress: options.Inet6RouteExcludeAddress, + IncludeUID: includeUID, + ExcludeUID: excludeUID, + IncludeAndroidUser: options.IncludeAndroidUser, + IncludePackage: options.IncludePackage, + ExcludePackage: options.ExcludePackage, + InterfaceMonitor: router.InterfaceMonitor(), + TableIndex: 2022, }, endpointIndependentNat: options.EndpointIndependentNat, udpTimeout: udpTimeout, diff --git a/option/tun.go b/option/tun.go index 4cf778041b..306d45415d 100644 --- a/option/tun.go +++ b/option/tun.go @@ -3,26 +3,28 @@ package option import "net/netip" type TunInboundOptions struct { - InterfaceName string `json:"interface_name,omitempty"` - MTU uint32 `json:"mtu,omitempty"` - Inet4Address Listable[netip.Prefix] `json:"inet4_address,omitempty"` - Inet6Address Listable[netip.Prefix] `json:"inet6_address,omitempty"` - AutoRoute bool `json:"auto_route,omitempty"` - StrictRoute bool `json:"strict_route,omitempty"` - Inet4RouteAddress Listable[netip.Prefix] `json:"inet4_route_address,omitempty"` - Inet6RouteAddress Listable[netip.Prefix] `json:"inet6_route_address,omitempty"` - IncludeInterface Listable[string] `json:"include_interface,omitempty"` - ExcludeInterface Listable[string] `json:"exclude_interface,omitempty"` - IncludeUID Listable[uint32] `json:"include_uid,omitempty"` - IncludeUIDRange Listable[string] `json:"include_uid_range,omitempty"` - ExcludeUID Listable[uint32] `json:"exclude_uid,omitempty"` - ExcludeUIDRange Listable[string] `json:"exclude_uid_range,omitempty"` - IncludeAndroidUser Listable[int] `json:"include_android_user,omitempty"` - IncludePackage Listable[string] `json:"include_package,omitempty"` - ExcludePackage Listable[string] `json:"exclude_package,omitempty"` - EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` - UDPTimeout int64 `json:"udp_timeout,omitempty"` - Stack string `json:"stack,omitempty"` - Platform *TunPlatformOptions `json:"platform,omitempty"` + InterfaceName string `json:"interface_name,omitempty"` + MTU uint32 `json:"mtu,omitempty"` + Inet4Address Listable[netip.Prefix] `json:"inet4_address,omitempty"` + Inet6Address Listable[netip.Prefix] `json:"inet6_address,omitempty"` + AutoRoute bool `json:"auto_route,omitempty"` + StrictRoute bool `json:"strict_route,omitempty"` + Inet4RouteAddress Listable[netip.Prefix] `json:"inet4_route_address,omitempty"` + Inet6RouteAddress Listable[netip.Prefix] `json:"inet6_route_address,omitempty"` + Inet4RouteExcludeAddress Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"` + Inet6RouteExcludeAddress Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"` + IncludeInterface Listable[string] `json:"include_interface,omitempty"` + ExcludeInterface Listable[string] `json:"exclude_interface,omitempty"` + IncludeUID Listable[uint32] `json:"include_uid,omitempty"` + IncludeUIDRange Listable[string] `json:"include_uid_range,omitempty"` + ExcludeUID Listable[uint32] `json:"exclude_uid,omitempty"` + ExcludeUIDRange Listable[string] `json:"exclude_uid_range,omitempty"` + IncludeAndroidUser Listable[int] `json:"include_android_user,omitempty"` + IncludePackage Listable[string] `json:"include_package,omitempty"` + ExcludePackage Listable[string] `json:"exclude_package,omitempty"` + EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` + UDPTimeout int64 `json:"udp_timeout,omitempty"` + Stack string `json:"stack,omitempty"` + Platform *TunPlatformOptions `json:"platform,omitempty"` InboundOptions } From c9aa5992d146761869097d50e1657f9e3c76d655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 3 Nov 2023 01:47:25 +0800 Subject: [PATCH 004/103] Add support for v2ray http upgrade transport --- constant/v2ray.go | 9 +- docs/configuration/shared/v2ray-transport.md | 30 ++++ .../shared/v2ray-transport.zh.md | 30 ++++ option/v2ray_transport.go | 21 ++- test/v2ray_httpupgrade_test.go | 16 ++ transport/v2ray/transport.go | 6 +- transport/v2raygrpclite/client.go | 2 +- transport/v2raygrpclite/server.go | 8 +- transport/v2rayhttp/client.go | 2 +- transport/v2rayhttp/server.go | 8 +- transport/v2rayhttpupgrade/client.go | 118 +++++++++++++++ transport/v2rayhttpupgrade/server.go | 139 ++++++++++++++++++ 12 files changed, 369 insertions(+), 20 deletions(-) create mode 100644 test/v2ray_httpupgrade_test.go create mode 100644 transport/v2rayhttpupgrade/client.go create mode 100644 transport/v2rayhttpupgrade/server.go diff --git a/constant/v2ray.go b/constant/v2ray.go index 2243d736fb..c3089a6cf6 100644 --- a/constant/v2ray.go +++ b/constant/v2ray.go @@ -1,8 +1,9 @@ package constant const ( - V2RayTransportTypeHTTP = "http" - V2RayTransportTypeWebsocket = "ws" - V2RayTransportTypeQUIC = "quic" - V2RayTransportTypeGRPC = "grpc" + V2RayTransportTypeHTTP = "http" + V2RayTransportTypeWebsocket = "ws" + V2RayTransportTypeQUIC = "quic" + V2RayTransportTypeGRPC = "grpc" + V2RayTransportTypeHTTPUpgrade = "httpupgrade" ) diff --git a/docs/configuration/shared/v2ray-transport.md b/docs/configuration/shared/v2ray-transport.md index 4b5b6f6669..418ef28db2 100644 --- a/docs/configuration/shared/v2ray-transport.md +++ b/docs/configuration/shared/v2ray-transport.md @@ -15,6 +15,7 @@ Available transports: * WebSocket * QUIC * gRPC +* HTTPUpgrade !!! warning "Difference from v2ray-core" @@ -184,3 +185,32 @@ In standard gRPC client: If enabled, the client transport sends keepalive pings even with no active connections. If disabled, when there are no active connections, `idle_timeout` and `ping_timeout` will be ignored and no keepalive pings will be sent. Disabled by default. + +### HTTPUpgrade + +```json +{ + "type": "httpupgrade", + "host": "", + "path": "", + "headers": {} +} +``` + +#### host + +Host domain. + +The server will verify if not empty. + +#### path + +Path of HTTP request. + +The server will verify if not empty. + +#### headers + +Extra headers of HTTP request. + +The server will write in response if not empty. diff --git a/docs/configuration/shared/v2ray-transport.zh.md b/docs/configuration/shared/v2ray-transport.zh.md index deab558983..2ea93562d6 100644 --- a/docs/configuration/shared/v2ray-transport.zh.md +++ b/docs/configuration/shared/v2ray-transport.zh.md @@ -14,6 +14,7 @@ V2Ray Transport 是 v2ray 发明的一组私有协议,并污染了其他协议 * WebSocket * QUIC * gRPC +* HTTPUpgrade !!! warning "与 v2ray-core 的区别" @@ -183,3 +184,32 @@ gRPC 服务名称。 如果启用,客户端传输即使没有活动连接也会发送 keepalive ping。如果禁用,则在没有活动连接时,将忽略 `idle_timeout` 和 `ping_timeout`,并且不会发送 keepalive ping。 默认禁用。 + +### HTTPUpgrade + +```json +{ + "type": "httpupgrade", + "host": "", + "path": "", + "headers": {} +} +``` + +#### host + +主机域名。 + +默认服务器将验证。 + +#### path + +HTTP 请求路径 + +默认服务器将验证。 + +#### headers + +HTTP 请求的额外标头。 + +默认服务器将写入响应。 diff --git a/option/v2ray_transport.go b/option/v2ray_transport.go index 54b0de7913..63af28a371 100644 --- a/option/v2ray_transport.go +++ b/option/v2ray_transport.go @@ -7,11 +7,12 @@ import ( ) type _V2RayTransportOptions struct { - Type string `json:"type,omitempty"` - HTTPOptions V2RayHTTPOptions `json:"-"` - WebsocketOptions V2RayWebsocketOptions `json:"-"` - QUICOptions V2RayQUICOptions `json:"-"` - GRPCOptions V2RayGRPCOptions `json:"-"` + Type string `json:"type,omitempty"` + HTTPOptions V2RayHTTPOptions `json:"-"` + WebsocketOptions V2RayWebsocketOptions `json:"-"` + QUICOptions V2RayQUICOptions `json:"-"` + GRPCOptions V2RayGRPCOptions `json:"-"` + HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"` } type V2RayTransportOptions _V2RayTransportOptions @@ -29,6 +30,8 @@ func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) { v = o.QUICOptions case C.V2RayTransportTypeGRPC: v = o.GRPCOptions + case C.V2RayTransportTypeHTTPUpgrade: + v = o.HTTPUpgradeOptions default: return nil, E.New("unknown transport type: " + o.Type) } @@ -50,6 +53,8 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error { v = &o.QUICOptions case C.V2RayTransportTypeGRPC: v = &o.GRPCOptions + case C.V2RayTransportTypeHTTPUpgrade: + v = &o.HTTPUpgradeOptions default: return E.New("unknown transport type: " + o.Type) } @@ -85,3 +90,9 @@ type V2RayGRPCOptions struct { PermitWithoutStream bool `json:"permit_without_stream,omitempty"` ForceLite bool `json:"-"` // for test } + +type V2RayHTTPUpgradeOptions struct { + Host string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + Headers HTTPHeader `json:"headers,omitempty"` +} diff --git a/test/v2ray_httpupgrade_test.go b/test/v2ray_httpupgrade_test.go new file mode 100644 index 0000000000..9a79aa3af9 --- /dev/null +++ b/test/v2ray_httpupgrade_test.go @@ -0,0 +1,16 @@ +package main + +import ( + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" +) + +func TestV2RayHTTPUpgrade(t *testing.T) { + t.Run("self", func(t *testing.T) { + testV2RayTransportSelf(t, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeHTTPUpgrade, + }) + }) +} diff --git a/transport/v2ray/transport.go b/transport/v2ray/transport.go index 9dfee28119..deb8a7f0ee 100644 --- a/transport/v2ray/transport.go +++ b/transport/v2ray/transport.go @@ -8,6 +8,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2rayhttp" + "github.com/sagernet/sing-box/transport/v2rayhttpupgrade" "github.com/sagernet/sing-box/transport/v2raywebsocket" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" @@ -35,6 +36,8 @@ func NewServerTransport(ctx context.Context, options option.V2RayTransportOption return NewQUICServer(ctx, options.QUICOptions, tlsConfig, handler) case C.V2RayTransportTypeGRPC: return NewGRPCServer(ctx, options.GRPCOptions, tlsConfig, handler) + case C.V2RayTransportTypeHTTPUpgrade: + return v2rayhttpupgrade.NewServer(ctx, options.HTTPUpgradeOptions, tlsConfig, handler) default: return nil, E.New("unknown transport type: " + options.Type) } @@ -56,7 +59,8 @@ func NewClientTransport(ctx context.Context, dialer N.Dialer, serverAddr M.Socks return nil, C.ErrTLSRequired } return NewQUICClient(ctx, dialer, serverAddr, options.QUICOptions, tlsConfig) - + case C.V2RayTransportTypeHTTPUpgrade: + return v2rayhttpupgrade.NewClient(ctx, dialer, serverAddr, options.HTTPUpgradeOptions, tlsConfig) default: return nil, E.New("unknown transport type: " + options.Type) } diff --git a/transport/v2raygrpclite/client.go b/transport/v2raygrpclite/client.go index 8480ac533b..588a813385 100644 --- a/transport/v2raygrpclite/client.go +++ b/transport/v2raygrpclite/client.go @@ -100,7 +100,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { conn.setup(nil, err) } else if response.StatusCode != 200 { response.Body.Close() - conn.setup(nil, E.New("unexpected status: ", response.StatusCode, " ", response.Status)) + conn.setup(nil, E.New("unexpected status: ", response.Status)) } else { conn.setup(response.Body, nil) } diff --git a/transport/v2raygrpclite/server.go b/transport/v2raygrpclite/server.go index a3025ca6e7..6d3e42ebe5 100644 --- a/transport/v2raygrpclite/server.go +++ b/transport/v2raygrpclite/server.go @@ -35,10 +35,6 @@ type Server struct { path string } -func (s *Server) Network() []string { - return []string{N.NetworkTCP} -} - func NewServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { server := &Server{ tlsConfig: tlsConfig, @@ -92,6 +88,10 @@ func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Reques s.handler.NewError(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) } +func (s *Server) Network() []string { + return []string{N.NetworkTCP} +} + func (s *Server) Serve(listener net.Listener) error { if s.tlsConfig != nil { if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) { diff --git a/transport/v2rayhttp/client.go b/transport/v2rayhttp/client.go index f280eeef55..44c135ef6f 100644 --- a/transport/v2rayhttp/client.go +++ b/transport/v2rayhttp/client.go @@ -143,7 +143,7 @@ func (c *Client) dialHTTP2(ctx context.Context) (net.Conn, error) { conn.Setup(nil, err) } else if response.StatusCode != 200 { response.Body.Close() - conn.Setup(nil, E.New("unexpected status: ", response.StatusCode, " ", response.Status)) + conn.Setup(nil, E.New("unexpected status: ", response.Status)) } else { conn.Setup(response.Body, nil) } diff --git a/transport/v2rayhttp/server.go b/transport/v2rayhttp/server.go index dcfb07a6e5..ef5fffcd2d 100644 --- a/transport/v2rayhttp/server.go +++ b/transport/v2rayhttp/server.go @@ -40,10 +40,6 @@ type Server struct { headers http.Header } -func (s *Server) Network() []string { - return []string{N.NetworkTCP} -} - func NewServer(ctx context.Context, options option.V2RayHTTPOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { server := &Server{ ctx: ctx, @@ -153,6 +149,10 @@ func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Reques s.handler.NewError(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) } +func (s *Server) Network() []string { + return []string{N.NetworkTCP} +} + func (s *Server) Serve(listener net.Listener) error { if s.tlsConfig != nil { if len(s.tlsConfig.NextProtos()) == 0 { diff --git a/transport/v2rayhttpupgrade/client.go b/transport/v2rayhttpupgrade/client.go new file mode 100644 index 0000000000..c10e1b8fdc --- /dev/null +++ b/transport/v2rayhttpupgrade/client.go @@ -0,0 +1,118 @@ +package v2rayhttpupgrade + +import ( + std_bufio "bufio" + "context" + "net" + "net/http" + "net/url" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/buf" + "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" + sHTTP "github.com/sagernet/sing/protocol/http" +) + +var _ adapter.V2RayClientTransport = (*Client)(nil) + +type Client struct { + dialer N.Dialer + tlsConfig tls.Config + serverAddr M.Socksaddr + requestURL url.URL + headers http.Header + host string +} + +func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayHTTPUpgradeOptions, tlsConfig tls.Config) (*Client, error) { + if tlsConfig != nil { + if len(tlsConfig.NextProtos()) == 0 { + tlsConfig.SetNextProtos([]string{"http/1.1"}) + } + } + var host string + if options.Host != "" { + host = options.Host + } else if tlsConfig != nil && tlsConfig.ServerName() != "" { + host = tlsConfig.ServerName() + } else { + host = serverAddr.String() + } + var requestURL url.URL + if tlsConfig == nil { + requestURL.Scheme = "http" + } else { + requestURL.Scheme = "https" + } + requestURL.Host = serverAddr.String() + requestURL.Path = options.Path + err := sHTTP.URLSetPath(&requestURL, options.Path) + if err != nil { + return nil, E.Cause(err, "parse path") + } + if !strings.HasPrefix(requestURL.Path, "/") { + requestURL.Path = "/" + requestURL.Path + } + headers := make(http.Header) + for key, value := range options.Headers { + headers[key] = value + } + return &Client{ + dialer: dialer, + tlsConfig: tlsConfig, + serverAddr: serverAddr, + requestURL: requestURL, + headers: headers, + host: host, + }, nil +} + +func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { + conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.serverAddr) + if err != nil { + return nil, err + } + if c.tlsConfig != nil { + conn, err = tls.ClientHandshake(ctx, conn, c.tlsConfig) + if err != nil { + return nil, err + } + } + request := &http.Request{ + Method: http.MethodGet, + URL: &c.requestURL, + Header: c.headers.Clone(), + Host: c.host, + } + request.Header.Set("Connection", "Upgrade") + request.Header.Set("Upgrade", "websocket") + err = request.Write(conn) + if err != nil { + return nil, err + } + bufReader := std_bufio.NewReader(conn) + response, err := http.ReadResponse(bufReader, request) + if err != nil { + return nil, err + } + if response.StatusCode != 101 || + !strings.EqualFold(response.Header.Get("Connection"), "upgrade") || + !strings.EqualFold(response.Header.Get("Upgrade"), "websocket") { + return nil, E.New("unexpected status: ", response.Status) + } + if bufReader.Buffered() > 0 { + buffer := buf.NewSize(bufReader.Buffered()) + _, err = buffer.ReadFullFrom(bufReader, buffer.Len()) + if err != nil { + return nil, err + } + conn = bufio.NewCachedConn(conn, buffer) + } + return conn, nil +} diff --git a/transport/v2rayhttpupgrade/server.go b/transport/v2rayhttpupgrade/server.go new file mode 100644 index 0000000000..653778f91d --- /dev/null +++ b/transport/v2rayhttpupgrade/server.go @@ -0,0 +1,139 @@ +package v2rayhttpupgrade + +import ( + "context" + "net" + "net/http" + "os" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + aTLS "github.com/sagernet/sing/common/tls" + sHttp "github.com/sagernet/sing/protocol/http" +) + +var _ adapter.V2RayServerTransport = (*Server)(nil) + +type Server struct { + ctx context.Context + tlsConfig tls.ServerConfig + handler adapter.V2RayServerTransportHandler + httpServer *http.Server + host string + path string + headers http.Header +} + +func NewServer(ctx context.Context, options option.V2RayHTTPUpgradeOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { + server := &Server{ + ctx: ctx, + tlsConfig: tlsConfig, + handler: handler, + host: options.Host, + path: options.Path, + headers: options.Headers.Build(), + } + if !strings.HasPrefix(server.path, "/") { + server.path = "/" + server.path + } + server.httpServer = &http.Server{ + Handler: server, + ReadHeaderTimeout: C.TCPTimeout, + MaxHeaderBytes: http.DefaultMaxHeaderBytes, + BaseContext: func(net.Listener) context.Context { + return ctx + }, + TLSNextProto: make(map[string]func(*http.Server, *tls.STDConn, http.Handler)), + } + return server, nil +} + +type httpFlusher interface { + FlushError() error +} + +func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + host := request.Host + if len(s.host) > 0 && host != s.host { + s.invalidRequest(writer, request, http.StatusBadRequest, E.New("bad host: ", host)) + return + } + if !strings.HasPrefix(request.URL.Path, s.path) { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path)) + return + } + if request.Method != http.MethodGet { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad method: ", request.Method)) + return + } + if !strings.EqualFold(request.Header.Get("Connection"), "upgrade") { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("not a upgrade request")) + return + } + if !strings.EqualFold(request.Header.Get("Upgrade"), "websocket") { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("not a websocket request")) + return + } + if request.Header.Get("Sec-WebSocket-Key") != "" { + s.invalidRequest(writer, request, http.StatusNotFound, E.New("real websocket request received")) + return + } + writer.Header().Set("Connection", "upgrade") + writer.Header().Set("Upgrade", "websocket") + writer.WriteHeader(http.StatusSwitchingProtocols) + if flusher, isFlusher := writer.(httpFlusher); isFlusher { + err := flusher.FlushError() + if err != nil { + s.invalidRequest(writer, request, http.StatusInternalServerError, E.New("flush response")) + } + } + hijacker, canHijack := writer.(http.Hijacker) + if !canHijack { + s.invalidRequest(writer, request, http.StatusInternalServerError, E.New("invalid connection, maybe HTTP/2")) + return + } + conn, _, err := hijacker.Hijack() + if err != nil { + s.invalidRequest(writer, request, http.StatusInternalServerError, E.Cause(err, "hijack failed")) + return + } + var metadata M.Metadata + metadata.Source = sHttp.SourceAddress(request) + s.handler.NewConnection(request.Context(), conn, metadata) +} + +func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Request, statusCode int, err error) { + if statusCode > 0 { + writer.WriteHeader(statusCode) + } + s.handler.NewError(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) +} + +func (s *Server) Network() []string { + return []string{N.NetworkTCP} +} + +func (s *Server) Serve(listener net.Listener) error { + if s.tlsConfig != nil { + if len(s.tlsConfig.NextProtos()) == 0 { + s.tlsConfig.SetNextProtos([]string{"http/1.1"}) + } + listener = aTLS.NewListener(listener, s.tlsConfig) + } + return s.httpServer.Serve(listener) +} + +func (s *Server) ServePacket(listener net.PacketConn) error { + return os.ErrInvalid +} + +func (s *Server) Close() error { + return common.Close(common.PtrOrNil(s.httpServer)) +} From 615e93b62bf29dfd09ad94d700163b94c2a8781b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 8 Nov 2023 12:09:22 +0800 Subject: [PATCH 005/103] Migrate multiplex and UoT server to inbound & Add tcp-brutal support for multiplex --- adapter/conn_router.go | 104 ++++++ adapter/router.go | 5 +- common/mux/client.go | 23 +- common/mux/protocol.go | 14 - common/mux/router.go | 65 ++++ common/mux/v2ray_legacy.go | 32 ++ common/uot/router.go | 53 +++ constant/speed.go | 3 + docs/configuration/inbound/hysteria2.zh.md | 2 +- docs/configuration/inbound/shadowsocks.md | 13 +- docs/configuration/inbound/shadowsocks.zh.md | 15 +- docs/configuration/inbound/trojan.md | 5 + docs/configuration/inbound/trojan.zh.md | 5 + docs/configuration/inbound/tuic.zh.md | 2 +- docs/configuration/inbound/vless.md | 5 + docs/configuration/inbound/vless.zh.md | 5 + docs/configuration/inbound/vmess.md | 5 + docs/configuration/inbound/vmess.zh.md | 5 + docs/configuration/outbound/hysteria2.zh.md | 2 +- docs/configuration/outbound/shadowsocks.md | 2 +- docs/configuration/outbound/shadowsocks.zh.md | 2 +- docs/configuration/outbound/trojan.md | 2 +- docs/configuration/outbound/trojan.zh.md | 2 +- docs/configuration/outbound/tuic.zh.md | 2 +- docs/configuration/outbound/vless.md | 5 + docs/configuration/outbound/vless.zh.md | 5 + docs/configuration/outbound/vmess.md | 4 +- docs/configuration/outbound/vmess.zh.md | 2 +- docs/configuration/shared/multiplex.md | 35 +- docs/configuration/shared/multiplex.zh.md | 35 +- docs/configuration/shared/tcp-brutal.md | 28 ++ docs/configuration/shared/tcp-brutal.zh.md | 28 ++ go.mod | 2 +- go.sum | 4 +- inbound/default.go | 2 +- inbound/http.go | 3 +- inbound/mixed.go | 3 +- inbound/naive.go | 3 +- inbound/shadowsocks.go | 12 +- inbound/shadowsocks_multi.go | 14 +- inbound/shadowsocks_relay.go | 9 +- inbound/socks.go | 3 +- inbound/trojan.go | 5 + inbound/tuic.go | 3 +- inbound/vless.go | 10 +- inbound/vmess.go | 11 +- mkdocs.yml | 1 + option/multiplex.go | 23 ++ option/outbound.go | 9 - option/shadowsocks.go | 15 +- option/simple.go | 10 +- option/trojan.go | 11 +- option/vless.go | 21 +- option/vmess.go | 27 +- outbound/shadowsocks.go | 4 +- outbound/socks.go | 2 +- outbound/trojan.go | 2 +- outbound/vless.go | 2 +- outbound/vmess.go | 2 +- route/router.go | 27 +- test/brutal_test.go | 346 ++++++++++++++++++ test/go.mod | 16 +- test/go.sum | 33 +- test/mux_test.go | 21 +- test/shadowsocks_test.go | 2 +- 65 files changed, 997 insertions(+), 176 deletions(-) create mode 100644 adapter/conn_router.go delete mode 100644 common/mux/protocol.go create mode 100644 common/mux/router.go create mode 100644 common/mux/v2ray_legacy.go create mode 100644 common/uot/router.go create mode 100644 constant/speed.go create mode 100644 docs/configuration/shared/tcp-brutal.md create mode 100644 docs/configuration/shared/tcp-brutal.zh.md create mode 100644 option/multiplex.go create mode 100644 test/brutal_test.go diff --git a/adapter/conn_router.go b/adapter/conn_router.go new file mode 100644 index 0000000000..a87c45e8c5 --- /dev/null +++ b/adapter/conn_router.go @@ -0,0 +1,104 @@ +package adapter + +import ( + "context" + "net" + + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type ConnectionRouter interface { + RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error + RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error +} + +func NewRouteHandler( + metadata InboundContext, + router ConnectionRouter, + logger logger.ContextLogger, +) UpstreamHandlerAdapter { + return &routeHandlerWrapper{ + metadata: metadata, + router: router, + logger: logger, + } +} + +func NewRouteContextHandler( + router ConnectionRouter, + logger logger.ContextLogger, +) UpstreamHandlerAdapter { + return &routeContextHandlerWrapper{ + router: router, + logger: logger, + } +} + +var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil) + +type routeHandlerWrapper struct { + metadata InboundContext + router ConnectionRouter + logger logger.ContextLogger +} + +func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + myMetadata := w.metadata + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.router.RouteConnection(ctx, conn, myMetadata) +} + +func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + myMetadata := w.metadata + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.router.RoutePacketConnection(ctx, conn, myMetadata) +} + +func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) { + w.logger.ErrorContext(ctx, err) +} + +var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil) + +type routeContextHandlerWrapper struct { + router ConnectionRouter + logger logger.ContextLogger +} + +func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + myMetadata := ContextFrom(ctx) + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.router.RouteConnection(ctx, conn, *myMetadata) +} + +func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + myMetadata := ContextFrom(ctx) + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.router.RoutePacketConnection(ctx, conn, *myMetadata) +} + +func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) { + w.logger.ErrorContext(ctx, err) +} diff --git a/adapter/router.go b/adapter/router.go index ec23d9311f..ab2d916c65 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -2,14 +2,12 @@ package adapter import ( "context" - "net" "net/netip" "github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-dns" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" - N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" mdns "github.com/miekg/dns" @@ -24,8 +22,7 @@ type Router interface { FakeIPStore() FakeIPStore - RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error - RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error + ConnectionRouter GeoIPReader() *geoip.Reader LoadGeosite(code string) (Rule, error) diff --git a/common/mux/client.go b/common/mux/client.go index 36dd31379b..6f201deac4 100644 --- a/common/mux/client.go +++ b/common/mux/client.go @@ -1,21 +1,42 @@ package mux import ( + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-mux" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" N "github.com/sagernet/sing/common/network" ) -func NewClientWithOptions(dialer N.Dialer, options option.MultiplexOptions) (*Client, error) { +type Client = mux.Client + +func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.OutboundMultiplexOptions) (*Client, error) { if !options.Enabled { return nil, nil } + var brutalOptions mux.BrutalOptions + if options.Brutal != nil && options.Brutal.Enabled { + brutalOptions = mux.BrutalOptions{ + Enabled: true, + SendBPS: uint64(options.Brutal.UpMbps * C.MbpsToBps), + ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps), + } + if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS { + return nil, E.New("brutal: invalid upload speed") + } + if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS { + return nil, E.New("brutal: invalid download speed") + } + } return mux.NewClient(mux.Options{ Dialer: dialer, + Logger: logger, Protocol: options.Protocol, MaxConnections: options.MaxConnections, MinStreams: options.MinStreams, MaxStreams: options.MaxStreams, Padding: options.Padding, + Brutal: brutalOptions, }) } diff --git a/common/mux/protocol.go b/common/mux/protocol.go deleted file mode 100644 index abb0e26865..0000000000 --- a/common/mux/protocol.go +++ /dev/null @@ -1,14 +0,0 @@ -package mux - -import ( - "github.com/sagernet/sing-mux" -) - -type ( - Client = mux.Client -) - -var ( - Destination = mux.Destination - HandleConnection = mux.HandleConnection -) diff --git a/common/mux/router.go b/common/mux/router.go new file mode 100644 index 0000000000..8a2296852b --- /dev/null +++ b/common/mux/router.go @@ -0,0 +1,65 @@ +package mux + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-mux" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" +) + +type Router struct { + router adapter.ConnectionRouter + service *mux.Service +} + +func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouter, error) { + if !options.Enabled { + return router, nil + } + var brutalOptions mux.BrutalOptions + if options.Brutal != nil && options.Brutal.Enabled { + brutalOptions = mux.BrutalOptions{ + Enabled: true, + SendBPS: uint64(options.Brutal.UpMbps * C.MbpsToBps), + ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps), + } + if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS { + return nil, E.New("brutal: invalid upload speed") + } + if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS { + return nil, E.New("brutal: invalid download speed") + } + } + service, err := mux.NewService(mux.ServiceOptions{ + NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context { + return log.ContextWithNewID(ctx) + }, + Logger: logger, + Handler: adapter.NewRouteContextHandler(router, logger), + Padding: options.Padding, + Brutal: brutalOptions, + }) + if err != nil { + return nil, err + } + return &Router{router, service}, nil +} + +func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + if metadata.Destination == mux.Destination { + return r.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata)) + } else { + return r.router.RouteConnection(ctx, conn, metadata) + } +} + +func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + return r.router.RoutePacketConnection(ctx, conn, metadata) +} diff --git a/common/mux/v2ray_legacy.go b/common/mux/v2ray_legacy.go new file mode 100644 index 0000000000..f53aff2db4 --- /dev/null +++ b/common/mux/v2ray_legacy.go @@ -0,0 +1,32 @@ +package mux + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + vmess "github.com/sagernet/sing-vmess" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" +) + +type V2RayLegacyRouter struct { + router adapter.ConnectionRouter + logger logger.ContextLogger +} + +func NewV2RayLegacyRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) adapter.ConnectionRouter { + return &V2RayLegacyRouter{router, logger} +} + +func (r *V2RayLegacyRouter) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + if metadata.Destination.Fqdn == vmess.MuxDestination.Fqdn { + r.logger.InfoContext(ctx, "inbound legacy multiplex connection") + return vmess.HandleMuxConnection(ctx, conn, adapter.NewRouteHandler(metadata, r.router, r.logger)) + } + return r.router.RouteConnection(ctx, conn, metadata) +} + +func (r *V2RayLegacyRouter) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + return r.router.RoutePacketConnection(ctx, conn, metadata) +} diff --git a/common/uot/router.go b/common/uot/router.go new file mode 100644 index 0000000000..fb2d23d557 --- /dev/null +++ b/common/uot/router.go @@ -0,0 +1,53 @@ +package uot + +import ( + "context" + "net" + "net/netip" + + "github.com/sagernet/sing-box/adapter" + 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" + "github.com/sagernet/sing/common/uot" +) + +var _ adapter.ConnectionRouter = (*Router)(nil) + +type Router struct { + router adapter.ConnectionRouter + logger logger.ContextLogger +} + +func NewRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) *Router { + return &Router{router, logger} +} + +func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + switch metadata.Destination.Fqdn { + case uot.MagicAddress: + request, err := uot.ReadRequest(conn) + if err != nil { + return E.Cause(err, "read UoT request") + } + if request.IsConnect { + r.logger.InfoContext(ctx, "inbound UoT connect connection to ", request.Destination) + } else { + r.logger.InfoContext(ctx, "inbound UoT connection to ", request.Destination) + } + metadata.Domain = metadata.Destination.Fqdn + metadata.Destination = request.Destination + return r.router.RoutePacketConnection(ctx, uot.NewConn(conn, *request), metadata) + case uot.LegacyMagicAddress: + r.logger.InfoContext(ctx, "inbound legacy UoT connection") + metadata.Domain = metadata.Destination.Fqdn + metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()} + return r.RoutePacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata) + } + return r.router.RouteConnection(ctx, conn, metadata) +} + +func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + return r.router.RoutePacketConnection(ctx, conn, metadata) +} diff --git a/constant/speed.go b/constant/speed.go new file mode 100644 index 0000000000..7a2ec130a2 --- /dev/null +++ b/constant/speed.go @@ -0,0 +1,3 @@ +package constant + +const MbpsToBps = 125000 diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index d21a13d09e..f43188c09f 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -62,7 +62,7 @@ Hysteria 用户 #### ignore_client_bandwidth -命令客户端使用 BBR 流量控制算法而不是 Hysteria CC。 +命令客户端使用 BBR 拥塞控制算法而不是 Hysteria CC。 与 `up_mbps` 和 `down_mbps` 冲突。 diff --git a/docs/configuration/inbound/shadowsocks.md b/docs/configuration/inbound/shadowsocks.md index 0349d01c72..415a59138e 100644 --- a/docs/configuration/inbound/shadowsocks.md +++ b/docs/configuration/inbound/shadowsocks.md @@ -8,7 +8,8 @@ ... // Listen Fields "method": "2022-blake3-aes-128-gcm", - "password": "8JCsPssfgS8tiRwiMlhARg==" + "password": "8JCsPssfgS8tiRwiMlhARg==", + "multiplex": {} } ``` @@ -23,7 +24,8 @@ "name": "sekai", "password": "PCD2Z4o12bKUoFa3cC97Hw==" } - ] + ], + "multiplex": {} } ``` @@ -41,7 +43,8 @@ "server_port": 8080, "password": "PCD2Z4o12bKUoFa3cC97Hw==" } - ] + ], + "multiplex": {} } ``` @@ -82,3 +85,7 @@ Both if empty. | none | / | | 2022 methods | `sing-box generate rand --base64 ` | | other methods | any string | + +#### multiplex + +See [Multiplex](/configuration/shared/multiplex#inbound) for details. diff --git a/docs/configuration/inbound/shadowsocks.zh.md b/docs/configuration/inbound/shadowsocks.zh.md index 11edd05756..36a292bb0c 100644 --- a/docs/configuration/inbound/shadowsocks.zh.md +++ b/docs/configuration/inbound/shadowsocks.zh.md @@ -8,7 +8,8 @@ ... // 监听字段 "method": "2022-blake3-aes-128-gcm", - "password": "8JCsPssfgS8tiRwiMlhARg==" + "password": "8JCsPssfgS8tiRwiMlhARg==", + "multiplex": {} } ``` @@ -23,7 +24,8 @@ "name": "sekai", "password": "PCD2Z4o12bKUoFa3cC97Hw==" } - ] + ], + "multiplex": {} } ``` @@ -41,7 +43,8 @@ "server_port": 8080, "password": "PCD2Z4o12bKUoFa3cC97Hw==" } - ] + ], + "multiplex": {} } ``` @@ -81,4 +84,8 @@ See [Listen Fields](/configuration/shared/listen) for details. |---------------|------------------------------------------| | none | / | | 2022 methods | `sing-box generate rand --base64 <密钥长度>` | -| other methods | 任意字符串 | \ No newline at end of file +| other methods | 任意字符串 | + +#### multiplex + +参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。 diff --git a/docs/configuration/inbound/trojan.md b/docs/configuration/inbound/trojan.md index cdaf8f5030..787d2b11bf 100644 --- a/docs/configuration/inbound/trojan.md +++ b/docs/configuration/inbound/trojan.md @@ -24,6 +24,7 @@ "server_port": 8081 } }, + "multiplex": {}, "transport": {} } ``` @@ -58,6 +59,10 @@ Fallback server configuration for specified ALPN. If not empty, TLS fallback requests with ALPN not in this table will be rejected. +#### multiplex + +See [Multiplex](/configuration/shared/multiplex#inbound) for details. + #### transport V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport). diff --git a/docs/configuration/inbound/trojan.zh.md b/docs/configuration/inbound/trojan.zh.md index 6cc8c4756e..f52422af82 100644 --- a/docs/configuration/inbound/trojan.zh.md +++ b/docs/configuration/inbound/trojan.zh.md @@ -24,6 +24,7 @@ "server_port": 8081 } }, + "multiplex": {}, "transport": {} } ``` @@ -60,6 +61,10 @@ TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 如果不为空,ALPN 不在此列表中的 TLS 回退请求将被拒绝。 +#### multiplex + +参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。 + #### transport V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。 \ No newline at end of file diff --git a/docs/configuration/inbound/tuic.zh.md b/docs/configuration/inbound/tuic.zh.md index 9a6f395e37..60a7ccf6fd 100644 --- a/docs/configuration/inbound/tuic.zh.md +++ b/docs/configuration/inbound/tuic.zh.md @@ -48,7 +48,7 @@ TUIC 用户密码 #### congestion_control -QUIC 流量控制算法 +QUIC 拥塞控制算法 可选值: `cubic`, `new_reno`, `bbr` diff --git a/docs/configuration/inbound/vless.md b/docs/configuration/inbound/vless.md index a8d0c426da..f78ae01c19 100644 --- a/docs/configuration/inbound/vless.md +++ b/docs/configuration/inbound/vless.md @@ -15,6 +15,7 @@ } ], "tls": {}, + "multiplex": {}, "transport": {} } ``` @@ -49,6 +50,10 @@ Available values: TLS configuration, see [TLS](/configuration/shared/tls/#inbound). +#### multiplex + +See [Multiplex](/configuration/shared/multiplex#inbound) for details. + #### transport V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport). diff --git a/docs/configuration/inbound/vless.zh.md b/docs/configuration/inbound/vless.zh.md index 3ce1261c9a..4beecd6faa 100644 --- a/docs/configuration/inbound/vless.zh.md +++ b/docs/configuration/inbound/vless.zh.md @@ -15,6 +15,7 @@ } ], "tls": {}, + "multiplex": {}, "transport": {} } ``` @@ -49,6 +50,10 @@ VLESS 子协议。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 +#### multiplex + +参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。 + #### transport V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。 diff --git a/docs/configuration/inbound/vmess.md b/docs/configuration/inbound/vmess.md index 2439d6e148..0e559d1ef6 100644 --- a/docs/configuration/inbound/vmess.md +++ b/docs/configuration/inbound/vmess.md @@ -15,6 +15,7 @@ } ], "tls": {}, + "multiplex": {}, "transport": {} } ``` @@ -44,6 +45,10 @@ VMess users. TLS configuration, see [TLS](/configuration/shared/tls/#inbound). +#### multiplex + +See [Multiplex](/configuration/shared/multiplex#inbound) for details. + #### transport V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport). diff --git a/docs/configuration/inbound/vmess.zh.md b/docs/configuration/inbound/vmess.zh.md index ae90145b1c..9554ab79e0 100644 --- a/docs/configuration/inbound/vmess.zh.md +++ b/docs/configuration/inbound/vmess.zh.md @@ -15,6 +15,7 @@ } ], "tls": {}, + "multiplex": {}, "transport": {} } ``` @@ -44,6 +45,10 @@ VMess 用户。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 +#### multiplex + +参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。 + #### transport V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。 diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index e9d1cb2d30..1e490a63af 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -44,7 +44,7 @@ 最大带宽。 -如果为空,将使用 BBR 流量控制算法而不是 Hysteria CC。 +如果为空,将使用 BBR 拥塞控制算法而不是 Hysteria CC。 #### obfs.type diff --git a/docs/configuration/outbound/shadowsocks.md b/docs/configuration/outbound/shadowsocks.md index e004d77b1a..6fc9348408 100644 --- a/docs/configuration/outbound/shadowsocks.md +++ b/docs/configuration/outbound/shadowsocks.md @@ -95,7 +95,7 @@ Conflict with `multiplex`. #### multiplex -Multiplex configuration, see [Multiplex](/configuration/shared/multiplex). +See [Multiplex](/configuration/shared/multiplex#outbound) for details. ### Dial Fields diff --git a/docs/configuration/outbound/shadowsocks.zh.md b/docs/configuration/outbound/shadowsocks.zh.md index 16c627e325..6d9b7a5c85 100644 --- a/docs/configuration/outbound/shadowsocks.zh.md +++ b/docs/configuration/outbound/shadowsocks.zh.md @@ -95,7 +95,7 @@ UDP over TCP 配置。 #### multiplex -多路复用配置, 参阅 [多路复用](/zh/configuration/shared/multiplex)。 +参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。 ### 拨号字段 diff --git a/docs/configuration/outbound/trojan.md b/docs/configuration/outbound/trojan.md index cee13401a6..34b16c7d38 100644 --- a/docs/configuration/outbound/trojan.md +++ b/docs/configuration/outbound/trojan.md @@ -51,7 +51,7 @@ TLS configuration, see [TLS](/configuration/shared/tls/#outbound). #### multiplex -Multiplex configuration, see [Multiplex](/configuration/shared/multiplex). +See [Multiplex](/configuration/shared/multiplex#outbound) for details. #### transport diff --git a/docs/configuration/outbound/trojan.zh.md b/docs/configuration/outbound/trojan.zh.md index ac9191873f..55bb970911 100644 --- a/docs/configuration/outbound/trojan.zh.md +++ b/docs/configuration/outbound/trojan.zh.md @@ -51,7 +51,7 @@ TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 #### multiplex -多路复用配置, 参阅 [多路复用](/zh/configuration/shared/multiplex)。 +参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。 #### transport diff --git a/docs/configuration/outbound/tuic.zh.md b/docs/configuration/outbound/tuic.zh.md index ce5a18999b..11d44448af 100644 --- a/docs/configuration/outbound/tuic.zh.md +++ b/docs/configuration/outbound/tuic.zh.md @@ -51,7 +51,7 @@ TUIC 用户密码 #### congestion_control -QUIC 流量控制算法 +QUIC 拥塞控制算法 可选值: `cubic`, `new_reno`, `bbr` diff --git a/docs/configuration/outbound/vless.md b/docs/configuration/outbound/vless.md index 5fc69bf407..0d2fadc646 100644 --- a/docs/configuration/outbound/vless.md +++ b/docs/configuration/outbound/vless.md @@ -12,6 +12,7 @@ "network": "tcp", "tls": {}, "packet_encoding": "", + "multiplex": {}, "transport": {}, ... // Dial Fields @@ -68,6 +69,10 @@ UDP packet encoding, xudp is used by default. | packetaddr | Supported by v2ray 5+ | | xudp | Supported by xray | +#### multiplex + +See [Multiplex](/configuration/shared/multiplex#outbound) for details. + #### transport V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport). diff --git a/docs/configuration/outbound/vless.zh.md b/docs/configuration/outbound/vless.zh.md index 75713bf514..3d3f24ee65 100644 --- a/docs/configuration/outbound/vless.zh.md +++ b/docs/configuration/outbound/vless.zh.md @@ -12,6 +12,7 @@ "network": "tcp", "tls": {}, "packet_encoding": "", + "multiplex": {}, "transport": {}, ... // 拨号字段 @@ -68,6 +69,10 @@ UDP 包编码,默认使用 xudp。 | packetaddr | 由 v2ray 5+ 支持 | | xudp | 由 xray 支持 | +#### multiplex + +参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。 + #### transport V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。 diff --git a/docs/configuration/outbound/vmess.md b/docs/configuration/outbound/vmess.md index 4522047724..ac29559e72 100644 --- a/docs/configuration/outbound/vmess.md +++ b/docs/configuration/outbound/vmess.md @@ -15,8 +15,8 @@ "network": "tcp", "tls": {}, "packet_encoding": "", - "multiplex": {}, "transport": {}, + "multiplex": {}, ... // Dial Fields } @@ -96,7 +96,7 @@ UDP packet encoding. #### multiplex -Multiplex configuration, see [Multiplex](/configuration/shared/multiplex). +See [Multiplex](/configuration/shared/multiplex#outbound) for details. #### transport diff --git a/docs/configuration/outbound/vmess.zh.md b/docs/configuration/outbound/vmess.zh.md index 16fcfc7acf..dbf1612e51 100644 --- a/docs/configuration/outbound/vmess.zh.md +++ b/docs/configuration/outbound/vmess.zh.md @@ -96,7 +96,7 @@ UDP 包编码。 #### multiplex -多路复用配置, 参阅 [多路复用](/zh/configuration/shared/multiplex)。 +参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。 #### transport diff --git a/docs/configuration/shared/multiplex.md b/docs/configuration/shared/multiplex.md index 833efaff3f..eab2116381 100644 --- a/docs/configuration/shared/multiplex.md +++ b/docs/configuration/shared/multiplex.md @@ -1,8 +1,14 @@ -### Server Requirements +### Inbound -`sing-box` :) +```json +{ + "enabled": true, + "padding": false, + "brutal": {} +} +``` -### Structure +### Outbound ```json { @@ -11,11 +17,27 @@ "max_connections": 4, "min_streams": 4, "max_streams": 0, - "padding": false + "padding": false, + "brutal": {} } ``` -### Fields + +### Inbound Fields + +#### enabled + +Enable multiplex support. + +#### padding + +If enabled, non-padded connections will be rejected. + +#### brutal + +See [TCP Brutal](/configuration/shared/tcp-brutal) for details. + +### Outbound Fields #### enabled @@ -59,3 +81,6 @@ Conflict with `max_connections` and `min_streams`. Enable padding. +#### brutal + +See [TCP Brutal](/configuration/shared/tcp-brutal) for details. diff --git a/docs/configuration/shared/multiplex.zh.md b/docs/configuration/shared/multiplex.zh.md index 99a0ba0354..ae1cad64e7 100644 --- a/docs/configuration/shared/multiplex.zh.md +++ b/docs/configuration/shared/multiplex.zh.md @@ -1,8 +1,14 @@ -### 服务器要求 +### 入站 -`sing-box` :) +```json +{ + "enabled": true, + "padding": false, + "brutal": {} +} +``` -### 结构 +### 出站 ```json { @@ -10,11 +16,27 @@ "protocol": "smux", "max_connections": 4, "min_streams": 4, - "max_streams": 0 + "max_streams": 0, + "padding": false, + "brutal": {} } ``` -### 字段 +### 入站字段 + +#### enabled + +启用多路复用支持。 + +#### padding + +如果启用,将拒绝非填充连接。 + +#### brutal + +参阅 [TCP Brutal](/zh/configuration/shared/tcp-brutal)。 + +### 出站字段 #### enabled @@ -58,3 +80,6 @@ 启用填充。 +#### brutal + +参阅 [TCP Brutal](/zh/configuration/shared/tcp-brutal)。 \ No newline at end of file diff --git a/docs/configuration/shared/tcp-brutal.md b/docs/configuration/shared/tcp-brutal.md new file mode 100644 index 0000000000..d248a463f1 --- /dev/null +++ b/docs/configuration/shared/tcp-brutal.md @@ -0,0 +1,28 @@ +### Server Requirements + +* Linux +* `brutal` congestion control algorithm kernel module installed + +See [tcp-brutal](https://github.com/apernet/tcp-brutal) for details. + +### Structure + +```json +{ + "enabled": true, + "up_mbps": 100, + "down_mbps": 100 +} +``` + +### Fields + +#### enabled + +Enable TCP Brutal congestion control algorithm。 + +#### up_mbps, down_mbps + +==Required== + +Upload and download bandwidth, in Mbps. \ No newline at end of file diff --git a/docs/configuration/shared/tcp-brutal.zh.md b/docs/configuration/shared/tcp-brutal.zh.md new file mode 100644 index 0000000000..efb8c51f60 --- /dev/null +++ b/docs/configuration/shared/tcp-brutal.zh.md @@ -0,0 +1,28 @@ +### 服务器要求 + +* Linux +* `brutal` 拥塞控制算法内核模块已安装 + +参阅 [tcp-brutal](https://github.com/apernet/tcp-brutal)。 + +### 结构 + +```json +{ + "enabled": true, + "up_mbps": 100, + "down_mbps": 100 +} +``` + +### 字段 + +#### enabled + +启用 TCP Brutal 拥塞控制算法。 + +#### up_mbps, down_mbps + +==必填== + +上传和下载带宽,以 Mbps 为单位。 diff --git a/go.mod b/go.mod index e61889528a..d45828c01b 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc github.com/sagernet/sing-dns v0.1.11 - github.com/sagernet/sing-mux v0.1.4 + github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 github.com/sagernet/sing-quic v0.1.5-0.20231123150204-077075e9b6ad github.com/sagernet/sing-shadowsocks v0.2.5 github.com/sagernet/sing-shadowsocks2 v0.1.5 diff --git a/go.sum b/go.sum index cf42420837..0ace12ec24 100644 --- a/go.sum +++ b/go.sum @@ -118,8 +118,8 @@ github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc h1:vESVuxHgbd2EzH github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE= github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE= -github.com/sagernet/sing-mux v0.1.4 h1:BPNPOQr6HkXG3iY/BrfvUKUl+A7gYsGKVSxvoR3PO50= -github.com/sagernet/sing-mux v0.1.4/go.mod h1:dKvcu/sb3fZ88uGv9vzAqUej6J4W+pHu5GqjRuFwAWs= +github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM= +github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07/go.mod h1:u/MZf32xPG8jEKe3t+xUV67EBnKtDtCaPhsJQOQGUYU= github.com/sagernet/sing-quic v0.1.5-0.20231123150204-077075e9b6ad h1:PyMeM7c5xbrMbqGkIOMo6m2ip8o7TP0JONfDWs17rzg= github.com/sagernet/sing-quic v0.1.5-0.20231123150204-077075e9b6ad/go.mod h1:aXHVP+osF3w5wJzoWZbJSrX3ceJiU9QMd0KPnKV6C/o= github.com/sagernet/sing-shadowsocks v0.2.5 h1:qxIttos4xu6ii7MTVJYA8EFQR7Q3KG6xMqmLJIFtBaY= diff --git a/inbound/default.go b/inbound/default.go index de538e1701..44c580deb9 100644 --- a/inbound/default.go +++ b/inbound/default.go @@ -22,7 +22,7 @@ type myInboundAdapter struct { protocol string network []string ctx context.Context - router adapter.Router + router adapter.ConnectionRouter logger log.ContextLogger tag string listenOptions option.ListenOptions diff --git a/inbound/http.go b/inbound/http.go index 14a614b1af..fa6c3d5867 100644 --- a/inbound/http.go +++ b/inbound/http.go @@ -8,6 +8,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -35,7 +36,7 @@ func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogge protocol: C.TypeHTTP, network: []string{N.NetworkTCP}, ctx: ctx, - router: router, + router: uot.NewRouter(router, logger), logger: logger, tag: tag, listenOptions: options.ListenOptions, diff --git a/inbound/mixed.go b/inbound/mixed.go index fceefe4d84..8c3bf3b4f9 100644 --- a/inbound/mixed.go +++ b/inbound/mixed.go @@ -7,6 +7,7 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -37,7 +38,7 @@ func NewMixed(ctx context.Context, router adapter.Router, logger log.ContextLogg protocol: C.TypeMixed, network: []string{N.NetworkTCP}, ctx: ctx, - router: router, + router: uot.NewRouter(router, logger), logger: logger, tag: tag, listenOptions: options.ListenOptions, diff --git a/inbound/naive.go b/inbound/naive.go index 0ea2bbb5df..1ff159d100 100644 --- a/inbound/naive.go +++ b/inbound/naive.go @@ -13,6 +13,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/log" @@ -43,7 +44,7 @@ func NewNaive(ctx context.Context, router adapter.Router, logger log.ContextLogg protocol: C.TypeNaive, network: options.Network.Build(), ctx: ctx, - router: router, + router: uot.NewRouter(router, logger), logger: logger, tag: tag, listenOptions: options.ListenOptions, diff --git a/inbound/shadowsocks.go b/inbound/shadowsocks.go index 822a5f57f8..a45b6daf4b 100644 --- a/inbound/shadowsocks.go +++ b/inbound/shadowsocks.go @@ -6,6 +6,8 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/mux" + "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -48,21 +50,27 @@ func newShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte protocol: C.TypeShadowsocks, network: options.Network.Build(), ctx: ctx, - router: router, + router: uot.NewRouter(router, logger), logger: logger, tag: tag, listenOptions: options.ListenOptions, }, } + inbound.connHandler = inbound inbound.packetHandler = inbound + var err error + inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } + var udpTimeout int64 if options.UDPTimeout != 0 { udpTimeout = options.UDPTimeout } else { udpTimeout = int64(C.UDPTimeout.Seconds()) } - var err error switch { case options.Method == shadowsocks.MethodNone: inbound.service = shadowsocks.NewNoneService(options.UDPTimeout, inbound.upstreamContextHandler()) diff --git a/inbound/shadowsocks_multi.go b/inbound/shadowsocks_multi.go index 6a1baac264..c3c7d2abd3 100644 --- a/inbound/shadowsocks_multi.go +++ b/inbound/shadowsocks_multi.go @@ -6,6 +6,8 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/mux" + "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -38,7 +40,7 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log. protocol: C.TypeShadowsocks, network: options.Network.Build(), ctx: ctx, - router: router, + router: uot.NewRouter(router, logger), logger: logger, tag: tag, listenOptions: options.ListenOptions, @@ -46,16 +48,18 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log. } inbound.connHandler = inbound inbound.packetHandler = inbound + var err error + inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } var udpTimeout int64 if options.UDPTimeout != 0 { udpTimeout = options.UDPTimeout } else { udpTimeout = int64(C.UDPTimeout.Seconds()) } - var ( - service shadowsocks.MultiService[int] - err error - ) + var service shadowsocks.MultiService[int] if common.Contains(shadowaead_2022.List, options.Method) { service, err = shadowaead_2022.NewMultiServiceWithPassword[int]( options.Method, diff --git a/inbound/shadowsocks_relay.go b/inbound/shadowsocks_relay.go index 2f624447be..44f39c9f6a 100644 --- a/inbound/shadowsocks_relay.go +++ b/inbound/shadowsocks_relay.go @@ -6,6 +6,8 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/mux" + "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -34,7 +36,7 @@ func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log. protocol: C.TypeShadowsocks, network: options.Network.Build(), ctx: ctx, - router: router, + router: uot.NewRouter(router, logger), logger: logger, tag: tag, listenOptions: options.ListenOptions, @@ -43,6 +45,11 @@ func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log. } inbound.connHandler = inbound inbound.packetHandler = inbound + var err error + inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } var udpTimeout int64 if options.UDPTimeout != 0 { udpTimeout = options.UDPTimeout diff --git a/inbound/socks.go b/inbound/socks.go index 8cc256efee..65dfb1e651 100644 --- a/inbound/socks.go +++ b/inbound/socks.go @@ -6,6 +6,7 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -30,7 +31,7 @@ func NewSocks(ctx context.Context, router adapter.Router, logger log.ContextLogg protocol: C.TypeSOCKS, network: []string{N.NetworkTCP}, ctx: ctx, - router: router, + router: uot.NewRouter(router, logger), logger: logger, tag: tag, listenOptions: options.ListenOptions, diff --git a/inbound/trojan.go b/inbound/trojan.go index 82432131b0..203b6d3927 100644 --- a/inbound/trojan.go +++ b/inbound/trojan.go @@ -6,6 +6,7 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" @@ -94,6 +95,10 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog return nil, E.Cause(err, "create server transport: ", options.Transport.Type) } } + inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } inbound.service = service inbound.connHandler = inbound return inbound, nil diff --git a/inbound/tuic.go b/inbound/tuic.go index bdef1c5deb..ff9b9ce728 100644 --- a/inbound/tuic.go +++ b/inbound/tuic.go @@ -9,6 +9,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -44,7 +45,7 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge protocol: C.TypeTUIC, network: []string{N.NetworkUDP}, ctx: ctx, - router: router, + router: uot.NewRouter(router, logger), logger: logger, tag: tag, listenOptions: options.ListenOptions, diff --git a/inbound/vless.go b/inbound/vless.go index 11df86bfed..029fdaf756 100644 --- a/inbound/vless.go +++ b/inbound/vless.go @@ -6,7 +6,9 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -42,7 +44,7 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg protocol: C.TypeVLESS, network: []string{N.NetworkTCP}, ctx: ctx, - router: router, + router: uot.NewRouter(router, logger), logger: logger, tag: tag, listenOptions: options.ListenOptions, @@ -50,6 +52,11 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg ctx: ctx, users: options.Users, } + var err error + inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } service := vless.NewService[int](logger, adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound)) service.UpdateUsers(common.MapIndexed(inbound.users, func(index int, _ option.VLESSUser) int { return index @@ -59,7 +66,6 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg return it.Flow })) inbound.service = service - var err error if options.TLS != nil { inbound.tlsConfig, err = tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) if err != nil { diff --git a/inbound/vmess.go b/inbound/vmess.go index 77a8b28acf..70676bbd8c 100644 --- a/inbound/vmess.go +++ b/inbound/vmess.go @@ -6,7 +6,9 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -42,7 +44,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg protocol: C.TypeVMess, network: []string{N.NetworkTCP}, ctx: ctx, - router: router, + router: uot.NewRouter(router, logger), logger: logger, tag: tag, listenOptions: options.ListenOptions, @@ -50,6 +52,11 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg ctx: ctx, users: options.Users, } + var err error + inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } var serviceOptions []vmess.ServiceOption if timeFunc := ntp.TimeFuncFromContext(ctx); timeFunc != nil { serviceOptions = append(serviceOptions, vmess.ServiceWithTimeFunc(timeFunc)) @@ -59,7 +66,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg } service := vmess.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound), serviceOptions...) inbound.service = service - err := service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.VMessUser) int { + err = service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.VMessUser) int { return index }), common.Map(options.Users, func(it option.VMessUser) string { return it.UUID diff --git a/mkdocs.yml b/mkdocs.yml index 5632affae6..98c46c6fb9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -75,6 +75,7 @@ nav: - Multiplex: configuration/shared/multiplex.md - V2Ray Transport: configuration/shared/v2ray-transport.md - UDP over TCP: configuration/shared/udp-over-tcp.md + - TCP Brutal: configuration/shared/tcp-brutal.md - Inbound: - configuration/inbound/index.md - Direct: configuration/inbound/direct.md diff --git a/option/multiplex.go b/option/multiplex.go new file mode 100644 index 0000000000..309d8bdc39 --- /dev/null +++ b/option/multiplex.go @@ -0,0 +1,23 @@ +package option + +type InboundMultiplexOptions struct { + Enabled bool `json:"enabled,omitempty"` + Padding bool `json:"padding,omitempty"` + Brutal *BrutalOptions `json:"brutal,omitempty"` +} + +type OutboundMultiplexOptions struct { + Enabled bool `json:"enabled,omitempty"` + Protocol string `json:"protocol,omitempty"` + MaxConnections int `json:"max_connections,omitempty"` + MinStreams int `json:"min_streams,omitempty"` + MaxStreams int `json:"max_streams,omitempty"` + Padding bool `json:"padding,omitempty"` + Brutal *BrutalOptions `json:"brutal,omitempty"` +} + +type BrutalOptions struct { + Enabled bool `json:"enabled,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` +} diff --git a/option/outbound.go b/option/outbound.go index 3d780ef4ed..2985319ea5 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -154,12 +154,3 @@ type ServerOptions struct { func (o ServerOptions) Build() M.Socksaddr { return M.ParseSocksaddrHostPort(o.Server, o.ServerPort) } - -type MultiplexOptions struct { - Enabled bool `json:"enabled,omitempty"` - Protocol string `json:"protocol,omitempty"` - MaxConnections int `json:"max_connections,omitempty"` - MinStreams int `json:"min_streams,omitempty"` - MaxStreams int `json:"max_streams,omitempty"` - Padding bool `json:"padding,omitempty"` -} diff --git a/option/shadowsocks.go b/option/shadowsocks.go index 38a18b832e..187b9b633d 100644 --- a/option/shadowsocks.go +++ b/option/shadowsocks.go @@ -7,6 +7,7 @@ type ShadowsocksInboundOptions struct { Password string `json:"password,omitempty"` Users []ShadowsocksUser `json:"users,omitempty"` Destinations []ShadowsocksDestination `json:"destinations,omitempty"` + Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` } type ShadowsocksUser struct { @@ -23,11 +24,11 @@ type ShadowsocksDestination struct { type ShadowsocksOutboundOptions struct { DialerOptions ServerOptions - Method string `json:"method"` - Password string `json:"password"` - Plugin string `json:"plugin,omitempty"` - PluginOptions string `json:"plugin_opts,omitempty"` - Network NetworkList `json:"network,omitempty"` - UDPOverTCPOptions *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` - MultiplexOptions *MultiplexOptions `json:"multiplex,omitempty"` + Method string `json:"method"` + Password string `json:"password"` + Plugin string `json:"plugin,omitempty"` + PluginOptions string `json:"plugin_opts,omitempty"` + Network NetworkList `json:"network,omitempty"` + UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` } diff --git a/option/simple.go b/option/simple.go index 55bfe93005..b8692e0cf1 100644 --- a/option/simple.go +++ b/option/simple.go @@ -17,11 +17,11 @@ type HTTPMixedInboundOptions struct { type SocksOutboundOptions struct { DialerOptions ServerOptions - Version string `json:"version,omitempty"` - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Network NetworkList `json:"network,omitempty"` - UDPOverTCPOptions *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` + Version string `json:"version,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Network NetworkList `json:"network,omitempty"` + UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` } type HTTPOutboundOptions struct { diff --git a/option/trojan.go b/option/trojan.go index 11392cd501..d24ed16aed 100644 --- a/option/trojan.go +++ b/option/trojan.go @@ -6,6 +6,7 @@ type TrojanInboundOptions struct { TLS *InboundTLSOptions `json:"tls,omitempty"` Fallback *ServerOptions `json:"fallback,omitempty"` FallbackForALPN map[string]*ServerOptions `json:"fallback_for_alpn,omitempty"` + Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` Transport *V2RayTransportOptions `json:"transport,omitempty"` } @@ -17,9 +18,9 @@ type TrojanUser struct { type TrojanOutboundOptions struct { DialerOptions ServerOptions - Password string `json:"password"` - Network NetworkList `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - Multiplex *MultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` + Password string `json:"password"` + Network NetworkList `json:"network,omitempty"` + TLS *OutboundTLSOptions `json:"tls,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` } diff --git a/option/vless.go b/option/vless.go index 5547bd88c7..09010922e9 100644 --- a/option/vless.go +++ b/option/vless.go @@ -2,9 +2,10 @@ package option type VLESSInboundOptions struct { ListenOptions - Users []VLESSUser `json:"users,omitempty"` - TLS *InboundTLSOptions `json:"tls,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` + Users []VLESSUser `json:"users,omitempty"` + TLS *InboundTLSOptions `json:"tls,omitempty"` + Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` } type VLESSUser struct { @@ -16,11 +17,11 @@ type VLESSUser struct { type VLESSOutboundOptions struct { DialerOptions ServerOptions - UUID string `json:"uuid"` - Flow string `json:"flow,omitempty"` - Network NetworkList `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - Multiplex *MultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` - PacketEncoding *string `json:"packet_encoding,omitempty"` + UUID string `json:"uuid"` + Flow string `json:"flow,omitempty"` + Network NetworkList `json:"network,omitempty"` + TLS *OutboundTLSOptions `json:"tls,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` + PacketEncoding *string `json:"packet_encoding,omitempty"` } diff --git a/option/vmess.go b/option/vmess.go index a2ba210315..8045e9d6da 100644 --- a/option/vmess.go +++ b/option/vmess.go @@ -2,9 +2,10 @@ package option type VMessInboundOptions struct { ListenOptions - Users []VMessUser `json:"users,omitempty"` - TLS *InboundTLSOptions `json:"tls,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` + Users []VMessUser `json:"users,omitempty"` + TLS *InboundTLSOptions `json:"tls,omitempty"` + Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` } type VMessUser struct { @@ -16,14 +17,14 @@ type VMessUser struct { type VMessOutboundOptions struct { DialerOptions ServerOptions - UUID string `json:"uuid"` - Security string `json:"security"` - AlterId int `json:"alter_id,omitempty"` - GlobalPadding bool `json:"global_padding,omitempty"` - AuthenticatedLength bool `json:"authenticated_length,omitempty"` - Network NetworkList `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - PacketEncoding string `json:"packet_encoding,omitempty"` - Multiplex *MultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` + UUID string `json:"uuid"` + Security string `json:"security"` + AlterId int `json:"alter_id,omitempty"` + GlobalPadding bool `json:"global_padding,omitempty"` + AuthenticatedLength bool `json:"authenticated_length,omitempty"` + Network NetworkList `json:"network,omitempty"` + TLS *OutboundTLSOptions `json:"tls,omitempty"` + PacketEncoding string `json:"packet_encoding,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` } diff --git a/outbound/shadowsocks.go b/outbound/shadowsocks.go index e436e1241d..addb6e576f 100644 --- a/outbound/shadowsocks.go +++ b/outbound/shadowsocks.go @@ -62,9 +62,9 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte return nil, err } } - uotOptions := common.PtrValueOrDefault(options.UDPOverTCPOptions) + uotOptions := common.PtrValueOrDefault(options.UDPOverTCP) if !uotOptions.Enabled { - outbound.multiplexDialer, err = mux.NewClientWithOptions((*shadowsocksDialer)(outbound), common.PtrValueOrDefault(options.MultiplexOptions)) + outbound.multiplexDialer, err = mux.NewClientWithOptions((*shadowsocksDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } diff --git a/outbound/socks.go b/outbound/socks.go index bd6b1ada73..063f7b952a 100644 --- a/outbound/socks.go +++ b/outbound/socks.go @@ -54,7 +54,7 @@ func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, optio client: socks.NewClient(outboundDialer, options.ServerOptions.Build(), version, options.Username, options.Password), resolve: version == socks.Version4, } - uotOptions := common.PtrValueOrDefault(options.UDPOverTCPOptions) + uotOptions := common.PtrValueOrDefault(options.UDPOverTCP) if uotOptions.Enabled { outbound.uotClient = &uot.Client{ Dialer: outbound.client, diff --git a/outbound/trojan.go b/outbound/trojan.go index e8ccb6ae9d..1436961394 100644 --- a/outbound/trojan.go +++ b/outbound/trojan.go @@ -62,7 +62,7 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog return nil, E.Cause(err, "create client transport: ", options.Transport.Type) } } - outbound.multiplexDialer, err = mux.NewClientWithOptions((*trojanDialer)(outbound), common.PtrValueOrDefault(options.Multiplex)) + outbound.multiplexDialer, err = mux.NewClientWithOptions((*trojanDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } diff --git a/outbound/vless.go b/outbound/vless.go index a78bddcd2e..506521f700 100644 --- a/outbound/vless.go +++ b/outbound/vless.go @@ -81,7 +81,7 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg if err != nil { return nil, err } - outbound.multiplexDialer, err = mux.NewClientWithOptions((*vlessDialer)(outbound), common.PtrValueOrDefault(options.Multiplex)) + outbound.multiplexDialer, err = mux.NewClientWithOptions((*vlessDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } diff --git a/outbound/vmess.go b/outbound/vmess.go index a981464bbd..6add541481 100644 --- a/outbound/vmess.go +++ b/outbound/vmess.go @@ -64,7 +64,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg return nil, E.Cause(err, "create client transport: ", options.Transport.Type) } } - outbound.multiplexDialer, err = mux.NewClientWithOptions((*vmessDialer)(outbound), common.PtrValueOrDefault(options.Multiplex)) + outbound.multiplexDialer, err = mux.NewClientWithOptions((*vmessDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } diff --git a/route/router.go b/route/router.go index 676ebd7b20..2d2f2cde57 100644 --- a/route/router.go +++ b/route/router.go @@ -16,7 +16,6 @@ import ( "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-box/common/geosite" - "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" @@ -27,6 +26,7 @@ import ( "github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/transport/fakeip" "github.com/sagernet/sing-dns" + mux "github.com/sagernet/sing-mux" "github.com/sagernet/sing-tun" "github.com/sagernet/sing-vmess" "github.com/sagernet/sing/common" @@ -606,30 +606,13 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad metadata.Network = N.NetworkTCP switch metadata.Destination.Fqdn { case mux.Destination.Fqdn: - r.logger.InfoContext(ctx, "inbound multiplex connection") - handler := adapter.NewUpstreamHandler(metadata, r.RouteConnection, r.RoutePacketConnection, r) - return mux.HandleConnection(ctx, handler, r.logger, conn, adapter.UpstreamMetadata(metadata)) + return E.New("global multiplex is deprecated since sing-box v1.7.0, enable multiplex in inbound options instead.") case vmess.MuxDestination.Fqdn: - r.logger.InfoContext(ctx, "inbound legacy multiplex connection") - return vmess.HandleMuxConnection(ctx, conn, adapter.NewUpstreamHandler(metadata, r.RouteConnection, r.RoutePacketConnection, r)) + return E.New("global multiplex (v2ray legacy) not supported since sing-box v1.7.0.") case uot.MagicAddress: - request, err := uot.ReadRequest(conn) - if err != nil { - return E.Cause(err, "read UoT request") - } - if request.IsConnect { - r.logger.InfoContext(ctx, "inbound UoT connect connection to ", request.Destination) - } else { - r.logger.InfoContext(ctx, "inbound UoT connection to ", request.Destination) - } - metadata.Domain = metadata.Destination.Fqdn - metadata.Destination = request.Destination - return r.RoutePacketConnection(ctx, uot.NewConn(conn, *request), metadata) + return E.New("global UoT not supported since sing-box v1.7.0.") case uot.LegacyMagicAddress: - r.logger.InfoContext(ctx, "inbound legacy UoT connection") - metadata.Domain = metadata.Destination.Fqdn - metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()} - return r.RoutePacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata) + return E.New("global UoT (legacy) not supported since sing-box v1.7.0.") } if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) { diff --git a/test/brutal_test.go b/test/brutal_test.go new file mode 100644 index 0000000000..6bfdfcc1b1 --- /dev/null +++ b/test/brutal_test.go @@ -0,0 +1,346 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + + "github.com/gofrs/uuid/v5" +) + +func TestBrutalShadowsocks(t *testing.T) { + method := shadowaead_2022.List[0] + password := mkBase64(t, 16) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + MixedOptions: option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.NewListenAddress(netip.IPv4Unspecified()), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeShadowsocks, + ShadowsocksOptions: option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.NewListenAddress(netip.IPv4Unspecified()), + ListenPort: serverPort, + }, + Method: method, + Password: password, + Multiplex: &option.InboundMultiplexOptions{ + Enabled: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeShadowsocks, + Tag: "ss-out", + ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Method: method, + Password: password, + Multiplex: &option.OutboundMultiplexOptions{ + Enabled: true, + Protocol: "smux", + Padding: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + DefaultOptions: option.DefaultRule{ + Inbound: []string{"mixed-in"}, + Outbound: "ss-out", + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestBrutalTrojan(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + password := mkBase64(t, 16) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + MixedOptions: option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.NewListenAddress(netip.IPv4Unspecified()), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeTrojan, + TrojanOptions: option.TrojanInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.NewListenAddress(netip.IPv4Unspecified()), + ListenPort: serverPort, + }, + Users: []option.TrojanUser{{Password: password}}, + Multiplex: &option.InboundMultiplexOptions{ + Enabled: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeTrojan, + Tag: "ss-out", + TrojanOptions: option.TrojanOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Password: password, + Multiplex: &option.OutboundMultiplexOptions{ + Enabled: true, + Protocol: "yamux", + Padding: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + DefaultOptions: option.DefaultRule{ + Inbound: []string{"mixed-in"}, + Outbound: "ss-out", + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestBrutalVMess(t *testing.T) { + user, _ := uuid.NewV4() + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + MixedOptions: option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.NewListenAddress(netip.IPv4Unspecified()), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeVMess, + VMessOptions: option.VMessInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.NewListenAddress(netip.IPv4Unspecified()), + ListenPort: serverPort, + }, + Users: []option.VMessUser{{UUID: user.String()}}, + Multiplex: &option.InboundMultiplexOptions{ + Enabled: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeVMess, + Tag: "ss-out", + VMessOptions: option.VMessOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: user.String(), + Multiplex: &option.OutboundMultiplexOptions{ + Enabled: true, + Protocol: "h2mux", + Padding: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + DefaultOptions: option.DefaultRule{ + Inbound: []string{"mixed-in"}, + Outbound: "ss-out", + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestBrutalVLESS(t *testing.T) { + user, _ := uuid.NewV4() + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + MixedOptions: option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.NewListenAddress(netip.IPv4Unspecified()), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeVLESS, + VLESSOptions: option.VLESSInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.NewListenAddress(netip.IPv4Unspecified()), + ListenPort: serverPort, + }, + Users: []option.VLESSUser{{UUID: user.String()}}, + Multiplex: &option.InboundMultiplexOptions{ + Enabled: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.InboundRealityOptions{ + Enabled: true, + Handshake: option.InboundRealityHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "google.com", + ServerPort: 443, + }, + }, + ShortID: []string{"0123456789abcdef"}, + PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", + }, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeVLESS, + Tag: "ss-out", + VLESSOptions: option.VLESSOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + UUID: user.String(), + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.OutboundRealityOptions{ + Enabled: true, + ShortID: "0123456789abcdef", + PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", + }, + UTLS: &option.OutboundUTLSOptions{ + Enabled: true, + }, + }, + Multiplex: &option.OutboundMultiplexOptions{ + Enabled: true, + Protocol: "h2mux", + Padding: true, + Brutal: &option.BrutalOptions{ + Enabled: true, + UpMbps: 100, + DownMbps: 100, + }, + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + DefaultOptions: option.DefaultRule{ + Inbound: []string{"mixed-in"}, + Outbound: "ss-out", + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} diff --git a/test/go.mod b/test/go.mod index 9aa1bf0315..42c9d087b4 100644 --- a/test/go.mod +++ b/test/go.mod @@ -11,11 +11,11 @@ require ( github.com/docker/go-connections v0.4.0 github.com/gofrs/uuid/v5 v5.0.0 github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460 - github.com/sagernet/sing v0.2.17 + github.com/sagernet/sing v0.2.18-0.20231119032432-6a556bfa50cc github.com/sagernet/sing-dns v0.1.11 github.com/sagernet/sing-quic v0.1.4 github.com/sagernet/sing-shadowsocks v0.2.5 - github.com/sagernet/sing-shadowsocks2 v0.1.4 + github.com/sagernet/sing-shadowsocks2 v0.1.5 github.com/spyzhov/ajson v0.9.0 github.com/stretchr/testify v1.8.4 go.uber.org/goleak v1.3.0 @@ -40,6 +40,8 @@ require ( github.com/go-chi/render v1.0.3 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.1.2 // indirect @@ -70,18 +72,18 @@ require ( github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a // indirect github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect - github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab // indirect + github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 // indirect github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect - github.com/sagernet/sing-mux v0.1.4 // indirect + github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 // indirect github.com/sagernet/sing-shadowtls v0.1.4 // indirect - github.com/sagernet/sing-tun v0.1.20 // indirect + github.com/sagernet/sing-tun v0.1.21-0.20231119035513-f6ea97c5af71 // indirect github.com/sagernet/sing-vmess v0.1.8 // indirect github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 // indirect github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 // indirect github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 // indirect - github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e // indirect github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f // indirect + github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed // indirect github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect @@ -94,7 +96,7 @@ require ( golang.org/x/mod v0.14.0 // indirect golang.org/x/sys v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.3.0 // indirect + golang.org/x/time v0.4.0 // indirect golang.org/x/tools v0.15.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/grpc v1.59.0 // indirect diff --git a/test/go.sum b/test/go.sum index 3c87ccbfac..c3d155cb45 100644 --- a/test/go.sum +++ b/test/go.sum @@ -43,6 +43,10 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -117,8 +121,8 @@ github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a h1:wZHruBx github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a/go.mod h1:dNV1ZP9y3qx5ltULeKaQZTZWTLHflgW5DES+Ses7cMI= github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA= github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms= -github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab h1:u+xQoi/Yc6bNUvTfrDD6HhGRybn2lzrhf5vmS+wb4Ho= -github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab/go.mod h1:3akUhSHSVtLuJaYcW5JPepUraBOW06Ibz2HKwaK5rOk= +github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 h1:dSPgjIw0CT6ISLeEh8Q20dZMBMFCcEceo23+LncRcNQ= +github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930/go.mod h1:JpKHkOYgh4wLwrX2BhH3ZIvCvazCkTnPeEcmigZJfHY= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460 h1:dAe4OIJAtE0nHOzTHhAReQteh3+sa63rvXbuIpbeOTY= @@ -127,22 +131,22 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= -github.com/sagernet/sing v0.2.17 h1:vMPKb3MV0Aa5ws4dCJkRI8XEjrsUcDn810czd0FwmzI= -github.com/sagernet/sing v0.2.17/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= +github.com/sagernet/sing v0.2.18-0.20231119032432-6a556bfa50cc h1:dmU0chO0QrBpARo8sqyOc+mvPLW+qux4ca16kb2WIc8= +github.com/sagernet/sing v0.2.18-0.20231119032432-6a556bfa50cc/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE= github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE= -github.com/sagernet/sing-mux v0.1.4 h1:BPNPOQr6HkXG3iY/BrfvUKUl+A7gYsGKVSxvoR3PO50= -github.com/sagernet/sing-mux v0.1.4/go.mod h1:dKvcu/sb3fZ88uGv9vzAqUej6J4W+pHu5GqjRuFwAWs= +github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM= +github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07/go.mod h1:u/MZf32xPG8jEKe3t+xUV67EBnKtDtCaPhsJQOQGUYU= github.com/sagernet/sing-quic v0.1.4 h1:F5KRGXMXKQEmP8VrzVollf9HWcRqggcuG9nRCL+5IJ8= github.com/sagernet/sing-quic v0.1.4/go.mod h1:aXHVP+osF3w5wJzoWZbJSrX3ceJiU9QMd0KPnKV6C/o= github.com/sagernet/sing-shadowsocks v0.2.5 h1:qxIttos4xu6ii7MTVJYA8EFQR7Q3KG6xMqmLJIFtBaY= github.com/sagernet/sing-shadowsocks v0.2.5/go.mod h1:MGWGkcU2xW2G2mfArT9/QqpVLOGU+dBaahZCtPHdt7A= -github.com/sagernet/sing-shadowsocks2 v0.1.4 h1:vht2M8t3m5DTgXR2j24KbYOygG5aOp+MUhpQnAux728= -github.com/sagernet/sing-shadowsocks2 v0.1.4/go.mod h1:Mgdee99NxxNd5Zld3ixIs18yVs4x2dI2VTDDE1N14Wc= +github.com/sagernet/sing-shadowsocks2 v0.1.5 h1:JDeAJ4ZWlYZ7F6qEVdDKPhQEangxKw/JtmU+i/YfCYE= +github.com/sagernet/sing-shadowsocks2 v0.1.5/go.mod h1:KF65y8lI5PGHyMgRZGYXYsH9ilgRc/yr+NYbSNGuBm4= github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= -github.com/sagernet/sing-tun v0.1.20 h1:vYWo/w6fkKc8I1WP/IB8eBWZVsGIC6eoEoNR6XqEDlY= -github.com/sagernet/sing-tun v0.1.20/go.mod h1:6kkPL/u9tWcLFfu55VbwMDnO++17cUihSmImkZjdZro= +github.com/sagernet/sing-tun v0.1.21-0.20231119035513-f6ea97c5af71 h1:WQi0TwhjbSNFFbxybIgAUSjVvo7uWSsLD28ldoM2avY= +github.com/sagernet/sing-tun v0.1.21-0.20231119035513-f6ea97c5af71/go.mod h1:hyzA4gDWbeg2SXklqPDswBKa//QcjlZqKw9aPcNdQ9A= github.com/sagernet/sing-vmess v0.1.8 h1:XVWad1RpTy9b5tPxdm5MCU8cGfrTGdR8qCq6HV2aCNc= github.com/sagernet/sing-vmess v0.1.8/go.mod h1:vhx32UNzTDUkNwOyIjcZQohre1CaytquC5mPplId8uA= github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 h1:HuE6xSwco/Xed8ajZ+coeYLmioq0Qp1/Z2zczFaV8as= @@ -151,10 +155,10 @@ github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 h1:Px+hN4Vzgx+iCGV github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6/go.mod h1:zovq6vTvEM6ECiqE3Eeb9rpIylPpamPcmrJ9tv0Bt0M= github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 h1:kDUqhc9Vsk5HJuhfIATJ8oQwBmpOZJuozQG7Vk88lL4= github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM= -github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e h1:7uw2njHFGE+VpWamge6o56j2RWk4omF6uLKKxMmcWvs= -github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e/go.mod h1:45TUl8+gH4SIKr4ykREbxKWTxkDlSzFENzctB1dVRRY= github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f h1:Kvo8w8Y9lzFGB/7z09MJ3TR99TFtfI/IuY87Ygcycho= github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f/go.mod h1:mySs0abhpc/gLlvhoq7HP1RzOaRmIXVeZGCh++zoApk= +github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed h1:90a510OeE9siSJoYsI8nSjPmA+u5ROMDts/ZkdNsuXY= +github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s= github.com/spyzhov/ajson v0.9.0 h1:tF46gJGOenYVj+k9K1U1XpCxVWhmiyY5PsVCAs1+OJ0= @@ -222,6 +226,7 @@ golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -231,8 +236,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= +golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/test/mux_test.go b/test/mux_test.go index b57c6cee99..564b936d8e 100644 --- a/test/mux_test.go +++ b/test/mux_test.go @@ -18,7 +18,7 @@ var muxProtocols = []string{ } func TestVMessSMux(t *testing.T) { - testVMessMux(t, option.MultiplexOptions{ + testVMessMux(t, option.OutboundMultiplexOptions{ Enabled: true, Protocol: "smux", }) @@ -27,7 +27,7 @@ func TestVMessSMux(t *testing.T) { func TestShadowsocksMux(t *testing.T) { for _, protocol := range muxProtocols { t.Run(protocol, func(t *testing.T) { - testShadowsocksMux(t, option.MultiplexOptions{ + testShadowsocksMux(t, option.OutboundMultiplexOptions{ Enabled: true, Protocol: protocol, }) @@ -36,7 +36,7 @@ func TestShadowsocksMux(t *testing.T) { } func TestShadowsockH2Mux(t *testing.T) { - testShadowsocksMux(t, option.MultiplexOptions{ + testShadowsocksMux(t, option.OutboundMultiplexOptions{ Enabled: true, Protocol: "h2mux", Padding: true, @@ -44,14 +44,14 @@ func TestShadowsockH2Mux(t *testing.T) { } func TestShadowsockSMuxPadding(t *testing.T) { - testShadowsocksMux(t, option.MultiplexOptions{ + testShadowsocksMux(t, option.OutboundMultiplexOptions{ Enabled: true, Protocol: "smux", Padding: true, }) } -func testShadowsocksMux(t *testing.T, options option.MultiplexOptions) { +func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ @@ -90,9 +90,9 @@ func testShadowsocksMux(t *testing.T, options option.MultiplexOptions) { Server: "127.0.0.1", ServerPort: serverPort, }, - Method: method, - Password: password, - MultiplexOptions: &options, + Method: method, + Password: password, + Multiplex: &options, }, }, }, @@ -110,7 +110,7 @@ func testShadowsocksMux(t *testing.T, options option.MultiplexOptions) { testSuit(t, clientPort, testPort) } -func testVMessMux(t *testing.T, options option.MultiplexOptions) { +func testVMessMux(t *testing.T, options option.OutboundMultiplexOptions) { user, _ := uuid.NewV4() startInstance(t, option.Options{ Inbounds: []option.Inbound{ @@ -136,6 +136,9 @@ func testVMessMux(t *testing.T, options option.MultiplexOptions) { UUID: user.String(), }, }, + Multiplex: &option.InboundMultiplexOptions{ + Enabled: true, + }, }, }, }, diff --git a/test/shadowsocks_test.go b/test/shadowsocks_test.go index f3fc61d1e8..7d063d9a96 100644 --- a/test/shadowsocks_test.go +++ b/test/shadowsocks_test.go @@ -249,7 +249,7 @@ func TestShadowsocksUoT(t *testing.T) { }, Method: method, Password: password, - UDPOverTCPOptions: &option.UDPOverTCPOptions{ + UDPOverTCP: &option.UDPOverTCPOptions{ Enabled: true, }, }, From 4fcf29da96519e781f8c58e1fb24ba5872a5b668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 14 Nov 2023 21:48:55 +0800 Subject: [PATCH 006/103] Update quic-go to v0.40.0 --- go.mod | 10 +++++----- go.sum | 27 +++++++++++---------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index d45828c01b..f11ae050b9 100644 --- a/go.mod +++ b/go.mod @@ -24,12 +24,12 @@ require ( github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a github.com/sagernet/gomobile v0.0.0-20230915142329-c6740b6d2950 github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 - github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460 + github.com/sagernet/quic-go v0.40.0 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc github.com/sagernet/sing-dns v0.1.11 github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 - github.com/sagernet/sing-quic v0.1.5-0.20231123150204-077075e9b6ad + github.com/sagernet/sing-quic v0.1.5-0.20231123150216-00957d136203 github.com/sagernet/sing-shadowsocks v0.2.5 github.com/sagernet/sing-shadowsocks2 v0.1.5 github.com/sagernet/sing-shadowtls v0.1.4 @@ -65,7 +65,7 @@ require ( github.com/gobwas/pool v0.2.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.1.2 // indirect - github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect @@ -73,11 +73,11 @@ require ( github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/libdns/libdns v0.2.1 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/onsi/ginkgo/v2 v2.9.7 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/qtls-go1-20 v0.3.4 // indirect + github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect diff --git a/go.sum b/go.sum index 0ace12ec24..2b9a369ea0 100644 --- a/go.sum +++ b/go.sum @@ -6,9 +6,6 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/ github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/caddyserver/certmagic v0.19.2 h1:HZd1AKLx4592MalEGQS39DKs2ZOAJCEM/xYPMQ2/ui0= github.com/caddyserver/certmagic v0.19.2/go.mod h1:fsL01NomQ6N+kE2j37ZCnig2MFosG+MIO4ztnmG/zz8= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg= github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -44,11 +41,10 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk= +github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c h1:PgxFEySCI41sH0mB7/2XswdXbUykQsRUGod8Rn+NubM= @@ -80,9 +76,9 @@ github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= -github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= +github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= +github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= github.com/ooni/go-libtor v1.1.8 h1:Wo3V3DVTxl5vZdxtQakqYP+DAHx7pPtAFSl1bnAa08w= github.com/ooni/go-libtor v1.1.8/go.mod h1:q1YyLwRD9GeMyeerVvwc0vJ2YgwDLTp2bdVcrh/JXyI= github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= @@ -93,8 +89,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg= -github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= +github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= +github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= @@ -108,8 +104,8 @@ github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 h1:dSPgjIw0CT6ISLe github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930/go.mod h1:JpKHkOYgh4wLwrX2BhH3ZIvCvazCkTnPeEcmigZJfHY= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= -github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460 h1:dAe4OIJAtE0nHOzTHhAReQteh3+sa63rvXbuIpbeOTY= -github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460/go.mod h1:uJGpmJCOcMQqMlHKc3P1Vz6uygmpz4bPeVIoOhdVQnM= +github.com/sagernet/quic-go v0.40.0 h1:DvQNPb72lzvNQDe9tcUyHTw8eRv6PLtM2mNYmdlzUMo= +github.com/sagernet/quic-go v0.40.0/go.mod h1:VqtdhlbkeeG5Okhb3eDMb/9o0EoglReHunNT9ukrJAI= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= @@ -120,8 +116,8 @@ github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKh github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE= github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM= github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07/go.mod h1:u/MZf32xPG8jEKe3t+xUV67EBnKtDtCaPhsJQOQGUYU= -github.com/sagernet/sing-quic v0.1.5-0.20231123150204-077075e9b6ad h1:PyMeM7c5xbrMbqGkIOMo6m2ip8o7TP0JONfDWs17rzg= -github.com/sagernet/sing-quic v0.1.5-0.20231123150204-077075e9b6ad/go.mod h1:aXHVP+osF3w5wJzoWZbJSrX3ceJiU9QMd0KPnKV6C/o= +github.com/sagernet/sing-quic v0.1.5-0.20231123150216-00957d136203 h1:e3C94gdfTDal52fk0BS/BRcDeF3USqBr8OjQc7XBk4E= +github.com/sagernet/sing-quic v0.1.5-0.20231123150216-00957d136203/go.mod h1:B6OgRz+qLn3N1114dcZVExkdarArtsAX2MgWJIfB72c= github.com/sagernet/sing-shadowsocks v0.2.5 h1:qxIttos4xu6ii7MTVJYA8EFQR7Q3KG6xMqmLJIFtBaY= github.com/sagernet/sing-shadowsocks v0.2.5/go.mod h1:MGWGkcU2xW2G2mfArT9/QqpVLOGU+dBaahZCtPHdt7A= github.com/sagernet/sing-shadowsocks2 v0.1.5 h1:JDeAJ4ZWlYZ7F6qEVdDKPhQEangxKw/JtmU+i/YfCYE= @@ -185,7 +181,6 @@ golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From a5cea605b2228c8088aa314fe6f668602da81eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 24 Nov 2023 20:58:07 +0800 Subject: [PATCH 007/103] Add `wifi_ssid` and `wifi_bssid` route and DNS rules --- adapter/router.go | 6 ++++ experimental/libbox/platform.go | 10 ++++++ experimental/libbox/platform/interface.go | 1 + experimental/libbox/service.go | 8 +++++ option/rule.go | 2 ++ option/rule_dns.go | 2 ++ route/router.go | 26 +++++++++++++++ route/router_geo_resources.go | 8 +++++ route/rule_default.go | 10 ++++++ route/rule_dns.go | 10 ++++++ route/rule_item_wifi_bssid.go | 39 +++++++++++++++++++++++ route/rule_item_wifi_ssid.go | 39 +++++++++++++++++++++++ 12 files changed, 161 insertions(+) create mode 100644 route/rule_item_wifi_bssid.go create mode 100644 route/rule_item_wifi_ssid.go diff --git a/adapter/router.go b/adapter/router.go index ab2d916c65..e4c3904d51 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -41,6 +41,7 @@ type Router interface { NetworkMonitor() tun.NetworkUpdateMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor PackageManager() tun.PackageManager + WIFIState() WIFIState Rules() []Rule ClashServer() ClashServer @@ -78,3 +79,8 @@ type DNSRule interface { type InterfaceUpdateListener interface { InterfaceUpdated() } + +type WIFIState struct { + SSID string + BSSID string +} diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go index b7418bd279..451a72a9e1 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -19,6 +19,7 @@ type PlatformInterface interface { UsePlatformInterfaceGetter() bool GetInterfaces() (NetworkInterfaceIterator, error) UnderNetworkExtension() bool + ReadWIFIState() *WIFIState ClearDNSCache() } @@ -38,6 +39,15 @@ type NetworkInterface struct { Addresses StringIterator } +type WIFIState struct { + SSID string + BSSID string +} + +func NewWIFIState(wifiSSID string, wifiBSSID string) *WIFIState { + return &WIFIState{wifiSSID, wifiBSSID} +} + type NetworkInterfaceIterator interface { Next() *NetworkInterface HasNext() bool diff --git a/experimental/libbox/platform/interface.go b/experimental/libbox/platform/interface.go index 8c47197186..54d35fa315 100644 --- a/experimental/libbox/platform/interface.go +++ b/experimental/libbox/platform/interface.go @@ -23,6 +23,7 @@ type Interface interface { Interfaces() ([]NetworkInterface, error) UnderNetworkExtension() bool ClearDNSCache() + ReadWIFIState() adapter.WIFIState process.Searcher } diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index ce4d6d2aad..3375f7502c 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -210,6 +210,14 @@ func (w *platformInterfaceWrapper) ClearDNSCache() { w.iif.ClearDNSCache() } +func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState { + wifiState := w.iif.ReadWIFIState() + if wifiState == nil { + return adapter.WIFIState{} + } + return (adapter.WIFIState)(*wifiState) +} + func (w *platformInterfaceWrapper) DisableColors() bool { return runtime.GOOS != "android" } diff --git a/option/rule.go b/option/rule.go index f78a752d91..8caba96e57 100644 --- a/option/rule.go +++ b/option/rule.go @@ -78,6 +78,8 @@ type DefaultRule struct { User Listable[string] `json:"user,omitempty"` UserID Listable[int32] `json:"user_id,omitempty"` ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` Invert bool `json:"invert,omitempty"` Outbound string `json:"outbound,omitempty"` } diff --git a/option/rule_dns.go b/option/rule_dns.go index 563d30850b..ba572b9aa2 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -78,6 +78,8 @@ type DefaultDNSRule struct { UserID Listable[int32] `json:"user_id,omitempty"` Outbound Listable[string] `json:"outbound,omitempty"` ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` Invert bool `json:"invert,omitempty"` Server string `json:"server,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` diff --git a/route/router.go b/route/router.go index 2d2f2cde57..972c4ec048 100644 --- a/route/router.go +++ b/route/router.go @@ -86,6 +86,8 @@ type Router struct { clashServer adapter.ClashServer v2rayServer adapter.V2RayServer platformInterface platform.Interface + needWIFIState bool + wifiState adapter.WIFIState } func NewRouter( @@ -116,6 +118,7 @@ func NewRouter( defaultMark: options.DefaultMark, pauseManager: pause.ManagerFromContext(ctx), platformInterface: platformInterface, + needWIFIState: hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule), } router.dnsClient = dns.NewClient(dns.ClientOptions{ DisableCache: dnsOptions.DNSClientOptions.DisableCache, @@ -328,6 +331,11 @@ func NewRouter( service.ContextWith[serviceNTP.TimeService](ctx, timeService) router.timeService = timeService } + if platformInterface != nil && router.interfaceMonitor != nil && router.needWIFIState { + router.interfaceMonitor.RegisterCallback(func(_ int) { + router.updateWIFIState() + }) + } return router, nil } @@ -468,6 +476,9 @@ func (r *Router) Start() error { r.geositeCache = nil r.geositeReader = nil } + if r.needWIFIState { + r.updateWIFIState() + } for i, rule := range r.rules { err := rule.Start() if err != nil { @@ -940,6 +951,10 @@ func (r *Router) Rules() []adapter.Rule { return r.rules } +func (r *Router) WIFIState() adapter.WIFIState { + return r.wifiState +} + func (r *Router) NetworkMonitor() tun.NetworkUpdateMonitor { return r.networkMonitor } @@ -1019,3 +1034,14 @@ func (r *Router) ResetNetwork() error { } return nil } + +func (r *Router) updateWIFIState() { + if r.platformInterface == nil { + return + } + state := r.platformInterface.ReadWIFIState() + if state != r.wifiState { + r.wifiState = state + r.logger.Info("updated WIFI state: SSID=", state.SSID, ", BSSID=", state.BSSID) + } +} diff --git a/route/router_geo_resources.go b/route/router_geo_resources.go index 523833dcff..8715cf922c 100644 --- a/route/router_geo_resources.go +++ b/route/router_geo_resources.go @@ -307,3 +307,11 @@ func isProcessDNSRule(rule option.DefaultDNSRule) bool { func notPrivateNode(code string) bool { return code != "private" } + +func isWIFIRule(rule option.DefaultRule) bool { + return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 +} + +func isWIFIDNSRule(rule option.DefaultDNSRule) bool { + return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 +} diff --git a/route/rule_default.go b/route/rule_default.go index 01322c13aa..2d62f97a46 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -184,6 +184,16 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(router, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFIBSSID) > 0 { + item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } diff --git a/route/rule_dns.go b/route/rule_dns.go index 15e4b16ff1..5132f02456 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -180,6 +180,16 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(router, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFIBSSID) > 0 { + item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } diff --git a/route/rule_item_wifi_bssid.go b/route/rule_item_wifi_bssid.go new file mode 100644 index 0000000000..3b1ff9c852 --- /dev/null +++ b/route/rule_item_wifi_bssid.go @@ -0,0 +1,39 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*WIFIBSSIDItem)(nil) + +type WIFIBSSIDItem struct { + bssidList []string + bssidMap map[string]bool + router adapter.Router +} + +func NewWIFIBSSIDItem(router adapter.Router, bssidList []string) *WIFIBSSIDItem { + bssidMap := make(map[string]bool) + for _, bssid := range bssidList { + bssidMap[bssid] = true + } + return &WIFIBSSIDItem{ + bssidList, + bssidMap, + router, + } +} + +func (r *WIFIBSSIDItem) Match(metadata *adapter.InboundContext) bool { + return r.bssidMap[r.router.WIFIState().BSSID] +} + +func (r *WIFIBSSIDItem) String() string { + if len(r.bssidList) == 1 { + return F.ToString("wifi_bssid=", r.bssidList[0]) + } + return F.ToString("wifi_bssid=[", strings.Join(r.bssidList, " "), "]") +} diff --git a/route/rule_item_wifi_ssid.go b/route/rule_item_wifi_ssid.go new file mode 100644 index 0000000000..62cf935eb0 --- /dev/null +++ b/route/rule_item_wifi_ssid.go @@ -0,0 +1,39 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*WIFISSIDItem)(nil) + +type WIFISSIDItem struct { + ssidList []string + ssidMap map[string]bool + router adapter.Router +} + +func NewWIFISSIDItem(router adapter.Router, ssidList []string) *WIFISSIDItem { + ssidMap := make(map[string]bool) + for _, ssid := range ssidList { + ssidMap[ssid] = true + } + return &WIFISSIDItem{ + ssidList, + ssidMap, + router, + } +} + +func (r *WIFISSIDItem) Match(metadata *adapter.InboundContext) bool { + return r.ssidMap[r.router.WIFIState().SSID] +} + +func (r *WIFISSIDItem) String() string { + if len(r.ssidList) == 1 { + return F.ToString("wifi_ssid=", r.ssidList[0]) + } + return F.ToString("wifi_ssid=[", strings.Join(r.ssidList, " "), "]") +} From 0f643bf414a5314d003c9f7981de2640a3869b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 9 Nov 2023 17:04:08 +0800 Subject: [PATCH 008/103] documentation: Bump version & Refactor docs --- Makefile | 10 +- docs/changelog.md | 122 +++++ docs/clients/android/features.md | 64 +++ docs/clients/android/index.md | 22 + docs/clients/apple/features.md | 52 +++ docs/clients/apple/index.md | 32 ++ docs/clients/general.md | 63 +++ docs/clients/index.md | 13 + docs/clients/index.zh.md | 12 + docs/clients/privacy.md | 8 + docs/configuration/dns/rule.md | 32 +- docs/configuration/dns/rule.zh.md | 30 +- docs/configuration/dns/server.md | 4 +- docs/configuration/experimental/index.md | 8 +- docs/configuration/experimental/index.zh.md | 4 +- docs/configuration/inbound/http.md | 2 +- docs/configuration/inbound/http.zh.md | 2 +- docs/configuration/inbound/hysteria.md | 2 +- docs/configuration/inbound/hysteria2.md | 13 +- docs/configuration/inbound/hysteria2.zh.md | 10 +- docs/configuration/inbound/mixed.md | 2 +- docs/configuration/inbound/mixed.zh.md | 2 +- docs/configuration/inbound/naive.md | 2 +- docs/configuration/inbound/redirect.md | 2 +- docs/configuration/inbound/redirect.zh.md | 2 +- docs/configuration/inbound/tproxy.md | 2 +- docs/configuration/inbound/tproxy.zh.md | 2 +- docs/configuration/inbound/trojan.md | 2 +- docs/configuration/inbound/trojan.zh.md | 2 +- docs/configuration/inbound/tuic.md | 2 +- docs/configuration/inbound/tun.md | 12 +- docs/configuration/inbound/tun.zh.md | 10 +- docs/configuration/outbound/hysteria.md | 2 +- docs/configuration/outbound/hysteria2.md | 10 +- docs/configuration/outbound/hysteria2.zh.md | 6 + docs/configuration/outbound/selector.md | 2 +- docs/configuration/outbound/selector.zh.md | 2 +- docs/configuration/outbound/shadowsocksr.md | 2 +- docs/configuration/outbound/tor.md | 2 +- docs/configuration/outbound/tuic.md | 2 +- docs/configuration/outbound/wireguard.md | 4 +- docs/configuration/route/index.md | 8 +- docs/configuration/route/index.zh.md | 8 +- docs/configuration/route/rule.md | 30 +- docs/configuration/route/rule.zh.md | 30 +- docs/configuration/shared/dial.md | 2 +- docs/configuration/shared/dial.zh.md | 2 +- docs/configuration/shared/tls.md | 10 +- docs/configuration/shared/v2ray-transport.md | 4 +- docs/contributing/environment.md | 50 --- docs/contributing/index.md | 17 - docs/contributing/sub-projects.md | 67 --- docs/deprecated.md | 6 + docs/deprecated.zh.md | 17 + docs/examples/clash-api.md | 52 --- docs/examples/dns-hijack.md | 65 --- docs/examples/dns-hijack.zh.md | 65 --- docs/examples/fakeip.md | 106 ----- docs/examples/fakeip.zh.md | 106 ----- docs/examples/index.md | 11 - docs/examples/index.zh.md | 11 - docs/examples/linux-server-installation.md | 38 -- docs/examples/linux-server-installation.zh.md | 38 -- docs/examples/shadowsocks.md | 163 ------- docs/examples/shadowtls.md | 70 --- docs/examples/tun.md | 89 ---- docs/faq/fakeip.md | 19 - docs/faq/fakeip.zh.md | 18 - docs/faq/index.md | 58 --- docs/faq/index.zh.md | 50 --- docs/faq/known-issues.md | 20 - docs/faq/known-issues.zh.md | 19 - docs/features.md | 121 ----- docs/index.md | 2 +- docs/index.zh.md | 2 +- docs/installation/build-from-source.md | 63 +++ docs/installation/clients/sfa.md | 23 - docs/installation/clients/sfa.zh.md | 23 - docs/installation/clients/sfi.md | 23 - docs/installation/clients/sfi.zh.md | 23 - docs/installation/clients/sfm.md | 29 -- docs/installation/clients/sfm.zh.md | 29 -- docs/installation/clients/sft.md | 30 -- docs/installation/clients/sft.zh.md | 31 -- docs/installation/clients/specification.md | 25 -- docs/installation/docker.md | 31 ++ docs/installation/from-source.md | 50 --- docs/installation/from-source.zh.md | 39 -- docs/installation/package-manager.md | 90 ++++ docs/installation/package-manager/android.md | 7 - docs/installation/package-manager/macOS.md | 14 - docs/installation/package-manager/windows.md | 13 - docs/installation/scripts/arch-install.sh | 22 + docs/installation/scripts/deb-install.sh | 23 + docs/installation/scripts/rpm-install.sh | 22 + docs/manual/proxy-protocol/hysteria2.md | 211 +++++++++ docs/manual/proxy-protocol/shadowsocks.md | 126 ++++++ docs/manual/proxy-protocol/trojan.md | 214 +++++++++ docs/manual/proxy-protocol/tuic.md | 208 +++++++++ docs/manual/proxy/client.md | 425 ++++++++++++++++++ docs/manual/proxy/server.md | 10 + docs/support.md | 17 +- docs/support.zh.md | 18 +- mkdocs.yml | 145 +++--- 104 files changed, 2170 insertions(+), 1767 deletions(-) create mode 100644 docs/clients/android/features.md create mode 100644 docs/clients/android/index.md create mode 100644 docs/clients/apple/features.md create mode 100644 docs/clients/apple/index.md create mode 100644 docs/clients/general.md create mode 100644 docs/clients/index.md create mode 100644 docs/clients/index.zh.md create mode 100644 docs/clients/privacy.md delete mode 100644 docs/contributing/environment.md delete mode 100644 docs/contributing/index.md delete mode 100644 docs/contributing/sub-projects.md create mode 100644 docs/deprecated.zh.md delete mode 100644 docs/examples/clash-api.md delete mode 100644 docs/examples/dns-hijack.md delete mode 100644 docs/examples/dns-hijack.zh.md delete mode 100644 docs/examples/fakeip.md delete mode 100644 docs/examples/fakeip.zh.md delete mode 100644 docs/examples/index.md delete mode 100644 docs/examples/index.zh.md delete mode 100644 docs/examples/linux-server-installation.md delete mode 100644 docs/examples/linux-server-installation.zh.md delete mode 100644 docs/examples/shadowsocks.md delete mode 100644 docs/examples/shadowtls.md delete mode 100644 docs/examples/tun.md delete mode 100644 docs/faq/fakeip.md delete mode 100644 docs/faq/fakeip.zh.md delete mode 100644 docs/faq/index.md delete mode 100644 docs/faq/index.zh.md delete mode 100644 docs/faq/known-issues.md delete mode 100644 docs/faq/known-issues.zh.md delete mode 100644 docs/features.md create mode 100644 docs/installation/build-from-source.md delete mode 100644 docs/installation/clients/sfa.md delete mode 100644 docs/installation/clients/sfa.zh.md delete mode 100644 docs/installation/clients/sfi.md delete mode 100644 docs/installation/clients/sfi.zh.md delete mode 100644 docs/installation/clients/sfm.md delete mode 100644 docs/installation/clients/sfm.zh.md delete mode 100644 docs/installation/clients/sft.md delete mode 100644 docs/installation/clients/sft.zh.md delete mode 100644 docs/installation/clients/specification.md create mode 100644 docs/installation/docker.md delete mode 100644 docs/installation/from-source.md delete mode 100644 docs/installation/from-source.zh.md create mode 100644 docs/installation/package-manager.md delete mode 100644 docs/installation/package-manager/android.md delete mode 100644 docs/installation/package-manager/macOS.md delete mode 100644 docs/installation/package-manager/windows.md create mode 100644 docs/installation/scripts/arch-install.sh create mode 100644 docs/installation/scripts/deb-install.sh create mode 100644 docs/installation/scripts/rpm-install.sh create mode 100644 docs/manual/proxy-protocol/hysteria2.md create mode 100644 docs/manual/proxy-protocol/shadowsocks.md create mode 100644 docs/manual/proxy-protocol/trojan.md create mode 100644 docs/manual/proxy-protocol/tuic.md create mode 100644 docs/manual/proxy/client.md create mode 100644 docs/manual/proxy/server.md diff --git a/Makefile b/Makefile index ed62162a28..f1889ffc50 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ MAIN_PARAMS = $(PARAMS) -tags $(TAGS) MAIN = ./cmd/sing-box PREFIX ?= $(shell go env GOPATH) -.PHONY: test release +.PHONY: test release docs build: go build $(MAIN_PARAMS) $(MAIN) @@ -182,6 +182,14 @@ lib_install: go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230915142329-c6740b6d2950 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230915142329-c6740b6d2950 +docs: + mkdocs serve + +publish_docs: + mkdocs gh-deploy -m "Update" --force --ignore-version --no-history + +docs_install: + pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*" clean: rm -rf bin dist sing-box rm -f $(shell go env GOPATH)/sing-box diff --git a/docs/changelog.md b/docs/changelog.md index c500a2f262..dca53ad457 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,21 +1,80 @@ +--- +icon: material/alert-decagram +--- + +# ChangeLog + +#### 1.7.0-rc.3 + +* Fixes and improvements + +#### 1.7.0-rc.2 + +* Fix missing UDP user context on TUIC/Hysteria2 inbounds +* macOS: Add button for uninstall SystemExtension in the standalone graphical client + #### 1.6.6 * Fixes and improvements +#### 1.7.0-rc.1 + +* Fixes and improvements + +#### 1.7.0-beta.5 + +* Update gVisor to 20231113.0 +* Fixes and improvements + +#### 1.7.0-beta.4 + +* Add `wifi_ssid` and `wifi_bssid` route and DNS rules **1** +* Fixes and improvements + +**1**: + +Only supported in graphical clients on Android and iOS. + +#### 1.7.0-beta.3 + +* Fix zero TTL was incorrectly reset +* Fixes and improvements + #### 1.6.5 * Fix crash if TUIC inbound authentication failed * Fixes and improvements +#### 1.7.0-beta.2 + +* Fix crash if TUIC inbound authentication failed +* Update quic-go to v0.40.0 +* Fixes and improvements + #### 1.6.4 * Fixes and improvements +#### 1.7.0-beta.1 + +* Fixes and improvements + #### 1.6.3 * iOS/Android: Fix profile auto update * Fixes and improvements +#### 1.7.0-alpha.11 + +* iOS/Android: Fix profile auto update +* Fixes and improvements + +#### 1.7.0-alpha.10 + +* Fix tcp-brutal not working with TLS +* Fix Android client not closing in some cases +* Fixes and improvements + #### 1.6.2 * Fixes and improvements @@ -25,6 +84,34 @@ * Our [Android client](/installation/clients/sfa) is now available in the Google Play Store ▶️ * Fixes and improvements +#### 1.7.0-alpha.6 + +* Fixes and improvements + +#### 1.7.0-alpha.4 + +* Migrate multiplex and UoT server to inbound **1** +* Add TCP Brutal support for multiplex **2** + +**1**: + +Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound options. + +**2** + +Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server, see [TCP Brutal](/configuration/shared/tcp-brutal) for details. + +#### 1.7.0-alpha.3 + +* Add [HTTPUpgrade V2Ray transport](/configuration/shared/v2ray-transport#HTTPUpgrade) support **1** +* Fixes and improvements + +**1**: + +Introduced in V2Ray 5.10.0. + +The new HTTPUpgrade transport has better performance than WebSocket and is better suited for CDN abuse. + #### 1.6.0 * Fixes and improvements @@ -49,6 +136,23 @@ This update is intended to address the multi-send defects of the old implementat Based on discussions with the original author, the brutal CC and QUIC protocol parameters of the old protocol (Hysteria 1) have been updated to be consistent with Hysteria 2 +#### 1.7.0-alpha.2 + +* Fix bugs introduced in 1.7.0-alpha.1 + +#### 1.7.0-alpha.1 + +* Add [exclude route support](/configuration/inbound/tun) for TUN inbound +* Add `udp_disable_domain_unmapping` [inbound listen option](/configuration/shared/listen) **1** +* Fixes and improvements + +**1**: + +If enabled, for UDP proxy requests addressed to a domain, +the original packet address will be sent in the response instead of the mapped domain. + +This option is used for compatibility with clients that +do not support receiving UDP packets with domain addresses, such as Surge. #### 1.5.5 @@ -110,6 +214,24 @@ the old protocol (Hysteria 1) have been updated to be consistent with Hysteria 2 * Update golang.org/x/net to v0.17.0 * Fixes and improvements +#### 1.6.0-beta.3 + +* Update the legacy Hysteria protocol **1** +* Fixes and improvements + +**1** + +Based on discussions with the original author, the brutal CC and QUIC protocol parameters of +the old protocol (Hysteria 1) have been updated to be consistent with Hysteria 2 + +#### 1.6.0-beta.2 + +* Add TLS self sign key pair generate command +* Update brutal congestion control for Hysteria2 +* Fix Clash cache crash on arm32 devices +* Update golang.org/x/net to v0.17.0 +* Fixes and improvements + #### 1.5.3 * Fix compatibility with Android 14 diff --git a/docs/clients/android/features.md b/docs/clients/android/features.md new file mode 100644 index 0000000000..2702e6fa8c --- /dev/null +++ b/docs/clients/android/features.md @@ -0,0 +1,64 @@ +# :material-decagram: Features + +#### UI options + +* Display realtime network speed in the notification + +#### Service + +SFA allows you to run sing-box through ForegroundService or VpnService (when TUN is required). + +#### TUN + +SFA provides an unprivileged TUN implementation through Android VpnService. + +| TUN inbound option | Available | Note | +|-------------------------------|------------------|--------------------| +| `interface_name` | :material-close: | Managed by Android | +| `inet4_address` | :material-check: | / | +| `inet6_address` | :material-check: | / | +| `mtu` | :material-check: | / | +| `auto_route` | :material-check: | / | +| `strict_route` | :material-close: | Not implemented | +| `inet4_route_address` | :material-check: | / | +| `inet6_route_address` | :material-check: | / | +| `inet4_route_exclude_address` | :material-check: | / | +| `inet6_route_exclude_address` | :material-check: | / | +| `endpoint_independent_nat` | :material-check: | / | +| `stack` | :material-check: | / | +| `include_interface` | :material-close: | No permission | +| `exclude_interface` | :material-close: | No permission | +| `include_uid` | :material-close: | No permission | +| `exclude_uid` | :material-close: | No permission | +| `include_android_user` | :material-close: | No permission | +| `include_package` | :material-check: | / | +| `exclude_package` | :material-check: | / | +| `platform` | :material-check: | / | + +| Route/DNS rule option | Available | Note | +|-----------------------|------------------|-----------------------------------| +| `process_name` | :material-close: | No permission | +| `process_path` | :material-close: | No permission | +| `package_name` | :material-check: | / | +| `user` | :material-close: | Use `package_name` instead | +| `user_id` | :material-close: | Use `package_name` instead | +| `wifi_ssid` | :material-check: | Fine location permission required | +| `wifi_bssid` | :material-check: | Fine location permission required | + +### Override + +Overrides profile configuration items with platform-specific values. + +#### Per-app proxy + +SFA allows you to select a list of Android apps that require proxying or bypassing in the graphical interface to +override the `include_package` and `exclude_package` configuration items. + +In particular, the selector also provides the “China apps” scanning feature, providing Chinese users with an excellent +experience to bypass apps that do not require a proxy. Specifically, by scanning China application or SDK +characteristics through dex class path and other means, there will be almost no missed reports. + +### Chore + +* The working directory is located at `/sdcard/Android/data/io.nekohasekai.sfa/files` (External files directory) +* Crash logs is located in `$working_directory/stderr.log` diff --git a/docs/clients/android/index.md b/docs/clients/android/index.md new file mode 100644 index 0000000000..cbd1d387de --- /dev/null +++ b/docs/clients/android/index.md @@ -0,0 +1,22 @@ +--- +icon: material/android +--- + +# sing-box for Android + +SFA allows users to manage and run local or remote sing-box configuration files, and provides +platform-specific function implementation, such as TUN transparent proxy implementation. + +## :material-graph: Requirements + +* Android 5.0+ + +## :material-download: Download + +* [Play Store](https://play.google.com/store/apps/details?id=io.nekohasekai.sfa) +* [Play Store (Beta)](https://play.google.com/apps/testing/io.nekohasekai.sfa) +* [GitHub Releases](https://github.com/SagerNet/sing-box/releases) + +## :material-source-repository: Source code + +* [GitHub](https://github.com/SagerNet/sing-box-for-android) diff --git a/docs/clients/apple/features.md b/docs/clients/apple/features.md new file mode 100644 index 0000000000..95143d98f8 --- /dev/null +++ b/docs/clients/apple/features.md @@ -0,0 +1,52 @@ +# :material-decagram: Features + +#### UI options + +* Always On +* Include All Networks (Proxy traffic for LAN and cellular services) +* (Apple tvOS) Import profile from iPhone/iPad + +#### Service + +SFI/SFM/SFT allows you to run sing-box through NetworkExtension with Application Extension or System Extension. + +#### TUN + +SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension. + +| TUN inbound option | Available | Note | +|-------------------------------|-----------|-------------------| +| `interface_name` | ✖️ | Managed by Darwin | +| `inet4_address` | ✔️ | / | +| `inet6_address` | ✔️ | / | +| `mtu` | ✔️ | / | +| `auto_route` | ✔️ | / | +| `strict_route` | ✖️ | Not implemented | +| `inet4_route_address` | ✔️ | / | +| `inet6_route_address` | ✔️ | / | +| `inet4_route_exclude_address` | ✔️ | / | +| `inet6_route_exclude_address` | ✔️ | / | +| `endpoint_independent_nat` | ✔️ | / | +| `stack` | ✔️ | / | +| `include_interface` | ✖️ | Not implemented | +| `exclude_interface` | ✖️ | Not implemented | +| `include_uid` | ✖️ | Not implemented | +| `exclude_uid` | ✖️ | Not implemented | +| `include_android_user` | ✖️ | Not implemented | +| `include_package` | ✖️ | Not implemented | +| `exclude_package` | ✖️ | Not implemented | +| `platform` | ✔️ | / | + +| Route/DNS rule option | Available | Note | +|-----------------------|------------------|-----------------------| +| `process_name` | :material-close: | No permission | +| `process_path` | :material-close: | No permission | +| `package_name` | :material-close: | / | +| `user` | :material-close: | No permission | +| `user_id` | :material-close: | No permission | +| `wifi_ssid` | :material-alert: | Only supported on iOS | +| `wifi_bssid` | :material-alert: | Only supported on iOS | + +### Chore + +* Crash logs is located in `Settings` -> `View Service Log` diff --git a/docs/clients/apple/index.md b/docs/clients/apple/index.md new file mode 100644 index 0000000000..36a4edd9f4 --- /dev/null +++ b/docs/clients/apple/index.md @@ -0,0 +1,32 @@ +--- +icon: material/apple +--- + +# sing-box for Apple platforms + +SFI/SFM/SFT allows users to manage and run local or remote sing-box configuration files, and provides +platform-specific function implementation, such as TUN transparent proxy implementation. + +## :material-graph: Requirements + +* iOS 15.0+ / macOS 13.0+ / Apple tvOS 17.0+ +* An Apple account outside of mainland China + +## :material-download: Download + +* [App Store](https://apps.apple.com/us/app/sing-box/id6451272673) +* [TestFlight (Beta)](https://testflight.apple.com/join/AcqO44FH) + +## :material-file-download: Download (macOS standalone version) + +* [Homebrew Cask](https://formulae.brew.sh/cask/sfm) + +```bash +brew install sfm +``` + +* [GitHub Releases](https://github.com/SagerNet/sing-box/releases) + +## :material-source-repository: Source code + +* [GitHub](https://github.com/SagerNet/sing-box-for-apple) diff --git a/docs/clients/general.md b/docs/clients/general.md new file mode 100644 index 0000000000..4d57c991ce --- /dev/null +++ b/docs/clients/general.md @@ -0,0 +1,63 @@ +--- +icon: material/pencil-ruler +--- + +# General + +Describes and explains the functions implemented uniformly by sing-box graphical clients. + +### Profile + +Profile describes a sing-box configuration file and its state. + +#### Local + +* Local Profile represents a local sing-box configuration with minimal state +* The graphical client must provide an editor to modify configuration content + +#### iCloud (on iOS and macOS) + +* iCloud Profile represents a remote sing-box configuration with iCloud as the update source +* The configuration file is stored in the sing-box folder under iCloud +* The graphical client must provide an editor to modify configuration content + +#### Remote + +* Remote Profile represents a remote sing-box configuration with a URL as the update source. +* The graphical client should provide a configuration content viewer +* The graphical client must implement automatic profile update (default interval is 60 minutes) and HTTP Basic + authorization. + +At the same time, the graphical client must provide support for importing remote profiles +through a specific URL Scheme. The URL is defined as follows: + +``` +sing-box://import-remote-profile?url=urlEncodedURL#urlEncodedName +``` + +### Dashboard + +While the sing-box service is running, the graphical client should provide a Dashboard interface to manage the service. + +#### Status + +Dashboard should display status information such as memory, connection, and traffic. + +#### Mode + +Dashboard should provide a Mode selector for switching when the configuration uses at least two `clash_mode` values. + +#### Groups + +When the configuration includes group outbounds (specifically, Selector or URLTest), +the dashboard should provide a Group selector for status display or switching. + +### Chore + +#### Core + +Graphical clients should provide a Core region: + +* Display the current sing-box version +* Provides a button to clean the working directory +* Provides a memory limiter switch \ No newline at end of file diff --git a/docs/clients/index.md b/docs/clients/index.md new file mode 100644 index 0000000000..b80c161867 --- /dev/null +++ b/docs/clients/index.md @@ -0,0 +1,13 @@ +# :material-cellphone-link: Graphical Clients + +Maintained by Project S to provide a unified experience and platform-specific functionality. + +| Platform | Client | +|---------------------------------------|-----------------------------------------| +| :material-android: Android | [sing-box for Android](./android) | +| :material-apple: iOS/macOS/Apple tvOS | [sing-box for Apple platforms](./apple) | +| :material-laptop: Desktop | Working in progress | + +Some third-party projects that claim to use sing-box or use sing-box as a selling point are not listed here. The core +motivation of the maintainers of such projects is to acquire more users, and even though they provide friendly VPN +client features, the code is usually of poor quality and contains ads. diff --git a/docs/clients/index.zh.md b/docs/clients/index.zh.md new file mode 100644 index 0000000000..ef643086ac --- /dev/null +++ b/docs/clients/index.zh.md @@ -0,0 +1,12 @@ +# :material-cellphone-link: 图形界面客户端 + +由 Project S 维护,提供统一的体验与平台特定的功能。 + +| 平台 | 客户端 | +|---------------------------------------|-----------------------------------------| +| :material-android: Android | [sing-box for Android](./android) | +| :material-apple: iOS/macOS/Apple tvOS | [sing-box for Apple platforms](./apple) | +| :material-laptop: Desktop | 施工中 | + +此处没有列出一些声称使用或以 sing-box 为卖点的第三方项目。此类项目维护者的动机是获得更多用户,即使它们提供友好的商业 +VPN 客户端功能, 但代码质量很差且包含广告。 diff --git a/docs/clients/privacy.md b/docs/clients/privacy.md new file mode 100644 index 0000000000..f8b51afd1a --- /dev/null +++ b/docs/clients/privacy.md @@ -0,0 +1,8 @@ +--- +icon: material/security +--- + +# Privacy policy + +sing-box and official graphics clients do not collect or share personal data, +and the data generated by the software is always on your device. diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index f7c46db396..18e352b407 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -79,6 +79,12 @@ 1000 ], "clash_mode": "direct", + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], "invert": false, "outbound": [ "direct" @@ -188,7 +194,7 @@ Match port range. #### process_name -!!! error "" +!!! quote "" Only supported on Linux, Windows, and macOS. @@ -196,7 +202,7 @@ Match process name. #### process_path -!!! error "" +!!! quote "" Only supported on Linux, Windows, and macOS. @@ -208,7 +214,7 @@ Match android package name. #### user -!!! error "" +!!! quote "" Only supported on Linux. @@ -216,7 +222,7 @@ Match user name. #### user_id -!!! error "" +!!! quote "" Only supported on Linux. @@ -226,6 +232,24 @@ Match user id. Match Clash mode. +#### wifi_ssid + + + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi SSID. + +#### wifi_bssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi BSSID. + #### invert Invert match result. diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 9af71a40b3..98bfa8ab9a 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -78,6 +78,12 @@ 1000 ], "clash_mode": "direct", + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], "invert": false, "outbound": [ "direct" @@ -185,7 +191,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### process_name -!!! error "" +!!! quote "" 仅支持 Linux、Windows 和 macOS. @@ -193,7 +199,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### process_path -!!! error "" +!!! quote "" 仅支持 Linux、Windows 和 macOS. @@ -205,7 +211,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### user -!!! error "" +!!! quote "" 仅支持 Linux。 @@ -213,7 +219,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### user_id -!!! error "" +!!! quote "" 仅支持 Linux。 @@ -223,6 +229,22 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配 Clash 模式。 +#### wifi_ssid + +!!! quote "" + + 仅在 Android 与 iOS 的图形客户端中支持。 + +匹配 WiFi SSID。 + +#### wifi_bssid + +!!! quote "" + + 仅在 Android 与 iOS 的图形客户端中支持。 + +匹配 WiFi BSSID。 + #### invert 反选匹配结果。 diff --git a/docs/configuration/dns/server.md b/docs/configuration/dns/server.md index 48fb1bb0c0..2ce50b38e1 100644 --- a/docs/configuration/dns/server.md +++ b/docs/configuration/dns/server.md @@ -49,7 +49,7 @@ The address of the dns server. !!! warning "" - QUIC and HTTP3 transport is not included by default, see [Installation](/#installation). + QUIC and HTTP3 transport is not included by default, see [Installation](./#installation). !!! info "" @@ -57,7 +57,7 @@ The address of the dns server. !!! warning "" - DHCP transport is not included by default, see [Installation](/#installation). + DHCP transport is not included by default, see [Installation](./#installation). | RCode | Description | |-------------------|-----------------------| diff --git a/docs/configuration/experimental/index.md b/docs/configuration/experimental/index.md index 9af9ec09a9..308e851c3b 100644 --- a/docs/configuration/experimental/index.md +++ b/docs/configuration/experimental/index.md @@ -44,9 +44,9 @@ ### Clash API Fields -!!! error "" +!!! quote "" - Clash API is not included by default, see [Installation](/#installation). + Clash API is not included by default, see [Installation](./#installation). #### external_controller @@ -110,9 +110,9 @@ If not empty, `store_selected` will use a separate store keyed by it. ### V2Ray API Fields -!!! error "" +!!! quote "" - V2Ray API is not included by default, see [Installation](/#installation). + V2Ray API is not included by default, see [Installation](./#installation). #### listen diff --git a/docs/configuration/experimental/index.zh.md b/docs/configuration/experimental/index.zh.md index 7bc2a62c9d..88a95852c9 100644 --- a/docs/configuration/experimental/index.zh.md +++ b/docs/configuration/experimental/index.zh.md @@ -44,7 +44,7 @@ ### Clash API 字段 -!!! error "" +!!! quote "" 默认安装不包含 Clash API,参阅 [安装](/zh/#_2)。 @@ -108,7 +108,7 @@ Clash 中的默认模式,默认使用 `Rule`。 ### V2Ray API 字段 -!!! error "" +!!! quote "" 默认安装不包含 V2Ray API,参阅 [安装](/zh/#_2)。 diff --git a/docs/configuration/inbound/http.md b/docs/configuration/inbound/http.md index dc80cf0aa9..3b14905015 100644 --- a/docs/configuration/inbound/http.md +++ b/docs/configuration/inbound/http.md @@ -36,7 +36,7 @@ No authentication required if empty. #### set_system_proxy -!!! error "" +!!! quote "" Only supported on Linux, Android, Windows, and macOS. diff --git a/docs/configuration/inbound/http.zh.md b/docs/configuration/inbound/http.zh.md index b7228eb9c3..2f3d44f556 100644 --- a/docs/configuration/inbound/http.zh.md +++ b/docs/configuration/inbound/http.zh.md @@ -36,7 +36,7 @@ HTTP 用户 #### set_system_proxy -!!! error "" +!!! quote "" 仅支持 Linux、Android、Windows 和 macOS。 diff --git a/docs/configuration/inbound/hysteria.md b/docs/configuration/inbound/hysteria.md index 5026010493..f027a05650 100644 --- a/docs/configuration/inbound/hysteria.md +++ b/docs/configuration/inbound/hysteria.md @@ -31,7 +31,7 @@ !!! warning "" - QUIC, which is required by hysteria is not included by default, see [Installation](/#installation). + QUIC, which is required by hysteria is not included by default, see [Installation](./#installation). ### Listen Fields diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md index ab6f2e4dd1..4427b6517e 100644 --- a/docs/configuration/inbound/hysteria2.md +++ b/docs/configuration/inbound/hysteria2.md @@ -4,8 +4,8 @@ { "type": "hysteria2", "tag": "hy2-in", - - ... // Listen Fields + ... + // Listen Fields "up_mbps": 100, "down_mbps": 100, @@ -28,7 +28,14 @@ !!! warning "" - QUIC, which is required by Hysteria2 is not included by default, see [Installation](/#installation). + QUIC, which is required by Hysteria2 is not included by default, see [Installation](./#installation). + +!!! warning "Difference from official Hysteria2" + + The official program supports an authentication method called **userpass**, + which essentially uses a combination of `:` as the actual password, + while sing-box does not provide this alias. + To use sing-box with the official program, you need to fill in that combination as the actual password. ### Listen Fields diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index f43188c09f..4d5a94157c 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -4,8 +4,8 @@ { "type": "hysteria2", "tag": "hy2-in", - - ... // 监听字段 + ... + // 监听字段 "up_mbps": 100, "down_mbps": 100, @@ -30,6 +30,12 @@ 默认安装不包含被 Hysteria2 依赖的 QUIC,参阅 [安装](/zh/#_2)。 +!!! warning "与官方 Hysteria2 的区别" + + 官方程序支持一种名为 **userpass** 的验证方式, + 本质上上是将用户名与密码的组合 `:` 作为实际上的密码,而 sing-box 不提供此别名。 + 要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。 + ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 diff --git a/docs/configuration/inbound/mixed.md b/docs/configuration/inbound/mixed.md index 9e262c7d57..62313f489d 100644 --- a/docs/configuration/inbound/mixed.md +++ b/docs/configuration/inbound/mixed.md @@ -33,7 +33,7 @@ No authentication required if empty. #### set_system_proxy -!!! error "" +!!! quote "" Only supported on Linux, Android, Windows, and macOS. diff --git a/docs/configuration/inbound/mixed.zh.md b/docs/configuration/inbound/mixed.zh.md index 8f00af14d4..448c66b464 100644 --- a/docs/configuration/inbound/mixed.zh.md +++ b/docs/configuration/inbound/mixed.zh.md @@ -33,7 +33,7 @@ SOCKS 和 HTTP 用户 #### set_system_proxy -!!! error "" +!!! quote "" 仅支持 Linux、Android、Windows 和 macOS。 diff --git a/docs/configuration/inbound/naive.md b/docs/configuration/inbound/naive.md index 35ad109616..562a70703d 100644 --- a/docs/configuration/inbound/naive.md +++ b/docs/configuration/inbound/naive.md @@ -20,7 +20,7 @@ !!! warning "" - HTTP3 transport is not included by default, see [Installation](/#installation). + HTTP3 transport is not included by default, see [Installation](./#installation). ### Listen Fields diff --git a/docs/configuration/inbound/redirect.md b/docs/configuration/inbound/redirect.md index 9ea418eef6..97736e286b 100644 --- a/docs/configuration/inbound/redirect.md +++ b/docs/configuration/inbound/redirect.md @@ -1,4 +1,4 @@ -!!! error "" +!!! quote "" Only supported on Linux and macOS. diff --git a/docs/configuration/inbound/redirect.zh.md b/docs/configuration/inbound/redirect.zh.md index 125dc73f84..a03049e5cd 100644 --- a/docs/configuration/inbound/redirect.zh.md +++ b/docs/configuration/inbound/redirect.zh.md @@ -1,4 +1,4 @@ -!!! error "" +!!! quote "" 仅支持 Linux 和 macOS。 diff --git a/docs/configuration/inbound/tproxy.md b/docs/configuration/inbound/tproxy.md index c637707294..4975931e89 100644 --- a/docs/configuration/inbound/tproxy.md +++ b/docs/configuration/inbound/tproxy.md @@ -1,4 +1,4 @@ -!!! error "" +!!! quote "" Only supported on Linux. diff --git a/docs/configuration/inbound/tproxy.zh.md b/docs/configuration/inbound/tproxy.zh.md index 9ab98e5782..6e35ad5e0c 100644 --- a/docs/configuration/inbound/tproxy.zh.md +++ b/docs/configuration/inbound/tproxy.zh.md @@ -1,4 +1,4 @@ -!!! error "" +!!! quote "" 仅支持 Linux。 diff --git a/docs/configuration/inbound/trojan.md b/docs/configuration/inbound/trojan.md index 787d2b11bf..cd45539ddc 100644 --- a/docs/configuration/inbound/trojan.md +++ b/docs/configuration/inbound/trojan.md @@ -47,7 +47,7 @@ TLS configuration, see [TLS](/configuration/shared/tls/#inbound). #### fallback -!!! error "" +!!! quote "" There is no evidence that GFW detects and blocks Trojan servers based on HTTP responses, and opening the standard http/s port on the server is a much bigger signature. diff --git a/docs/configuration/inbound/trojan.zh.md b/docs/configuration/inbound/trojan.zh.md index f52422af82..54144ae680 100644 --- a/docs/configuration/inbound/trojan.zh.md +++ b/docs/configuration/inbound/trojan.zh.md @@ -49,7 +49,7 @@ TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 #### fallback -!!! error "" +!!! quote "" 没有证据表明 GFW 基于 HTTP 响应检测并阻止 Trojan 服务器,并且在服务器上打开标准 http/s 端口是一个更大的特征。 diff --git a/docs/configuration/inbound/tuic.md b/docs/configuration/inbound/tuic.md index 51724e5d84..d4d9aafdb2 100644 --- a/docs/configuration/inbound/tuic.md +++ b/docs/configuration/inbound/tuic.md @@ -24,7 +24,7 @@ !!! warning "" - QUIC, which is required by TUIC is not included by default, see [Installation](/#installation). + QUIC, which is required by TUIC is not included by default, see [Installation](./#installation). ### Listen Fields diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index e6c52c54c1..1d493d58e8 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -1,4 +1,4 @@ -!!! error "" +!!! quote "" Only supported on Linux, Windows and macOS. @@ -102,7 +102,7 @@ The maximum transmission unit. Set the default route to the Tun. -!!! error "" +!!! quote "" To avoid traffic loopback, set `route.auto_detect_interface` or `route.default_interface` or `outbound.bind_interface` @@ -171,11 +171,11 @@ TCP/IP stack. !!! warning "" - gVisor and LWIP stacks is not included by default, see [Installation](/#installation). + gVisor and LWIP stacks is not included by default, see [Installation](./#installation). #### include_interface -!!! error "" +!!! quote "" Interface rules are only supported on Linux and require auto_route. @@ -191,7 +191,7 @@ Conflict with `include_interface`. #### include_uid -!!! error "" +!!! quote "" UID rules are only supported on Linux and require auto_route. @@ -211,7 +211,7 @@ Exclude users in route, but in range. #### include_android_user -!!! error "" +!!! quote "" Android user and package rules are only supported on Android and require auto_route. diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index 8f246c042b..7ea3a6a030 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -1,4 +1,4 @@ -!!! error "" +!!! quote "" 仅支持 Linux、Windows 和 macOS。 @@ -102,7 +102,7 @@ tun 接口的 IPv6 前缀。 设置到 Tun 的默认路由。 -!!! error "" +!!! quote "" 为避免流量环回,请设置 `route.auto_detect_interface` 或 `route.default_interface` 或 `outbound.bind_interface`。 @@ -171,7 +171,7 @@ TCP/IP 栈。 #### include_interface -!!! error "" +!!! quote "" 接口规则仅在 Linux 下被支持,并且需要 `auto_route`。 @@ -187,7 +187,7 @@ TCP/IP 栈。 #### include_uid -!!! error "" +!!! quote "" UID 规则仅在 Linux 下被支持,并且需要 `auto_route`。 @@ -207,7 +207,7 @@ TCP/IP 栈。 #### include_android_user -!!! error "" +!!! quote "" Android 用户和应用规则仅在 Android 下被支持,并且需要 `auto_route`。 diff --git a/docs/configuration/outbound/hysteria.md b/docs/configuration/outbound/hysteria.md index 8eafa99c27..ff9974de61 100644 --- a/docs/configuration/outbound/hysteria.md +++ b/docs/configuration/outbound/hysteria.md @@ -26,7 +26,7 @@ !!! warning "" - QUIC, which is required by hysteria is not included by default, see [Installation](/#installation). + QUIC, which is required by hysteria is not included by default, see [Installation](./#installation). ### Fields diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md index 9861e332ac..90860c1c4f 100644 --- a/docs/configuration/outbound/hysteria2.md +++ b/docs/configuration/outbound/hysteria2.md @@ -24,7 +24,15 @@ !!! warning "" - QUIC, which is required by Hysteria2 is not included by default, see [Installation](/#installation). + QUIC, which is required by Hysteria2 is not included by default, see [Installation](./#installation). + +!!! warning "Difference from official Hysteria2" + + The official Hysteria2 supports an authentication method called **userpass**, + which essentially uses a combination of `:` as the actual password, + while sing-box does not provide this alias. + If you are planning to use sing-box with the official program, + please note that you will need to fill the combination as the password. ### Fields diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index 1e490a63af..5d20802794 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -26,6 +26,12 @@ 默认安装不包含被 Hysteria2 依赖的 QUIC,参阅 [安装](/zh/#_2)。 +!!! warning "与官方 Hysteria2 的区别" + + 官方程序支持一种名为 **userpass** 的验证方式, + 本质上上是将用户名与密码的组合 `:` 作为实际上的密码,而 sing-box 不提供此别名。 + 要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。 + ### 字段 #### server diff --git a/docs/configuration/outbound/selector.md b/docs/configuration/outbound/selector.md index 1d2c74a9ea..ee75358f5b 100644 --- a/docs/configuration/outbound/selector.md +++ b/docs/configuration/outbound/selector.md @@ -15,7 +15,7 @@ } ``` -!!! error "" +!!! quote "" The selector can only be controlled through the [Clash API](/configuration/experimental#clash-api-fields) currently. diff --git a/docs/configuration/outbound/selector.zh.md b/docs/configuration/outbound/selector.zh.md index 9e985ab1fa..ffe2d70ae1 100644 --- a/docs/configuration/outbound/selector.zh.md +++ b/docs/configuration/outbound/selector.zh.md @@ -15,7 +15,7 @@ } ``` -!!! error "" +!!! quote "" 选择器目前只能通过 [Clash API](/zh/configuration/experimental#clash-api) 来控制。 diff --git a/docs/configuration/outbound/shadowsocksr.md b/docs/configuration/outbound/shadowsocksr.md index 43c71b990f..0c7f1b32b7 100644 --- a/docs/configuration/outbound/shadowsocksr.md +++ b/docs/configuration/outbound/shadowsocksr.md @@ -25,7 +25,7 @@ !!! warning "" - ShadowsocksR is not included by default, see [Installation](/#installation). + ShadowsocksR is not included by default, see [Installation](./#installation). ### Fields diff --git a/docs/configuration/outbound/tor.md b/docs/configuration/outbound/tor.md index 2b0cc9f00e..fe7e4ff6f9 100644 --- a/docs/configuration/outbound/tor.md +++ b/docs/configuration/outbound/tor.md @@ -18,7 +18,7 @@ !!! info "" - Embedded tor is not included by default, see [Installation](/#installation). + Embedded tor is not included by default, see [Installation](./#installation). ### Fields diff --git a/docs/configuration/outbound/tuic.md b/docs/configuration/outbound/tuic.md index bbcc199ff2..522e78924a 100644 --- a/docs/configuration/outbound/tuic.md +++ b/docs/configuration/outbound/tuic.md @@ -23,7 +23,7 @@ !!! warning "" - QUIC, which is required by TUIC is not included by default, see [Installation](/#installation). + QUIC, which is required by TUIC is not included by default, see [Installation](./#installation). ### Fields diff --git a/docs/configuration/outbound/wireguard.md b/docs/configuration/outbound/wireguard.md index f4a8810895..c9c49c79b8 100644 --- a/docs/configuration/outbound/wireguard.md +++ b/docs/configuration/outbound/wireguard.md @@ -38,11 +38,11 @@ !!! warning "" - WireGuard is not included by default, see [Installation](/#installation). + WireGuard is not included by default, see [Installation](./#installation). !!! warning "" - gVisor, which is required by the unprivileged WireGuard is not included by default, see [Installation](/#installation). + gVisor, which is required by the unprivileged WireGuard is not included by default, see [Installation](./#installation). ### Fields diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 8c6ca6e110..846d497330 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -31,7 +31,7 @@ Default outbound tag. the first outbound will be used if empty. #### auto_detect_interface -!!! error "" +!!! quote "" Only supported on Linux, Windows and macOS. @@ -41,7 +41,7 @@ Takes no effect if `outbound.bind_interface` is set. #### override_android_vpn -!!! error "" +!!! quote "" Only supported on Android. @@ -49,7 +49,7 @@ Accept Android VPN as upstream NIC when `auto_detect_interface` enabled. #### default_interface -!!! error "" +!!! quote "" Only supported on Linux, Windows and macOS. @@ -59,7 +59,7 @@ Takes no effect if `auto_detect_interface` is set. #### default_mark -!!! error "" +!!! quote "" Only supported on Linux. diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index c3ef2e54cf..8bef5bea9a 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -32,7 +32,7 @@ #### auto_detect_interface -!!! error "" +!!! quote "" 仅支持 Linux、Windows 和 macOS。 @@ -42,7 +42,7 @@ #### override_android_vpn -!!! error "" +!!! quote "" 仅支持 Android。 @@ -50,7 +50,7 @@ #### default_interface -!!! error "" +!!! quote "" 仅支持 Linux、Windows 和 macOS。 @@ -60,7 +60,7 @@ #### default_mark -!!! error "" +!!! quote "" 仅支持 Linux。 diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 3cee478dc3..abbfde6fdc 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -83,6 +83,12 @@ 1000 ], "clash_mode": "direct", + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], "invert": false, "outbound": "direct" }, @@ -190,7 +196,7 @@ Match port range. #### process_name -!!! error "" +!!! quote "" Only supported on Linux, Windows, and macOS. @@ -198,7 +204,7 @@ Match process name. #### process_path -!!! error "" +!!! quote "" Only supported on Linux, Windows, and macOS. @@ -210,7 +216,7 @@ Match android package name. #### user -!!! error "" +!!! quote "" Only supported on Linux. @@ -218,7 +224,7 @@ Match user name. #### user_id -!!! error "" +!!! quote "" Only supported on Linux. @@ -228,6 +234,22 @@ Match user id. Match Clash mode. +#### wifi_ssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi SSID. + +#### wifi_bssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi BSSID. + #### invert Invert match result. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 4a09ed8e64..f4ab7890a7 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -81,6 +81,12 @@ 1000 ], "clash_mode": "direct", + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], "invert": false, "outbound": "direct" }, @@ -188,7 +194,7 @@ #### process_name -!!! error "" +!!! quote "" 仅支持 Linux、Windows 和 macOS。 @@ -196,7 +202,7 @@ #### process_path -!!! error "" +!!! quote "" 仅支持 Linux、Windows 和 macOS. @@ -208,7 +214,7 @@ #### user -!!! error "" +!!! quote "" 仅支持 Linux. @@ -216,7 +222,7 @@ #### user_id -!!! error "" +!!! quote "" 仅支持 Linux. @@ -226,6 +232,22 @@ 匹配 Clash 模式。 +#### wifi_ssid + +!!! quote "" + + 仅在 Android 与 iOS 的图形客户端中支持。 + +匹配 WiFi SSID。 + +#### wifi_bssid + +!!! quote "" + + 仅在 Android 与 iOS 的图形客户端中支持。 + +匹配 WiFi BSSID。 + #### invert 反选匹配结果。 diff --git a/docs/configuration/shared/dial.md b/docs/configuration/shared/dial.md index 1f524f0800..8139c7518c 100644 --- a/docs/configuration/shared/dial.md +++ b/docs/configuration/shared/dial.md @@ -41,7 +41,7 @@ The IPv6 address to bind to. #### routing_mark -!!! error "" +!!! quote "" Only supported on Linux. diff --git a/docs/configuration/shared/dial.zh.md b/docs/configuration/shared/dial.zh.md index 62b094f3fb..300e99ff7a 100644 --- a/docs/configuration/shared/dial.zh.md +++ b/docs/configuration/shared/dial.zh.md @@ -44,7 +44,7 @@ #### routing_mark -!!! error "" +!!! quote "" 仅支持 Linux。 diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index 4d395f7786..9a02bbff2f 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -201,7 +201,7 @@ The path to the server private key, in PEM format. !!! warning "" - uTLS is not included by default, see [Installation](/#installation). + uTLS is not included by default, see [Installation](./#installation). !!! note "" @@ -228,7 +228,7 @@ Chrome fingerprint will be used if empty. !!! warning "" - ECH is not included by default, see [Installation](/#installation). + ECH is not included by default, see [Installation](./#installation). ECH (Encrypted Client Hello) is a TLS extension that allows a client to encrypt the first part of its ClientHello message. @@ -280,7 +280,7 @@ If empty, load from DNS will be attempted. !!! warning "" - ACME is not included by default, see [Installation](/#installation). + ACME is not included by default, see [Installation](./#installation). #### domain @@ -359,11 +359,11 @@ See [DNS01 Challenge Fields](/configuration/shared/dns01_challenge) for details. !!! warning "" - reality server is not included by default, see [Installation](/#installation). + reality server is not included by default, see [Installation](./#installation). !!! warning "" - uTLS, which is required by reality client is not included by default, see [Installation](/#installation). + uTLS, which is required by reality client is not included by default, see [Installation](./#installation). #### handshake diff --git a/docs/configuration/shared/v2ray-transport.md b/docs/configuration/shared/v2ray-transport.md index 418ef28db2..b078bac8af 100644 --- a/docs/configuration/shared/v2ray-transport.md +++ b/docs/configuration/shared/v2ray-transport.md @@ -131,7 +131,7 @@ It needs to be consistent with the server. !!! warning "" - QUIC is not included by default, see [Installation](/#installation). + QUIC is not included by default, see [Installation](./#installation). !!! warning "Difference from v2ray-core" @@ -142,7 +142,7 @@ It needs to be consistent with the server. !!! note "" - standard gRPC has good compatibility but poor performance and is not included by default, see [Installation](/#installation). + standard gRPC has good compatibility but poor performance and is not included by default, see [Installation](./#installation). ```json { diff --git a/docs/contributing/environment.md b/docs/contributing/environment.md deleted file mode 100644 index db2ac91aff..0000000000 --- a/docs/contributing/environment.md +++ /dev/null @@ -1,50 +0,0 @@ -# Development environment - -#### For the documentation - -##### Setup - -You need to configure python3 and pip first. - -```shell -pip install mkdocs-material mkdocs-static-i18n -``` - -##### Run the site locally - -```shell -mkdocs serve -``` - -or - -```shell -python3 -m mkdocs serve -``` - -#### For the project - -By default you have the latest Go installed (currently 1.19), and added `GOPATH/bin` to the PATH environment variable. - -##### Setup - -```shell -make fmt_insalll -make lint_install -``` - -This installs the formatting and lint tools, which can be used via `make fmt` and `make lint`. - -For ProtoBuffer changes, you also need `make proto_install` and `make proto`. - -##### Build binary to the project directory - -```shell -make -``` - -##### Install binary to GOPATH/bin - -```shell -make install -``` \ No newline at end of file diff --git a/docs/contributing/index.md b/docs/contributing/index.md deleted file mode 100644 index d669bda62c..0000000000 --- a/docs/contributing/index.md +++ /dev/null @@ -1,17 +0,0 @@ -# Contributing to sing-box - -An introduction to contributing to the sing-box project. - -The sing-box project welcomes, and depends, on contributions from developers and users in the open source community. -Contributions can be made in a number of ways, a few examples are: - -* Code patches via pull requests -* Documentation improvements -* Bug reports and patch reviews - -### Reporting an Issue? - -Please follow -the [issue template](https://github.com/SagerNet/sing-box/issues/new?assignees=&labels=&template=bug_report.yml) to -submit bugs. Always include **FULL** log content, especially if you don't understand the code that generates it. - diff --git a/docs/contributing/sub-projects.md b/docs/contributing/sub-projects.md deleted file mode 100644 index 3f62a52cd9..0000000000 --- a/docs/contributing/sub-projects.md +++ /dev/null @@ -1,67 +0,0 @@ -The sing-box uses the following projects which also need to be maintained: - -#### sing - -Link: [GitHub repository](https://github.com/SagerNet/sing) - -As a base tool library, there are no dependencies other than `golang.org/x/sys`. - -#### sing-dns - -Link: [GitHub repository](https://github.com/SagerNet/sing-dns) - -Handles DNS lookups and caching. - -#### sing-tun - -Link: [GitHub repository](https://github.com/SagerNet/sing-tun) - -Handle Tun traffic forwarding, configure routing, monitor network and routing. - -This library needs to periodically update its dependency gVisor (according to tags), including checking for changes to -the used parts of the code and updating its usage. If you are involved in maintenance, you also need to check that if it -works or contains memory leaks. - -#### sing-shadowsocks - -Link: [GitHub repository](https://github.com/SagerNet/sing-shadowsocks) - -Provides Shadowsocks client and server - -#### sing-vmess - -Link: [GitHub repository](https://github.com/SagerNet/sing-vmess) - -Provides VMess client and server - -#### netlink - -Link: [GitHub repository](https://github.com/SagerNet/netlink) - -Fork of `vishvananda/netlink`, with some rule fixes. - -The library needs to be updated with the upstream. - -#### quic-go - -Link: [GitHub repository](https://github.com/SagerNet/quic-go) - -Fork of `lucas-clemente/quic-go` and `HyNetwork/quic-go`, contains quic flow control and other fixes used by Hysteria. - -Since the author of Hysteria does not follow the upstream updates in time, and the provided fork needs to use replace, -we need to do this. - -The library needs to be updated with the upstream. - -#### smux - -Link: [GitHub repository](https://github.com/SagerNet/smux) - -Fork of `xtaci/smux` - -Modify the code to support the writev it uses internally and unify the buffer pool, which prevents it from allocating -64k buffers for per connection and improves performance. - -Upstream doesn't seem to be updated anymore, maybe a replacement is needed. - -Note: while yamux is still actively maintained and better known, it seems to be less performant. diff --git a/docs/deprecated.md b/docs/deprecated.md index 521a399490..91a3b25e58 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -1,5 +1,11 @@ +--- +icon: material/delete-alert +--- + # Deprecated Feature List +### 1.6.0 + The following features will be marked deprecated in 1.5.0 and removed entirely in 1.6.0. #### ShadowsocksR diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md new file mode 100644 index 0000000000..f1125565f9 --- /dev/null +++ b/docs/deprecated.zh.md @@ -0,0 +1,17 @@ +--- +icon: material/delete-alert +--- + +# 废弃功能列表 + +### 1.6.0 + +下列功能已在 1.5.0 中标记为已弃用,并在 1.6.0 中完全删除。 + +#### ShadowsocksR + +ShadowsocksR 支持从未默认启用,自从常用的黑产代理销售面板停止使用该协议,继续维护它是没有意义的。 + +#### Proxy Protocol + +Proxy Protocol 支持由 Pull Request 添加,存在问题且仅由 HTTP 多路复用器(如 nginx)的后端使用,具有侵入性,对于代理目的毫无意义。 diff --git a/docs/examples/clash-api.md b/docs/examples/clash-api.md deleted file mode 100644 index ca5a01338f..0000000000 --- a/docs/examples/clash-api.md +++ /dev/null @@ -1,52 +0,0 @@ -```json -{ - "dns": { - "rules": [ - { - "domain": [ - "clash.razord.top", - "yacd.haishan.me" - ], - "server": "local" - }, - { - "clash_mode": "direct", - "server": "local" - } - ] - }, - "outbounds": [ - { - "type": "selector", - "tag": "default", - "outbounds": [ - "proxy-a", - "proxy-b" - ] - } - ], - "route": { - "rules": [ - { - "clash_mode": "direct", - "outbound": "direct" - }, - { - "domain": [ - "clash.razord.top", - "yacd.haishan.me" - ], - "outbound": "direct" - } - ], - "final": "default" - }, - "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "store_selected": true - } - } -} - -``` \ No newline at end of file diff --git a/docs/examples/dns-hijack.md b/docs/examples/dns-hijack.md deleted file mode 100644 index db9e86b808..0000000000 --- a/docs/examples/dns-hijack.md +++ /dev/null @@ -1,65 +0,0 @@ -#### Sniff Mode - -```json -{ - "inbounds": [ - { - "type": "tun", - "inet4_address": "172.19.0.1/30", - "auto_route": true, - "sniff": true // required - } - ], - "outbounds": [ - { - "type": "direct" - }, - { - "type": "dns", - "tag": "dns-out" - } - ], - "route": { - "rules": [ - { - "protocol": "dns", - "outbound": "dns-out" - } - ], - "auto_detect_interface": true - } -} -``` - -#### Port Mode - -```json -{ - "inbounds": [ - { - "type": "tun", - "inet4_address": "172.19.0.1/30", - "auto_route": true, - "sniff": true // not required - } - ], - "outbounds": [ - { - "type": "direct" - }, - { - "type": "dns", - "tag": "dns-out" - } - ], - "route": { - "rules": [ - { - "port": 53, - "outbound": "dns-out" - } - ], - "auto_detect_interface": true - } -} -``` \ No newline at end of file diff --git a/docs/examples/dns-hijack.zh.md b/docs/examples/dns-hijack.zh.md deleted file mode 100644 index 2e75b13a7b..0000000000 --- a/docs/examples/dns-hijack.zh.md +++ /dev/null @@ -1,65 +0,0 @@ -#### 探测模式 - -```json -{ - "inbounds": [ - { - "type": "tun", - "inet4_address": "172.19.0.1/30", - "auto_route": true, - "sniff": true // 必须 - } - ], - "outbounds": [ - { - "type": "direct" - }, - { - "type": "dns", - "tag": "dns-out" - } - ], - "route": { - "rules": [ - { - "protocol": "dns", - "outbound": "dns-out" - } - ], - "auto_detect_interface": true - } -} -``` - -#### 端口模式 - -```json -{ - "inbounds": [ - { - "type": "tun", - "inet4_address": "172.19.0.1/30", - "auto_route": true, - "sniff": true // 非必须 - } - ], - "outbounds": [ - { - "type": "direct" - }, - { - "type": "dns", - "tag": "dns-out" - } - ], - "route": { - "rules": [ - { - "port": 53, - "outbound": "dns-out" - } - ], - "auto_detect_interface": true - } -} -``` \ No newline at end of file diff --git a/docs/examples/fakeip.md b/docs/examples/fakeip.md deleted file mode 100644 index 21407eced4..0000000000 --- a/docs/examples/fakeip.md +++ /dev/null @@ -1,106 +0,0 @@ -```json -{ - "dns": { - "servers": [ - { - "tag": "google", - "address": "tls://8.8.8.8" - }, - { - "tag": "local", - "address": "223.5.5.5", - "detour": "direct" - }, - { - "tag": "remote", - "address": "fakeip" - }, - { - "tag": "block", - "address": "rcode://success" - } - ], - "rules": [ - { - "geosite": "category-ads-all", - "server": "block", - "disable_cache": true - }, - { - "outbound": "any", - "server": "local" - }, - { - "geosite": "cn", - "server": "local" - }, - { - "query_type": [ - "A", - "AAAA" - ], - "server": "remote" - } - ], - "fakeip": { - "enabled": true, - "inet4_range": "198.18.0.0/15", - "inet6_range": "fc00::/18" - }, - "independent_cache": true, - "strategy": "ipv4_only" - }, - "inbounds": [ - { - "type": "tun", - "inet4_address": "172.19.0.1/30", - "auto_route": true, - "sniff": true, - "domain_strategy": "ipv4_only" // remove this line if you want to resolve the domain remotely (if the server is not sing-box, UDP may not work due to wrong behavior). - } - ], - "outbounds": [ - { - "type": "shadowsocks", - "tag": "proxy", - "server": "mydomain.com", - "server_port": 8080, - "method": "2022-blake3-aes-128-gcm", - "password": "8JCsPssfgS8tiRwiMlhARg==" - }, - { - "type": "direct", - "tag": "direct" - }, - { - "type": "block", - "tag": "block" - }, - { - "type": "dns", - "tag": "dns-out" - } - ], - "route": { - "rules": [ - { - "protocol": "dns", - "outbound": "dns-out" - }, - { - "geosite": "cn", - "geoip": [ - "private", - "cn" - ], - "outbound": "direct" - }, - { - "geosite": "category-ads-all", - "outbound": "block" - } - ], - "auto_detect_interface": true - } -} -``` \ No newline at end of file diff --git a/docs/examples/fakeip.zh.md b/docs/examples/fakeip.zh.md deleted file mode 100644 index 947ce387ae..0000000000 --- a/docs/examples/fakeip.zh.md +++ /dev/null @@ -1,106 +0,0 @@ -```json -{ - "dns": { - "servers": [ - { - "tag": "google", - "address": "tls://8.8.8.8" - }, - { - "tag": "local", - "address": "223.5.5.5", - "detour": "direct" - }, - { - "tag": "remote", - "address": "fakeip" - }, - { - "tag": "block", - "address": "rcode://success" - } - ], - "rules": [ - { - "geosite": "category-ads-all", - "server": "block", - "disable_cache": true - }, - { - "outbound": "any", - "server": "local" - }, - { - "geosite": "cn", - "server": "local" - }, - { - "query_type": [ - "A", - "AAAA" - ], - "server": "remote" - } - ], - "fakeip": { - "enabled": true, - "inet4_range": "198.18.0.0/15", - "inet6_range": "fc00::/18" - }, - "independent_cache": true, - "strategy": "ipv4_only" - }, - "inbounds": [ - { - "type": "tun", - "inet4_address": "172.19.0.1/30", - "auto_route": true, - "sniff": true, - "domain_strategy": "ipv4_only" // 如果您想在远程解析域,删除此行 (如果服务器程序不为 sing-box,可能由于错误的行为导致 UDP 无法使用)。 - } - ], - "outbounds": [ - { - "type": "shadowsocks", - "tag": "proxy", - "server": "mydomain.com", - "server_port": 8080, - "method": "2022-blake3-aes-128-gcm", - "password": "8JCsPssfgS8tiRwiMlhARg==" - }, - { - "type": "direct", - "tag": "direct" - }, - { - "type": "block", - "tag": "block" - }, - { - "type": "dns", - "tag": "dns-out" - } - ], - "route": { - "rules": [ - { - "protocol": "dns", - "outbound": "dns-out" - }, - { - "geosite": "cn", - "geoip": [ - "private", - "cn" - ], - "outbound": "direct" - }, - { - "geosite": "category-ads-all", - "outbound": "block" - } - ], - "auto_detect_interface": true - } -} -``` \ No newline at end of file diff --git a/docs/examples/index.md b/docs/examples/index.md deleted file mode 100644 index 6bbab74145..0000000000 --- a/docs/examples/index.md +++ /dev/null @@ -1,11 +0,0 @@ -# Examples - -Configuration examples for sing-box. - -* [Linux Server Installation](./linux-server-installation) -* [Tun](./tun) -* [DNS Hijack](./dns-hijack.md) -* [Shadowsocks](./shadowsocks) -* [ShadowTLS](./shadowtls) -* [Clash API](./clash-api) -* [FakeIP](./fakeip) diff --git a/docs/examples/index.zh.md b/docs/examples/index.zh.md deleted file mode 100644 index 7528e5d438..0000000000 --- a/docs/examples/index.zh.md +++ /dev/null @@ -1,11 +0,0 @@ -# 示例 - -sing-box 的配置示例。 - -* [Linux 服务器安装](./linux-server-installation) -* [Tun](./tun) -* [DNS 劫持](./dns-hijack.md) -* [Shadowsocks](./shadowsocks) -* [ShadowTLS](./shadowtls) -* [Clash API](./clash-api) -* [FakeIP](./fakeip) diff --git a/docs/examples/linux-server-installation.md b/docs/examples/linux-server-installation.md deleted file mode 100644 index a785b130b7..0000000000 --- a/docs/examples/linux-server-installation.md +++ /dev/null @@ -1,38 +0,0 @@ -#### Requirements - -* Linux & Systemd -* Git -* C compiler environment - -#### Install - -```shell -git clone -b main https://github.com/SagerNet/sing-box -cd sing-box -./release/local/install_go.sh # skip if you have golang already installed -./release/local/install.sh -``` - -Edit configuration file in `/usr/local/etc/sing-box/config.json` - -```shell -./release/local/enable.sh -``` - -#### Update - -```shell -./release/local/update.sh -``` - -#### Other commands - -| Operation | Command | -|-----------|-----------------------------------------------| -| Start | `sudo systemctl start sing-box` | -| Stop | `sudo systemctl stop sing-box` | -| Kill | `sudo systemctl kill sing-box` | -| Restart | `sudo systemctl restart sing-box` | -| Logs | `sudo journalctl -u sing-box --output cat -e` | -| New Logs | `sudo journalctl -u sing-box --output cat -f` | -| Uninstall | `./release/local/uninstall.sh` | \ No newline at end of file diff --git a/docs/examples/linux-server-installation.zh.md b/docs/examples/linux-server-installation.zh.md deleted file mode 100644 index 91d908d67d..0000000000 --- a/docs/examples/linux-server-installation.zh.md +++ /dev/null @@ -1,38 +0,0 @@ -#### 依赖 - -* Linux & Systemd -* Git -* C 编译器环境 - -#### 安装 - -```shell -git clone -b main https://github.com/SagerNet/sing-box -cd sing-box -./release/local/install_go.sh # 如果已安装 golang 则跳过 -./release/local/install.sh -``` - -编辑配置文件 `/usr/local/etc/sing-box/config.json` - -```shell -./release/local/enable.sh -``` - -#### 更新 - -```shell -./release/local/update.sh -``` - -#### 其他命令 - -| 操作 | 命令 | -|------|-----------------------------------------------| -| 启动 | `sudo systemctl start sing-box` | -| 停止 | `sudo systemctl stop sing-box` | -| 强制停止 | `sudo systemctl kill sing-box` | -| 重启 | `sudo systemctl restart sing-box` | -| 查看日志 | `sudo journalctl -u sing-box --output cat -e` | -| 实时日志 | `sudo journalctl -u sing-box --output cat -f` | -| 卸载 | `./release/local/uninstall.sh` | \ No newline at end of file diff --git a/docs/examples/shadowsocks.md b/docs/examples/shadowsocks.md deleted file mode 100644 index 7e002fab4d..0000000000 --- a/docs/examples/shadowsocks.md +++ /dev/null @@ -1,163 +0,0 @@ -# Shadowsocks - -!!! warning "" - - For censorship bypass usage in China, we recommend using UDP over TCP and disabling UDP on the server. - -## Single User - -#### Server - -```json -{ - "inbounds": [ - { - "type": "shadowsocks", - "listen": "::", - "listen_port": 8080, - "network": "tcp", - "method": "2022-blake3-aes-128-gcm", - "password": "8JCsPssfgS8tiRwiMlhARg==" - } - ] -} -``` - -#### Client - -```json -{ - "inbounds": [ - { - "type": "mixed", - "listen": "::", - "listen_port": 2080 - } - ], - "outbounds": [ - { - "type": "shadowsocks", - "server": "127.0.0.1", - "server_port": 8080, - "method": "2022-blake3-aes-128-gcm", - "password": "8JCsPssfgS8tiRwiMlhARg==", - "udp_over_tcp": true - } - ] -} - -``` - -## Multiple Users - -#### Server - -```json -{ - "inbounds": [ - { - "type": "shadowsocks", - "listen": "::", - "listen_port": 8080, - "method": "2022-blake3-aes-128-gcm", - "password": "8JCsPssfgS8tiRwiMlhARg==", - "users": [ - { - "name": "sekai", - "password": "BXYxVUXJ9NgF7c7KPLQjkg==" - } - ] - } - ] -} -``` - -#### Client - -```json -{ - "inbounds": [ - { - "type": "mixed", - "listen": "::", - "listen_port": 2080 - } - ], - "outbounds": [ - { - "type": "shadowsocks", - "server": "127.0.0.1", - "server_port": 8080, - "method": "2022-blake3-aes-128-gcm", - "password": "8JCsPssfgS8tiRwiMlhARg==:BXYxVUXJ9NgF7c7KPLQjkg==" - } - ] -} - -``` - -## Relay - -#### Server - -```json -{ - "inbounds": [ - { - "type": "shadowsocks", - "listen": "::", - "listen_port": 8080, - "method": "2022-blake3-aes-128-gcm", - "password": "8JCsPssfgS8tiRwiMlhARg==" - } - ] -} -``` - -#### Relay - -```json -{ - "inbounds": [ - { - "type": "shadowsocks", - "listen": "::", - "listen_port": 8081, - "method": "2022-blake3-aes-128-gcm", - "password": "BXYxVUXJ9NgF7c7KPLQjkg==", - "destinations": [ - { - "name": "my_server", - "password": "8JCsPssfgS8tiRwiMlhARg==", - "server": "127.0.0.1", - "server_port": 8080 - } - ] - } - ] -} -``` - -#### Client - -```json -{ - "inbounds": [ - { - "type": "mixed", - "listen": "::", - "listen_port": 2080 - } - ], - "outbounds": [ - { - "type": "shadowsocks", - "server": "127.0.0.1", - "server_port": 8081, - "method": "2022-blake3-aes-128-gcm", - "password": "8JCsPssfgS8tiRwiMlhARg==:BXYxVUXJ9NgF7c7KPLQjkg==" - } - ] -} - -``` \ No newline at end of file diff --git a/docs/examples/shadowtls.md b/docs/examples/shadowtls.md deleted file mode 100644 index 352a305846..0000000000 --- a/docs/examples/shadowtls.md +++ /dev/null @@ -1,70 +0,0 @@ -#### Server - -```json -{ - "inbounds": [ - { - "type": "shadowtls", - "listen": "::", - "listen_port": 4443, - "version": 3, - "users": [ - { - "name": "sekai", - "password": "8JCsPssfgS8tiRwiMlhARg==" - } - ], - "handshake": { - "server": "google.com", - "server_port": 443 - }, - "detour": "shadowsocks-in" - }, - { - "type": "shadowsocks", - "tag": "shadowsocks-in", - "listen": "127.0.0.1", - "network": "tcp", - "method": "2022-blake3-aes-128-gcm", - "password": "8JCsPssfgS8tiRwiMlhARg==" - } - ] -} -``` - -#### Client - -```json -{ - "outbounds": [ - { - "type": "shadowsocks", - "method": "2022-blake3-aes-128-gcm", - "password": "8JCsPssfgS8tiRwiMlhARg==", - "detour": "shadowtls-out", - "multiplex": { - "enabled": true, - "max_connections": 4, - "min_streams": 4 - } - // or "udp_over_tcp": true - }, - { - "type": "shadowtls", - "tag": "shadowtls-out", - "server": "127.0.0.1", - "server_port": 4443, - "version": 3, - "password": "8JCsPssfgS8tiRwiMlhARg==", - "tls": { - "enabled": true, - "server_name": "google.com", - "utls": { - "enabled": true, - "fingerprint": "chrome" - } - } - } - ] -} -``` diff --git a/docs/examples/tun.md b/docs/examples/tun.md deleted file mode 100644 index 8671ef76a6..0000000000 --- a/docs/examples/tun.md +++ /dev/null @@ -1,89 +0,0 @@ -```json -{ - "dns": { - "servers": [ - { - "tag": "google", - "address": "tls://8.8.8.8" - }, - { - "tag": "local", - "address": "223.5.5.5", - "detour": "direct" - }, - { - "tag": "block", - "address": "rcode://success" - } - ], - "rules": [ - { - "geosite": "category-ads-all", - "server": "block", - "disable_cache": true - }, - { - "outbound": "any", - "server": "local" - }, - { - "geosite": "cn", - "server": "local" - } - ], - "strategy": "ipv4_only" - }, - "inbounds": [ - { - "type": "tun", - "inet4_address": "172.19.0.1/30", - "auto_route": true, - "strict_route": false, - "sniff": true - } - ], - "outbounds": [ - { - "type": "shadowsocks", - "tag": "proxy", - "server": "mydomain.com", - "server_port": 8080, - "method": "2022-blake3-aes-128-gcm", - "password": "8JCsPssfgS8tiRwiMlhARg==" - }, - { - "type": "direct", - "tag": "direct" - }, - { - "type": "block", - "tag": "block" - }, - { - "type": "dns", - "tag": "dns-out" - } - ], - "route": { - "rules": [ - { - "protocol": "dns", - "outbound": "dns-out" - }, - { - "geosite": "cn", - "geoip": [ - "private", - "cn" - ], - "outbound": "direct" - }, - { - "geosite": "category-ads-all", - "outbound": "block" - } - ], - "auto_detect_interface": true - } -} -``` \ No newline at end of file diff --git a/docs/faq/fakeip.md b/docs/faq/fakeip.md deleted file mode 100644 index 59d9e730d9..0000000000 --- a/docs/faq/fakeip.md +++ /dev/null @@ -1,19 +0,0 @@ -# FakeIP - -FakeIP refers to a type of behavior in a program that simultaneously hijacks both DNS and connection requests. It -responds to DNS requests with virtual results and restores mapping when accepting connections. - -#### Advantage - -* - -#### Limitation - -* Its mechanism breaks applications that depend on returning correct remote addresses. -* Only A and AAAA (IP) requests are supported, which may break applications that rely on other requests. - -#### Recommendation - -* Enable `dns.independent_cache` unless you always resolve FakeIP domains remotely. -* If using tun, make sure FakeIP ranges is included in the tun's routes. -* Enable `experimental.clash_api.store_fakeip` to persist FakeIP records, or use `dns.rules.rewrite_ttl` to avoid losing records after program restart in DNS cached environments. diff --git a/docs/faq/fakeip.zh.md b/docs/faq/fakeip.zh.md deleted file mode 100644 index 3ab77d2c6a..0000000000 --- a/docs/faq/fakeip.zh.md +++ /dev/null @@ -1,18 +0,0 @@ -# FakeIP - -FakeIP 是指同时劫持 DNS 和连接请求的程序中的一种行为。它通过虚拟结果响应 DNS 请求,在接受连接时恢复映射。 - -#### 优点 - -* - -#### 限制 - -* 它的机制会破坏依赖于返回正确远程地址的应用程序。 -* 仅支持 A 和 AAAA(IP)请求,这可能会破坏依赖于其他请求的应用程序。 - -#### 建议 - -* 启用 `dns.independent_cache` 除非您始终远程解析 FakeIP 域。 -* 如果使用 tun,请确保 tun 路由中包含 FakeIP 地址范围。 -* 启用 `experimental.clash_api.store_fakeip` 以持久化 FakeIP 记录,或者使用 `dns.rules.rewrite_ttl` 避免程序重启后在 DNS 被缓存的环境中丢失记录。 diff --git a/docs/faq/index.md b/docs/faq/index.md deleted file mode 100644 index c7ce30ad65..0000000000 --- a/docs/faq/index.md +++ /dev/null @@ -1,58 +0,0 @@ -# Frequently Asked Questions (FAQ) - -## Design - -#### Why does sing-box not have feature X? - -Every program contains novel features and omits someone's favorite feature. sing-box is designed with an eye to the -needs of performance, lightweight, usability, modularity, and code quality. Your favorite feature may be missing because -it doesn't fit, because it compromises performance or design clarity, or because it's a bad idea. - -If it bothers you that sing-box is missing feature X, please forgive us and investigate the features that sing-box does -have. You might find that they compensate in interesting ways for the lack of X. - -#### Naive outbound - -NaïveProxy's main function is chromium's network stack, and it makes no sense to implement only its transport protocol. - -#### Protocol combinations - -The "underlying transport" in v2ray-core is actually a combination of a number of proprietary protocols and uses the -names of their upstream protocols, resulting in a great deal of Linguistic corruption. - -For example, Trojan with v2ray's proprietary gRPC protocol, called Trojan gRPC by the v2ray community, is not actually a -protocol and has no role outside of abusing CDNs. - -## Tun - -#### What is tun? - -tun is a virtual network device in unix systems, and in windows there is wintun developed by WireGuard as an -alternative. The tun module of sing-box includes traffic processing, automatic routing, and network device listening, -and is mainly used as a transparent proxy. - -#### How is it different from system proxy? - -System proxy usually only supports TCP and is not accepted by all applications, but tun can handle all traffic. - -#### How is it different from traditional transparent proxy? - -They are usually only supported under Linux and require manipulation of firewalls like iptables, while tun only modifies -the routing table. - -The tproxy UDP is considered to have poor performance due to the need to create a new connection every write back in -v2ray and clash, but it is optimized in sing-box so you can still use it if needed. - -#### How does it handle DNS? - -In traditional transparent proxies, it is usually necessary to manually hijack port 53 to the DNS proxy server, while -tun is more flexible. - -sing-box's `auto_route` will hijack all DNS requests except on [macOS and Android](./known-issues#dns). - -You need to manually configure how to handle tun hijacked DNS traffic, see [Hijack DNS](/examples/dns-hijack). - -#### Why I can't use it with other local proxies (e.g. via socks)? - -Tun will hijack all traffic, including other proxy applications. In order to make tun work with other applications, you -need to create an inbound to proxy traffic from other applications or make them bypass the route. \ No newline at end of file diff --git a/docs/faq/index.zh.md b/docs/faq/index.zh.md deleted file mode 100644 index 557c53c7fa..0000000000 --- a/docs/faq/index.zh.md +++ /dev/null @@ -1,50 +0,0 @@ -# 常见问题 - -## 设计 - -#### 为什么 sing-box 没有功能 X? - -每个程序都包含新颖的功能并省略了某人最喜欢的功能。 sing-box 的设计着眼于高性能、轻量、可用性、模块化和代码质量的需求。 -您最喜欢的功能可能会丢失,因为它不适合,因为它会影响性能或设计清晰度,或者因为它是一个坏主意。 - -如果 sing-box 缺少功能 X 让您感到困扰,请原谅我们并调查 sing-box 确实有的功能。 您可能会发现它们以有趣的方式弥补了 X 的缺失。 - -#### Naive 出站 - -NaïveProxy 的主要功能是 chromium 的网络栈,仅实现它的传输协议是舍本逐末的。 - -#### 协议组合 - -v2ray-core 中的 "底层传输协议" 实际上是一些专有协议的组合,并使用其上游协议的名称,造成了大量的语言腐败。 - -例如,v2ray 社区将 v2ray 专有的 gRPC 协议称为 Trojan gRPC,其实并不是一个 协议,在滥用 CDN 之外没有任何作用。 - -## Tun - -#### 什么是 tun? - -tun 是 unix 系统中的虚拟网络设备,在 windows 中有 WireGuard 开发的 wintun 作为替代。 -sing-box 的 tun 模块包括流量处理、自动路由和网络设备监听,并主要用作透明代理。 - -#### 它与系统代理有什么不同? - -系统代理通常只支持 TCP,且不被所有应用程序接受,但 tun 可以处理所有流量。 - -#### 它与传统的透明代理有什么不同? - -它们通常仅支持 Linux,并且需要操作防火墙像 iptables,而 tun 仅修改路由表。 - -tproxy UDP 被认为性能很差,因为在 v2ray 和 clash 中每次回写都需要创建一个新连接,但 sing-box 进行了优化,因此您仍然可以在需要时使用它。 - -#### 它如何处理 DNS? - -在传统的透明代理中,通常需要手动劫持 53 端口到 DNS 代理服务器,而 tun 更灵活。 - -sing-box 的 `auto_route` 将劫持所有 DNS 请求,除了 [特殊情况](./known-issues#dns)。 - -您需要手动配置以处理 tun 劫持的 DNS 流量,请参阅 [DNS劫持](/zh/examples/dns-hijack)。 - -#### 为什么我不能将它与其他本地代理一起使用(例如通过 socks)? - -tun 将劫持所有流量,包括其他代理应用程序。 -为了使 tun 与其他应用程序一起工作,您需要创建入站以代理来自其他程序的流量或让它们绕过路由。 \ No newline at end of file diff --git a/docs/faq/known-issues.md b/docs/faq/known-issues.md deleted file mode 100644 index ac6892e1ee..0000000000 --- a/docs/faq/known-issues.md +++ /dev/null @@ -1,20 +0,0 @@ -#### DNS - -##### on macOS - -`auto-route` cannot automatically hijack DNS requests sent to the LAN, so it's need to manually set DNS to servers on -the public internet. - -##### on Android - -`auto-route` cannot automatically hijack DNS requests when Android's `Private DNS` enabled or `strict_route` disabled. - -#### System proxy - -##### on Linux - -Usually only browsers and GNOME applications accept GNOME proxy settings. - -##### on Android - -With the system proxy enabled, some applications will error out (usually from China). \ No newline at end of file diff --git a/docs/faq/known-issues.zh.md b/docs/faq/known-issues.zh.md deleted file mode 100644 index 1d25e723cb..0000000000 --- a/docs/faq/known-issues.zh.md +++ /dev/null @@ -1,19 +0,0 @@ -#### DNS - -##### macOS - -`auto-route` 无法自动劫持发往局域网的 DNS 请求,需要手动设置位于公网的 DNS 服务器。 - -##### Android - -`auto-route` 无法自动劫持 DNS 请求如果 `私人 DNS` 开启或 `strict_route` 禁用。 - -#### 系统代理 - -##### Linux - -通常只有浏览器和 GNOME 应用程序接受 GNOME 代理设置。 - -##### Android - -启用系统代理后,某些应用程序会出错(通常来自中国)。 diff --git a/docs/features.md b/docs/features.md deleted file mode 100644 index 84185bca2b..0000000000 --- a/docs/features.md +++ /dev/null @@ -1,121 +0,0 @@ -#### Server - -| Feature | v2ray-core | clash | -|------------------------------------------------------------|------------|-------| -| Direct inbound | ✔ | X | -| SOCKS4a inbound | ✔ | X | -| Mixed (http/socks) inbound | X | ✔ | -| Shadowsocks AEAD 2022 single-user/multi-user/relay inbound | X | X | -| VMess/Trojan inbound | ✔ | X | -| Naive/Hysteria inbound | X | X | -| Resolve incoming domain names using custom policy | X | X | -| Set system proxy on Windows/Linux/macOS/Android | X | X | -| TLS certificate auto reload | X | X | -| TLS ACME certificate issuer | X | X | - -#### Client - -| Feature | v2ray-core | clash | -|--------------------------------------------------------|------------------------------------|----------| -| Set upstream client (proxy chain) | TCP only, and has poor performance | TCP only | -| Bind to network interface | Linux only | ✔ | -| Custom dns strategy for resolving server address | X | X | -| Fast fallback (RFC 6555) support for connect to server | X | X | -| SOCKS4/4a outbound | added by me | X | -| Shadowsocks AEAD 2022 outbound | X | X | -| Shadowsocks UDP over TCP | X | X | -| Multiplex (smux/yamux) | mux.cool | X | -| Tor/WireGuard/Hysteria outbound | X | X | -| Selector outbound and Clash API | X | ✔ | - -#### Sniffing - -| Protocol | v2ray-core | clash-premium | -|------------------|-------------|---------------| -| HTTP Host | ✔ | X | -| QUIC ClientHello | added by me | added by me | -| STUN | X | X | - -| Feature | v2ray-core | clash-premium | -|-----------------------------------------|---------------------------|---------------| -| For routing only | added by me | X | -| No performance impact (like TCP splice) | no general splice support | X | -| Set separately for each server | ✔ | X | - -#### Routing - -| Feature | v2ray-core | clash-premium | -|----------------------------|------------|---------------| -| Auto detect interface | X | tun only | -| Set default interface name | X | tun only | -| Set default firewall mark | X | X | - -#### Routing Rule - -| Rule | v2ray-core | clash | -|----------------------|----------------------------|-------| -| Inbound | ✔ | X | -| IP Version | X | X | -| User from inbound | vmess and shadowsocks only | X | -| Sniffed protocol | ✔ | X | -| GeoSite | ✔ | X | -| Process name | X | ✔ | -| Android package name | X | X | -| Linux user/user id | X | X | -| Invert rule | X | X | -| Logical rule | X | X | - -#### DNS - -| Feature | v2ray-core | clash | -|------------------------------------|-------------|-------| -| DNS proxy | A/AAAA only | ✔ | -| DNS cache | A/AAAA only | X | -| DNS routing | X | X | -| DNS Over QUIC | ✔ | X | -| DNS Over HTTP3 | X | X | -| Fake dns response with custom code | X | X | - -#### Tun - -| Feature | clash-premium | -|-------------------------------------------|---------------| -| Full IPv6 support | X | -| Auto route on Linux/Windows/macOS/Android | ✔ | -| Embed windows driver | X | -| Custom address/mtu | X | -| Limit uid (Linux) in routing | X | -| Limit android user in routing | X | -| Limit android package in routing | X | - -#### Memory usage - -| GeoSite code | sing-box | v2ray-core | -|-------------------|----------|------------| -| cn | 17.8M | 140.3M | -| cn (Loyalsoldier) | 74.3M | 246.7M | - -#### Benchmark - -##### Shadowsocks - -| / | none | aes-128-gcm | 2022-blake3-aes-128-gcm | -|------------------------------------|:---------:|:-----------:|:-----------------------:| -| v2ray-core (5.0.7) | 13.0 Gbps | 5.02 Gbps | / | -| shadowsocks-rust (v1.15.0-alpha.5) | 10.7 Gbps | / | 9.36 Gbps | -| sing-box | 29.0 Gbps | / | 11.8 Gbps | - -##### VMess - -| / | TCP | HTTP | H2 TLS | WebSocket TLS | gRPC TLS | -|--------------------|:---------:|:---------:|:---------:|:-------------:|:---------:| -| v2ray-core (5.1.0) | 7.86 GBps | 2.86 Gbps | 1.83 Gbps | 2.36 Gbps | 2.43 Gbps | -| sing-box | 7.96 Gbps | 8.09 Gbps | 6.11 Gbps | 8.02 Gbps | 6.35 Gbps | - -#### License - -| / | License | -|------------|-----------------------------------| -| sing-box | GPLv3 or later (Full open-source) | -| v2ray-core | MIT (Full open-source) | -| clash | GPLv3 | \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 2c34509c24..2af7222e7b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,7 +2,7 @@ description: Welcome to the wiki page for the sing-box project. --- -# Home +# :material-home: Home Welcome to the wiki page for the sing-box project. diff --git a/docs/index.zh.md b/docs/index.zh.md index 2453bb73b4..72877118f3 100644 --- a/docs/index.zh.md +++ b/docs/index.zh.md @@ -2,7 +2,7 @@ description: 欢迎来到该 sing-box 项目的文档页。 --- -# 开始 +# :material-home: 开始 欢迎来到该 sing-box 项目的文档页。 diff --git a/docs/installation/build-from-source.md b/docs/installation/build-from-source.md new file mode 100644 index 0000000000..0ac7169a14 --- /dev/null +++ b/docs/installation/build-from-source.md @@ -0,0 +1,63 @@ +--- +icon: material/file-code +--- + +# Build from source + +## :material-graph: Requirements + +Before sing-box 1.4.0: + +* Go 1.18.5 - 1.20.x + +Since sing-box 1.4.0: + +* Go 1.18.5 - ~ +* Go 1.20.0 - ~ if `with_quic` tag enabled + +You can download and install Go from: https://go.dev/doc/install, latest version is recommended. + +## :material-fast-forward: Simple Build + +```bash +make +``` + +Or build and install binary to `GOBIN`: + +```bash +make install +``` + +## :material-cog: Custom Build + +```bash +TAGS="tag_a tag_b" make +``` + +or + +```bash +go build -tags "tag_a tag_b" ./cmd/sing-box +``` + +## :material-folder-settings: Build Tags + +| Build Tag | Enabled by default | Description | +|------------------------------------|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `with_quic` | ✔ | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server), [Naive inbound](/configuration/inbound/naive), [Hysteria Inbound](/configuration/inbound/hysteria), [Hysteria Outbound](/configuration/outbound/hysteria) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). | +| `with_grpc` | ✖️ | Build with standard gRPC support, see [V2Ray Transport#gRPC](/configuration/shared/v2ray-transport#grpc). | +| `with_dhcp` | ✔ | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server). | +| `with_wireguard` | ✔ | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard). | +| `with_ech` | ✔ | Build with TLS ECH extension support for TLS outbound, see [TLS](/configuration/shared/tls#ech). | +| `with_utls` | ✔ | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). | +| `with_reality_server` | ✔ | Build with reality TLS server support, see [TLS](/configuration/shared/tls). | +| `with_acme` | ✔ | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls). | +| `with_clash_api` | ✔ | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | +| `with_v2ray_api` | ✖️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | +| `with_gvisor` | ✔ | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). | +| `with_embedded_tor` (CGO required) | ✖️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor). | +| `with_lwip` (CGO required) | ✖️ | Build with LWIP Tun stack support, see [Tun inbound](/configuration/inbound/tun#stack). | + + +It is not recommended to change the default build tag list unless you really know what you are adding. diff --git a/docs/installation/clients/sfa.md b/docs/installation/clients/sfa.md deleted file mode 100644 index 54b4f3e553..0000000000 --- a/docs/installation/clients/sfa.md +++ /dev/null @@ -1,23 +0,0 @@ -# SFA - -Experimental Android client for sing-box. - -#### Requirements - -* Android 5.0+ - -#### Download - -* [Play Store](https://play.google.com/store/apps/details?id=io.nekohasekai.sfa) -* [Github Releases](https://github.com/SagerNet/sing-box/releases) - -#### Note - -* User Agent in remote profile request is `SFA/$version ($version_code; sing-box $sing_box_version)` -* The working directory is located at `/sdcard/Android/data/io.nekohasekai.sfa/files` (External files directory) -* Crash logs is located in `$working_directory/stderr.log` - -#### Privacy policy - -* SFA did not collect or share personal data. -* The data generated by the software is always on your device. diff --git a/docs/installation/clients/sfa.zh.md b/docs/installation/clients/sfa.zh.md deleted file mode 100644 index 7dfe63352e..0000000000 --- a/docs/installation/clients/sfa.zh.md +++ /dev/null @@ -1,23 +0,0 @@ -# SFA - -实验性的 Android sing-box 客户端。 - -#### 要求 - -* Android 5.0+ - -#### 下载 - -* [AppCenter](https://install.appcenter.ms/users/nekohasekai/apps/sfa/distribution_groups/publictest) -* [Github Releases](https://github.com/SagerNet/sing-box/releases) - -#### 注意事项 - -* 远程配置文件请求中的 User Agent 为 `SFA/$version ($version_code; sing-box $sing_box_version)` -* 工作目录位于 `/sdcard/Android/data/io.nekohasekai.sfa/files` (外部文件目录) -* 崩溃日志位于 `$working_directory/stderr.log` - -#### 隐私政策 - -* SFA 不收集或共享个人数据。 -* 软件生成的数据始终在您的设备上。 diff --git a/docs/installation/clients/sfi.md b/docs/installation/clients/sfi.md deleted file mode 100644 index 35c15c918b..0000000000 --- a/docs/installation/clients/sfi.md +++ /dev/null @@ -1,23 +0,0 @@ -# SFI - -Experimental iOS client for sing-box. - -#### Requirements - -* iOS 15.0+ -* An Apple account outside of mainland China - -#### Download - -* [AppStore](https://apps.apple.com/us/app/sing-box/id6451272673) -* [TestFlight](https://testflight.apple.com/join/AcqO44FH) - -#### Note - -* User Agent in remote profile request is `SFI/$version ($version_code; sing-box $sing_box_version)` -* Crash logs is located in `Settings` -> `View Service Log` - -#### Privacy policy - -* SFI did not collect or share personal data. -* The data generated by the software is always on your device. diff --git a/docs/installation/clients/sfi.zh.md b/docs/installation/clients/sfi.zh.md deleted file mode 100644 index 90c3845f4f..0000000000 --- a/docs/installation/clients/sfi.zh.md +++ /dev/null @@ -1,23 +0,0 @@ -# SFI - -实验性的 iOS sing-box 客户端。 - -#### 要求 - -* iOS 15.0+ -* 一个非中国大陆地区的 Apple 账号 - -#### 下载 - -* [AppStore](https://apps.apple.com/us/app/sing-box/id6451272673) -* [TestFlight](https://testflight.apple.com/join/AcqO44FH) - -#### 注意事项 - -* 远程配置文件请求中的 User Agent 为 `SFI/$version ($version_code; sing-box $sing_box_version)` -* 崩溃日志位于 `Settings` -> `View Service Log` - -#### 隐私政策 - -* SFI 不收集或共享个人数据。 -* 软件生成的数据始终在您的设备上。 diff --git a/docs/installation/clients/sfm.md b/docs/installation/clients/sfm.md deleted file mode 100644 index 8d2237c0a6..0000000000 --- a/docs/installation/clients/sfm.md +++ /dev/null @@ -1,29 +0,0 @@ -# SFM - -Experimental macOS client for sing-box. - -#### Requirements - -* macOS 13.0+ -* An Apple account outside of mainland China (App Store Version) - -#### Download (App Store Version) - -* [AppStore](https://apps.apple.com/us/app/sing-box/id6451272673) -* [TestFlight](https://testflight.apple.com/join/AcqO44FH) - -#### Download (Independent Version) - -* [GitHub Release](https://github.com/SagerNet/sing-box/releases/latest) -* Homebrew (Cask): `brew install sfm` -* Homebrew (Tap): `brew tap sagernet/sing-box && brew install sagernet/sing-box/sfm` - -#### Note - -* User Agent in remote profile request is `SFM/$version ($version_code; sing-box $sing_box_version)` -* Crash logs is located in `Settings` -> `View Service Log` - -#### Privacy policy - -* SFM did not collect or share personal data. -* The data generated by the software is always on your device. diff --git a/docs/installation/clients/sfm.zh.md b/docs/installation/clients/sfm.zh.md deleted file mode 100644 index a6e42b8186..0000000000 --- a/docs/installation/clients/sfm.zh.md +++ /dev/null @@ -1,29 +0,0 @@ -# SFM - -实验性的 macOS sing-box 客户端。 - -#### 要求 - -* macOS 13.0+ -* 一个非中国大陆地区的 Apple 账号 (商店版本) - -#### 下载 (商店版本) - -* [AppStore](https://apps.apple.com/us/app/sing-box/id6451272673) -* [TestFlight](https://testflight.apple.com/join/AcqO44FH) - -#### 下载 (独立版本) - -* [GitHub Release](https://github.com/SagerNet/sing-box/releases/latest) -* Homebrew (Cask): `brew install sfm` -* Homebrew (Tap): `brew tap sagernet/sing-box && brew install sagernet/sing-box/sfm` - -#### 注意事项 - -* 远程配置文件请求中的 User Agent 为 `SFM/$version ($version_code; sing-box $sing_box_version)` -* 崩溃日志位于 `Settings` -> `View Service Log` - -#### 隐私政策 - -* SFM 不收集或共享个人数据。 -* 软件生成的数据始终在您的设备上。 diff --git a/docs/installation/clients/sft.md b/docs/installation/clients/sft.md deleted file mode 100644 index b05bc07edf..0000000000 --- a/docs/installation/clients/sft.md +++ /dev/null @@ -1,30 +0,0 @@ -# SFT - -Experimental Apple tvOS client for sing-box. - -#### Requirements - -* tvOS 17.0+ - -#### Download - -* [AppStore](https://apps.apple.com/us/app/sing-box/id6451272673) -* [TestFlight](https://testflight.apple.com/join/AcqO44FH) - -#### Features - -Full functionality, except for: - -* Only remote configuration files can be created manually -* You need to update SFI to the latest version to import profiles from iPhone/iPad -* No iCloud profile support - -#### Note - -* User Agent in remote profile request is `SFT/$version ($version_code; sing-box $sing_box_version)` -* Crash logs is located in `Settings` -> `View Service Log` - -#### Privacy policy - -* SFT did not collect or share personal data. -* The data generated by the software is always on your device. diff --git a/docs/installation/clients/sft.zh.md b/docs/installation/clients/sft.zh.md deleted file mode 100644 index 239d76e2e1..0000000000 --- a/docs/installation/clients/sft.zh.md +++ /dev/null @@ -1,31 +0,0 @@ -# SFI - -实验性的 Apple tvOS sing-box 客户端。 - -#### 要求 - -* tvOS 17.0+ -* 一个非中国大陆地区的 Apple 账号 - -#### 下载 - -* [AppStore](https://apps.apple.com/us/app/sing-box/id6451272673) -* [TestFlight](https://testflight.apple.com/join/AcqO44FH) - -#### 特性 - -完整的功能,除了: - -* 只能手动创建远程配置文件 -* 您需要将 SFI 更新到最新版本才能从 iPhone/iPad 导入配置文件 -* 没有 iCloud 配置文件支持 - -#### 注意事项 - -* 远程配置文件请求中的 User Agent 为 `SFT/$version ($version_code; sing-box $sing_box_version)` -* 崩溃日志位于 `Settings` -> `View Service Log` - -#### 隐私政策 - -* SFT 不收集或共享个人数据。 -* 软件生成的数据始终在您的设备上。 diff --git a/docs/installation/clients/specification.md b/docs/installation/clients/specification.md deleted file mode 100644 index ec36dd78cf..0000000000 --- a/docs/installation/clients/specification.md +++ /dev/null @@ -1,25 +0,0 @@ -# Specification - -## Profile - -Profile defines a sing-box configuration with metadata in a GUI client. - -## Profile Types - -### Local - -Create a empty configuration or import from a local file. - -### iCloud (on Apple platforms) - -Create a new configuration or use an existing configuration on iCloud. - -### Remote - -Use a remote URL as the configuration source, with HTTP basic authentication and automatic update support. - -#### URL specification - -``` -sing-box://import-remote-profile?url=urlEncodedURL#urlEncodedName -``` \ No newline at end of file diff --git a/docs/installation/docker.md b/docs/installation/docker.md new file mode 100644 index 0000000000..ae1682dd70 --- /dev/null +++ b/docs/installation/docker.md @@ -0,0 +1,31 @@ +--- +icon: material/docker +--- + +# Docker + +## :material-console: Command + +```bash +docker run -d \ + -v /etc/sing-box:/etc/sing-box/ \ + --name=sing-box \ + --restart=always \ + ghcr.io/sagernet/sing-box \ + -D /var/lib/sing-box \ + -C /etc/sing-box/ run +``` + +## :material-box-shadow: Compose + +```yaml +version: "3.8" +services: + sing-box: + image: ghcr.io/sagernet/sing-box + container_name: sing-box + restart: always + volumes: + - /etc/sing-box:/etc/sing-box/ + command: -D /var/lib/sing-box -C /etc/sing-box/ run +``` diff --git a/docs/installation/from-source.md b/docs/installation/from-source.md deleted file mode 100644 index da0ffc02d8..0000000000 --- a/docs/installation/from-source.md +++ /dev/null @@ -1,50 +0,0 @@ -# Install from source - -## Requirements - -Before sing-box 1.4.0: - -* Go 1.18.5 - 1.20.x - -Since sing-box 1.4.0: - -* Go 1.18.5 - ~ -* Go 1.20.0 - ~ if `with_quic` tag enabled - -## Installation - -```bash -go install -v github.com/sagernet/sing-box/cmd/sing-box@latest -``` - -Install with options: - -```bash -go install -v -tags with_quic,with_wireguard github.com/sagernet/sing-box/cmd/sing-box@latest -``` - -| Build Tag | Description | -|------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `with_quic` | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server), [Naive inbound](/configuration/inbound/naive), [Hysteria Inbound](/configuration/inbound/hysteria), [Hysteria Outbound](/configuration/outbound/hysteria) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). | -| `with_grpc` | Build with standard gRPC support, see [V2Ray Transport#gRPC](/configuration/shared/v2ray-transport#grpc). | -| `with_dhcp` | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server). | -| `with_wireguard` | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard). | -| `with_shadowsocksr` | Build with ShadowsocksR support, see [ShadowsocksR outbound](/configuration/outbound/shadowsocksr). | -| `with_ech` | Build with TLS ECH extension support for TLS outbound, see [TLS](/configuration/shared/tls#ech). | -| `with_utls` | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). | -| `with_reality_server` | Build with reality TLS server support, see [TLS](/configuration/shared/tls). | -| `with_acme` | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls). | -| `with_clash_api` | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | -| `with_v2ray_api` | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | -| `with_gvisor` | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). | -| `with_embedded_tor` (CGO required) | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor). | -| `with_lwip` (CGO required) | Build with LWIP Tun stack support, see [Tun inbound](/configuration/inbound/tun#stack). | - -The binary is built under $GOPATH/bin - -```bash -sing-box version -``` - -It is also recommended to use systemd to manage sing-box service, -see [Linux server installation example](/examples/linux-server-installation). \ No newline at end of file diff --git a/docs/installation/from-source.zh.md b/docs/installation/from-source.zh.md deleted file mode 100644 index fbcb9ba827..0000000000 --- a/docs/installation/from-source.zh.md +++ /dev/null @@ -1,39 +0,0 @@ -# 从源代码安装 - -sing-box 需要 Golang **1.18.5** 或更高版本。 - -```bash -go install -v github.com/sagernet/sing-box/cmd/sing-box@latest -``` - -自定义安装: - -```bash -go install -v -tags with_clash_api github.com/sagernet/sing-box/cmd/sing-box@latest -``` - -| 构建标志 | 描述 | -|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `with_quic` | 启用 QUIC 支持,参阅 [QUIC 和 HTTP3 DNS 传输层](/configuration/dns/server),[Naive 入站](/configuration/inbound/naive),[Hysteria 入站](/configuration/inbound/hysteria),[Hysteria 出站](/configuration/outbound/hysteria) 和 [V2Ray 传输层#QUIC](/configuration/shared/v2ray-transport#quic)。 | -| `with_grpc` | 启用标准 gRPC 支持,参阅 [V2Ray 传输层#gRPC](/configuration/shared/v2ray-transport#grpc)。 | -| `with_dhcp` | 启用 DHCP 支持,参阅 [DHCP DNS 传输层](/configuration/dns/server)。 | -| `with_wireguard` | 启用 WireGuard 支持,参阅 [WireGuard 出站](/configuration/outbound/wireguard)。 | -| `with_shadowsocksr` | 启用 ShadowsocksR 支持,参阅 [ShadowsocksR 出站](/configuration/outbound/shadowsocksr)。 | -| `with_ech` | 启用 TLS ECH 扩展支持,参阅 [TLS](/configuration/shared/tls#ech)。 | -| `with_utls` | 启用 [uTLS](https://github.com/refraction-networking/utls) 支持,参阅 [TLS](/configuration/shared/tls#utls)。 | -| `with_reality_server` | 启用 reality TLS 服务器支持,参阅 [TLS](/configuration/shared/tls)。 | -| `with_acme` | 启用 ACME TLS 证书签发支持,参阅 [TLS](/configuration/shared/tls)。 | -| `with_clash_api` | 启用 Clash API 支持,参阅 [实验性](/configuration/experimental#clash-api-fields)。 | -| `with_v2ray_api` | 启用 V2Ray API 支持,参阅 [实验性](/configuration/experimental#v2ray-api-fields)。 | -| `with_gvisor` | 启用 gVisor 支持,参阅 [Tun 入站](/configuration/inbound/tun#stack) 和 [WireGuard 出站](/configuration/outbound/wireguard#system_interface)。 | -| `with_embedded_tor` (需要 CGO) | 启用 嵌入式 Tor 支持,参阅 [Tor 出站](/configuration/outbound/tor)。 | -| `with_lwip` (需要 CGO) | 启用 LWIP Tun 栈支持,参阅 [Tun 入站](/configuration/inbound/tun#stack)。 | - -二进制文件将被构建在 `$GOPATH/bin` 下。 - -```bash -sing-box version -``` - -同时推荐使用 systemd 来管理 sing-box 服务器实例。 -参阅 [Linux 服务器安装示例](/examples/linux-server-installation)。 \ No newline at end of file diff --git a/docs/installation/package-manager.md b/docs/installation/package-manager.md new file mode 100644 index 0000000000..6ce1669d02 --- /dev/null +++ b/docs/installation/package-manager.md @@ -0,0 +1,90 @@ +--- +icon: material/package +--- + +# Package Manager + +## :material-download-box: Manual Installation + +=== ":material-debian: Debian / DEB" + + ```bash + bash <(curl -fsSL https://sing-box.app/deb-install.sh) + ``` + +=== ":material-redhat: Redhat / RPM" + + ```bash + bash <(curl -fsSL https://sing-box.app/rpm-install.sh) + ``` + +=== ":simple-archlinux: Archlinux / PKG" + + ```bash + bash <(curl -fsSL https://sing-box.app/arch-install.sh) + ``` + +## :material-book-lock-open: Managed Installation + +=== ":material-linux: Linux" + + | Type | Platform | Link | Command | Actively maintained | + |----------|--------------------|---------------------|------------------------------|---------------------| + | AUR | (Linux) Arch Linux | [sing-box][aur] ᴬᵁᴿ | `? -S sing-box` | :material-check: | + | nixpkgs | (Linux) NixOS | [sing-box][nixpkgs] | `nix-env -iA nixos.sing-box` | :material-check: | + | Homebrew | macOS / Linux | [sing-box][brew] | `brew install sing-box` | :material-check: | + | Alpine | (Linux) Alpine | [sing-box][alpine] | `apk add sing-box` | :material-alert: | + +=== ":material-apple: macOS" + + | Type | Platform | Link | Command | Actively maintained | + |----------|---------------|------------------|-------------------------|---------------------| + | Homebrew | macOS / Linux | [sing-box][brew] | `brew install sing-box` | :material-check: | + +=== ":material-microsoft-windows: Windows" + + | Type | Platform | Link | Command | Actively maintained | + |------------|--------------------|---------------------|------------------------------|---------------------| + | Scoop | Windows | [sing-box][scoop] | `scoop install sing-box` | :material-check: | + | Chocolatey | Windows | [sing-box][choco] | `choco install sing-box` | :material-check: | + | winget | Windows | [sing-box][winget] | `winget install sing-box` | :material-alert: | + +=== ":material-android: Android" + + | Type | Platform | Link | Command | Actively maintained | + |------------|--------------------|---------------------|------------------------------|---------------------| + | Termux | Android | [sing-box][termux] | `pkg add sing-box` | :material-check: | + +## :material-book-multiple: Service Management + +For Linux systems with [systemd][systemd], usually the installation already includes a sing-box service, +you can manage the service using the following command: + +| Operation | Command | +|-----------|-----------------------------------------------| +| Enable | `sudo systemctl enable sing-box` | +| Disable | `sudo systemctl disable sing-box` | +| Start | `sudo systemctl start sing-box` | +| Stop | `sudo systemctl stop sing-box` | +| Kill | `sudo systemctl kill sing-box` | +| Restart | `sudo systemctl restart sing-box` | +| Logs | `sudo journalctl -u sing-box --output cat -e` | +| New Logs | `sudo journalctl -u sing-box --output cat -f` | + +[alpine]: https://pkgs.alpinelinux.org/packages?name=sing-box + +[aur]: https://aur.archlinux.org/packages/sing-box + +[nixpkgs]: https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/tools/networking/sing-box/default.nix + +[termux]: https://github.com/termux/termux-packages/tree/master/packages/sing-box + +[brew]: https://formulae.brew.sh/formula/sing-box + +[choco]: https://chocolatey.org/packages/sing-box + +[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/sing-box.json + +[winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/SagerNet/sing-box + +[systemd]: https://systemd.io/ \ No newline at end of file diff --git a/docs/installation/package-manager/android.md b/docs/installation/package-manager/android.md deleted file mode 100644 index 1177b86b3a..0000000000 --- a/docs/installation/package-manager/android.md +++ /dev/null @@ -1,7 +0,0 @@ -# Android - -## Termux - -```shell -pkg add sing-box -``` diff --git a/docs/installation/package-manager/macOS.md b/docs/installation/package-manager/macOS.md deleted file mode 100644 index f0192b5a6c..0000000000 --- a/docs/installation/package-manager/macOS.md +++ /dev/null @@ -1,14 +0,0 @@ -# macOS - -## Homebrew (core) - -```shell -brew install sing-box -``` - -## Homebrew (Tap) - -```shell -brew tap sagernet/sing-box -brew install sagernet/sing-box/sing-box -``` \ No newline at end of file diff --git a/docs/installation/package-manager/windows.md b/docs/installation/package-manager/windows.md deleted file mode 100644 index 46e078b93f..0000000000 --- a/docs/installation/package-manager/windows.md +++ /dev/null @@ -1,13 +0,0 @@ -# Windows - -## Chocolatey - -```shell -choco install sing-box -``` - -## winget - -```shell -winget install sing-box -``` \ No newline at end of file diff --git a/docs/installation/scripts/arch-install.sh b/docs/installation/scripts/arch-install.sh new file mode 100644 index 0000000000..50ce57679d --- /dev/null +++ b/docs/installation/scripts/arch-install.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e -o pipefail + +ARCH_RAW=$(uname -m) +case "${ARCH_RAW}" in + 'x86_64') ARCH='amd64';; + 'x86' | 'i686' | 'i386') ARCH='386';; + 'aarch64' | 'arm64') ARCH='arm64';; + 'armv7l') ARCH='armv7';; + 's390x') ARCH='s390x';; + *) echo "Unsupported architecture: ${ARCH_RAW}"; exit 1;; +esac + +VERSION=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest \ + | grep tag_name \ + | cut -d ":" -f2 \ + | sed 's/\"//g;s/\,//g;s/\ //g;s/v//') + +curl -Lo sing-box.pkg.tar.zst "https://github.com/SagerNet/sing-box/releases/download/v${VERSION}/sing-box_${VERSION}_linux_${ARCH}.pkg.tar.zst" +sudo pacman -U sing-box.pkg.tar.zst +rm sing-box.pkg.tar.zst diff --git a/docs/installation/scripts/deb-install.sh b/docs/installation/scripts/deb-install.sh new file mode 100644 index 0000000000..587b6f61ba --- /dev/null +++ b/docs/installation/scripts/deb-install.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e -o pipefail + +ARCH_RAW=$(uname -m) +case "${ARCH_RAW}" in + 'x86_64') ARCH='amd64';; + 'x86' | 'i686' | 'i386') ARCH='386';; + 'aarch64' | 'arm64') ARCH='arm64';; + 'armv7l') ARCH='armv7';; + 's390x') ARCH='s390x';; + *) echo "Unsupported architecture: ${ARCH_RAW}"; exit 1;; +esac + +VERSION=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest \ + | grep tag_name \ + | cut -d ":" -f2 \ + | sed 's/\"//g;s/\,//g;s/\ //g;s/v//') + +curl -Lo sing-box.deb "https://github.com/SagerNet/sing-box/releases/download/v${VERSION}/sing-box_${VERSION}_linux_${ARCH}.deb" +sudo dpkg -i sing-box.deb +rm sing-box.deb + diff --git a/docs/installation/scripts/rpm-install.sh b/docs/installation/scripts/rpm-install.sh new file mode 100644 index 0000000000..fc3d731400 --- /dev/null +++ b/docs/installation/scripts/rpm-install.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e -o pipefail + +ARCH_RAW=$(uname -m) +case "${ARCH_RAW}" in + 'x86_64') ARCH='amd64';; + 'x86' | 'i686' | 'i386') ARCH='386';; + 'aarch64' | 'arm64') ARCH='arm64';; + 'armv7l') ARCH='armv7';; + 's390x') ARCH='s390x';; + *) echo "Unsupported architecture: ${ARCH_RAW}"; exit 1;; +esac + +VERSION=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest \ + | grep tag_name \ + | cut -d ":" -f2 \ + | sed 's/\"//g;s/\,//g;s/\ //g;s/v//') + +curl -Lo sing-box.rpm "https://github.com/SagerNet/sing-box/releases/download/v${VERSION}/sing-box_${VERSION}_linux_${ARCH}.rpm" +sudo rpm -i sing-box.rpm +rm sing-box.rpm diff --git a/docs/manual/proxy-protocol/hysteria2.md b/docs/manual/proxy-protocol/hysteria2.md new file mode 100644 index 0000000000..9cc07e6f12 --- /dev/null +++ b/docs/manual/proxy-protocol/hysteria2.md @@ -0,0 +1,211 @@ +--- +icon: material/lightning-bolt +--- + +# Hysteria 2 + +The most popular Chinese-made simple protocol based on QUIC, the selling point is Brutal, +a congestion control algorithm that can resist packet loss by manually specifying the required rate by the user. + +!!! warning + + Even though GFW rarely blocks UDP-based proxies, such protocols actually have far more characteristics than TCP based proxies. + +| Specification | Binary Characteristics | Active Detect Hiddenness | +|---------------------------------------------------------------------------|------------------------|--------------------------| +| [hysteria.network](https://v2.hysteria.network/docs/developers/Protocol/) | :material-alert: | :material-check: | + +## :material-text-box-check: Password Generator + +| Generate Password | Action | +|----------------------------|-----------------------------------------------------------------| +| | | + + + +## :material-alert: Difference from official Hysteria + +The official program supports an authentication method called **userpass**, +which essentially uses a combination of `:` as the actual password, +while sing-box does not provide this alias. +To use sing-box with the official program, you need to fill in that combination as the actual password. + +## :material-server: Server Example + +!!! info "" + + Replace `up_mbps` and `down_mbps` values with the actual bandwidth of your server. + +=== ":material-harddisk: With local certificate" + + ```json + { + "inbounds": [ + { + "type": "hysteria2", + "listen": "::", + "listen_port": 8080, + "up_mbps": 100, + "down_mbps": 100, + "users": [ + { + "name": "sekai", + "password": "" + } + ], + "tls": { + "enabled": true, + "server_name": "example.org", + "key_path": "/path/to/key.pem", + "certificate_path": "/path/to/certificate.pem" + } + } + ] + } + ``` + +=== ":material-auto-fix: With ACME" + + ```json + { + "inbounds": [ + { + "type": "hysteria2", + "listen": "::", + "listen_port": 8080, + "up_mbps": 100, + "down_mbps": 100, + "users": [ + { + "name": "sekai", + "password": "" + } + ], + "tls": { + "enabled": true, + "server_name": "example.org", + "acme": { + "domain": "example.org", + "email": "admin@example.org" + } + } + } + ] + } + ``` + +=== ":material-cloud: With ACME and Cloudflare API" + + ```json + { + "inbounds": [ + { + "type": "hysteria2", + "listen": "::", + "listen_port": 8080, + "up_mbps": 100, + "down_mbps": 100, + "users": [ + { + "name": "sekai", + "password": "" + } + ], + "tls": { + "enabled": true, + "server_name": "example.org", + "acme": { + "domain": "example.org", + "email": "admin@example.org", + "dns01_challenge": { + "provider": "cloudflare", + "api_token": "my_token" + } + } + } + } + ] + } + ``` + +## :material-cellphone-link: Client Example + +!!! info "" + + Replace `up_mbps` and `down_mbps` values with the actual bandwidth of your client. + +=== ":material-web-check: With valid certificate" + + ```json + { + "outbounds": [ + { + "type": "hysteria2", + "server": "127.0.0.1", + "server_port": 8080, + "up_mbps": 100, + "down_mbps": 100, + "password": "", + "tls": { + "enabled": true, + "server_name": "example.org" + } + } + ] + } + ``` + +=== ":material-check: With self-sign certificate" + + !!! info "Tip" + + Use `sing-box merge` command to merge configuration and certificate into one file. + + ```json + { + "outbounds": [ + { + "type": "hysteria2", + "server": "127.0.0.1", + "server_port": 8080, + "up_mbps": 100, + "down_mbps": 100, + "password": "", + "tls": { + "enabled": true, + "server_name": "example.org", + "certificate_path": "/path/to/certificate.pem" + } + } + ] + } + ``` + +=== ":material-alert: Ignore certificate verification" + + ```json + { + "outbounds": [ + { + "type": "hysteria2", + "server": "127.0.0.1", + "server_port": 8080, + "up_mbps": 100, + "down_mbps": 100, + "password": "", + "tls": { + "enabled": true, + "server_name": "example.org", + "insecure": true + } + } + ] + } + ``` diff --git a/docs/manual/proxy-protocol/shadowsocks.md b/docs/manual/proxy-protocol/shadowsocks.md new file mode 100644 index 0000000000..be0bc20800 --- /dev/null +++ b/docs/manual/proxy-protocol/shadowsocks.md @@ -0,0 +1,126 @@ +--- +icon: material/send +--- + +# Shadowsocks + +As the most well-known Chinese-made proxy protocol, +Shadowsocks exists in multiple versions, +but only AEAD 2022 ciphers TCP with multiplexing is recommended. + +| Ciphers | Specification | Cryptographic Security | Binary Characteristics | Active Detect Hiddenness | +|----------------|------------------------------------------------------------|------------------------|------------------------|--------------------------| +| Stream Ciphers | [shadowsocks.org](https://shadowsocks.org/doc/stream.html) | :material-alert: | :material-alert: | :material-alert: | +| AEAD | [shadowsocks.org](https://shadowsocks.org/doc/aead.html) | :material-check: | :material-alert: | :material-alert: | +| AEAD 2022 | [shadowsocks.org](https://shadowsocks.org/doc/sip022.html) | :material-check: | :material-check: | :material-help: | + +## :material-text-box-check: Password Generator + +| For `2022-blake3-aes-128-gcm` cipher | For other ciphers | Action | +|--------------------------------------|-------------------------------|-----------------------------------------------------------------| +| | | | + + + +## :material-server: Server Example + +!!! info "" + + Password of cipher `2022-blake3-aes-128-gcm` can be generated by command `sing-box generate rand 16 --base64` + +=== ":material-account: Single-user" + + ```json + { + "inbounds": [ + { + "type": "shadowsocks", + "listen": "::", + "listen_port": 8080, + "network": "tcp", + "method": "2022-blake3-aes-128-gcm", + "password": "", + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +=== ":material-account-multiple: Multi-user" + + ```json + { + "inbounds": [ + { + "type": "shadowsocks", + "listen": "::", + "listen_port": 8080, + "network": "tcp", + "method": "2022-blake3-aes-128-gcm", + "password": "", + "users": [ + { + "name": "sekai", + "password": "" + } + ], + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +## :material-cellphone-link: Client Example + +=== ":material-account: Single-user" + + ```json + { + "outbounds": [ + { + "type": "shadowsocks", + "server": "127.0.0.1", + "server_port": 8080, + "method": "2022-blake3-aes-128-gcm", + "password": "", + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +=== ":material-account-multiple: Multi-user" + + ```json + { + "outbounds": [ + { + "type": "shadowsocks", + "server": "127.0.0.1", + "server_port": 8080, + "method": "2022-blake3-aes-128-gcm", + "password": ":", + "multiplex": { + "enabled": true + } + } + ] + } + ``` diff --git a/docs/manual/proxy-protocol/trojan.md b/docs/manual/proxy-protocol/trojan.md new file mode 100644 index 0000000000..4c2e16f17e --- /dev/null +++ b/docs/manual/proxy-protocol/trojan.md @@ -0,0 +1,214 @@ +--- +icon: material/horse +--- + +# Trojan + +As the most commonly used TLS proxy made in China, Trojan can be used in various combinations, +but only the combination of uTLS and multiplexing is recommended. + +| Protocol and implementation combination | Specification | Binary Characteristics | Active Detect Hiddenness | +|-----------------------------------------|----------------------------------------------------------------------|------------------------|--------------------------| +| Origin / trojan-gfw | [trojan-gfw.github.io](https://trojan-gfw.github.io/trojan/protocol) | :material-check: | :material-check: | +| Basic Go implementation | / | :material-alert: | :material-check: | +| with privates transport by V2Ray | No formal definition | :material-alert: | :material-alert: | +| with uTLS enabled | No formal definition | :material-help: | :material-check: | + +## :material-text-box-check: Password Generator + +| Generate Password | Action | +|----------------------------|-----------------------------------------------------------------| +| | | + + + +## :material-server: Server Example + +=== ":material-harddisk: With local certificate" + + ```json + { + "inbounds": [ + { + "type": "trojan", + "listen": "::", + "listen_port": 8080, + "users": [ + { + "name": "example", + "password": "password" + } + ], + "tls": { + "enabled": true, + "server_name": "example.org", + "key_path": "/path/to/key.pem", + "certificate_path": "/path/to/certificate.pem" + }, + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +=== ":material-auto-fix: With ACME" + + ```json + { + "inbounds": [ + { + "type": "trojan", + "listen": "::", + "listen_port": 8080, + "users": [ + { + "name": "example", + "password": "password" + } + ], + "tls": { + "enabled": true, + "server_name": "example.org", + "acme": { + "domain": "example.org", + "email": "admin@example.org" + } + }, + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +=== ":material-cloud: With ACME and Cloudflare API" + + ```json + { + "inbounds": [ + { + "type": "trojan", + "listen": "::", + "listen_port": 8080, + "users": [ + { + "name": "example", + "password": "password" + } + ], + "tls": { + "enabled": true, + "server_name": "example.org", + "acme": { + "domain": "example.org", + "email": "admin@example.org", + "dns01_challenge": { + "provider": "cloudflare", + "api_token": "my_token" + } + } + }, + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +## :material-cellphone-link: Client Example + +=== ":material-web-check: With valid certificate" + + ```json + { + "outbounds": [ + { + "type": "trojan", + "server": "127.0.0.1", + "server_port": 8080, + "password": "password", + "tls": { + "enabled": true, + "server_name": "example.org", + "utls": { + "enabled": true, + "fingerprint": "firefox" + } + }, + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +=== ":material-check: With self-sign certificate" + + !!! info "Tip" + + Use `sing-box merge` command to merge configuration and certificate into one file. + + ```json + { + "outbounds": [ + { + "type": "trojan", + "server": "127.0.0.1", + "server_port": 8080, + "password": "password", + "tls": { + "enabled": true, + "server_name": "example.org", + "certificate_path": "/path/to/certificate.pem", + "utls": { + "enabled": true, + "fingerprint": "firefox" + } + }, + "multiplex": { + "enabled": true + } + } + ] + } + ``` + +=== ":material-alert: Ignore certificate verification" + + ```json + { + "outbounds": [ + { + "type": "trojan", + "server": "127.0.0.1", + "server_port": 8080, + "password": "password", + "tls": { + "enabled": true, + "server_name": "example.org", + "insecure": true, + "utls": { + "enabled": true, + "fingerprint": "firefox" + } + }, + "multiplex": { + "enabled": true + } + } + ] + } + ``` + diff --git a/docs/manual/proxy-protocol/tuic.md b/docs/manual/proxy-protocol/tuic.md new file mode 100644 index 0000000000..a2e01d882b --- /dev/null +++ b/docs/manual/proxy-protocol/tuic.md @@ -0,0 +1,208 @@ +--- +icon: material/alpha-t-box +--- + +# TUIC + +A recently popular Chinese-made simple protocol based on QUIC, the selling point is the BBR congestion control algorithm. + +!!! warning + + Even though GFW rarely blocks UDP-based proxies, such protocols actually have far more characteristics than TCP based proxies. + +| Specification | Binary Characteristics | Active Detect Hiddenness | +|-----------------------------------------------------------|------------------------|--------------------------| +| [GitHub](https://github.com/EAimTY/tuic/blob/dev/SPEC.md) | :material-alert: | :material-check: | + +## Password Generator + +| Generated UUID | Generated Password | Action | +|------------------------|----------------------------|-----------------------------------------------------------------| +| | | | + + + +## :material-server: Server Example + +=== ":material-harddisk: With local certificate" + + ```json + { + "inbounds": [ + { + "type": "tuic", + "listen": "::", + "listen_port": 8080, + "users": [ + { + "name": "sekai", + "uuid": "", + "password": "" + } + ], + "congestion_control": "bbr", + "tls": { + "enabled": true, + "server_name": "example.org", + "key_path": "/path/to/key.pem", + "certificate_path": "/path/to/certificate.pem" + } + } + ] + } + ``` + +=== ":material-auto-fix: With ACME" + + ```json + { + "inbounds": [ + { + "type": "tuic", + "listen": "::", + "listen_port": 8080, + "users": [ + { + "name": "sekai", + "uuid": "", + "password": "" + } + ], + "congestion_control": "bbr", + "tls": { + "enabled": true, + "server_name": "example.org", + "acme": { + "domain": "example.org", + "email": "admin@example.org" + } + } + } + ] + } + ``` + +=== ":material-cloud: With ACME and Cloudflare API" + + ```json + { + "inbounds": [ + { + "type": "tuic", + "listen": "::", + "listen_port": 8080, + "users": [ + { + "name": "sekai", + "uuid": "", + "password": "" + } + ], + "congestion_control": "bbr", + "tls": { + "enabled": true, + "server_name": "example.org", + "acme": { + "domain": "example.org", + "email": "admin@example.org", + "dns01_challenge": { + "provider": "cloudflare", + "api_token": "my_token" + } + } + } + } + ] + } + ``` + +## :material-cellphone-link: Client Example + +=== ":material-web-check: With valid certificate" + + ```json + { + "outbounds": [ + { + "type": "tuic", + "server": "127.0.0.1", + "server_port": 8080, + "uuid": "", + "password": "", + "congestion_control": "bbr", + "tls": { + "enabled": true, + "server_name": "example.org" + } + } + ] + } + ``` + +=== ":material-check: With self-sign certificate" + + !!! info "Tip" + + Use `sing-box merge` command to merge configuration and certificate into one file. + + ```json + { + "outbounds": [ + { + "type": "tuic", + "server": "127.0.0.1", + "server_port": 8080, + "uuid": "", + "password": "", + "congestion_control": "bbr", + "tls": { + "enabled": true, + "server_name": "example.org", + "certificate_path": "/path/to/certificate.pem" + } + } + ] + } + ``` + +=== ":material-alert: Ignore certificate verification" + + ```json + { + "outbounds": [ + { + "type": "tuic", + "server": "127.0.0.1", + "server_port": 8080, + "uuid": "", + "password": "", + "congestion_control": "bbr", + "tls": { + "enabled": true, + "server_name": "example.org", + "insecure": true + } + } + ] + } + ``` + diff --git a/docs/manual/proxy/client.md b/docs/manual/proxy/client.md new file mode 100644 index 0000000000..60db02deb6 --- /dev/null +++ b/docs/manual/proxy/client.md @@ -0,0 +1,425 @@ +--- +icon: material/cellphone-link +--- + +# Client + +### :material-ray-start: Introduction + +For a long time, the modern usage and principles of proxy clients +for graphical operating systems have not been clearly described. +However, we can categorize them into three types: +system proxy, firewall redirection, and virtual interface. + +### :material-web-refresh: System Proxy + +Almost all graphical environments support system-level proxies, +which are essentially ordinary HTTP proxies that only support TCP. + +| Operating System / Desktop Environment | System Proxy | Application Support | +|:---------------------------------------------|:-------------------------------------|:--------------------| +| Windows | :material-check: | :material-check: | +| macOS | :material-check: | :material-check: | +| GNOME/KDE | :material-check: | :material-check: | +| Android | ROOT or adb (permission) is required | :material-check: | +| Android/iOS (with sing-box graphical client) | via `tun.platform.http_proxy` | :material-check: | + +As one of the most well-known proxy methods, it has many shortcomings: +many TCP clients that are not based on HTTP do not check and use the system proxy. +Moreover, UDP and ICMP traffics bypass the proxy. + +```mermaid +flowchart LR + dns[DNS query] -- Is HTTP request? --> proxy[HTTP proxy] + dns --> leak[Leak] + tcp[TCP connection] -- Is HTTP request? --> proxy + tcp -- Check and use HTTP CONNECT? --> proxy + tcp --> leak + udp[UDP packet] --> leak +``` + +### :material-wall-fire: Firewall Redirection + +This type of usage typically relies on the firewall or hook interface provided by the operating system, +such as Windows’ WFP, Linux’s redirect, TProxy and eBPF, and macOS’s pf. +Although it is intrusive and cumbersome to configure, +it remains popular within the community of amateur proxy open source projects like V2Ray, +due to the low technical requirements it imposes on the software. + +### :material-expansion-card: Virtual Interface + +All L2/L3 proxies (seriously defined VPNs, such as OpenVPN, WireGuard) are based on virtual network interfaces, +which is also the only way for all L4 proxies to work as VPNs on mobile platforms like Android, iOS. + +The sing-box inherits and develops clash-premium’s TUN inbound (L3 to L4 conversion) +as the most reasonable method for performing transparent proxying. + +```mermaid +flowchart TB + packet[IP Packet] + packet --> windows[Windows / macOS] + packet --> linux[Linux] + tun[TUN interface] + windows -. route .-> tun + linux -. iproute2 route/rule .-> tun + tun --> gvisor[gVisor TUN stack] + tun --> system[system TUN stack] + assemble([L3 to L4 assemble]) + gvisor --> assemble + system --> assemble + assemble --> conn[TCP and UDP connections] + conn --> router[sing-box Router] + router --> direct[Direct outbound] + router --> proxy[Proxy outbounds] + router -- DNS hijack --> dns_out[DNS outbound] + dns_out --> dns_router[DNS router] + dns_router --> router + direct --> adi([auto detect interface]) + proxy --> adi + adi --> default[Default network interface in the system] + default --> destination[Destination server] + default --> proxy_server[Proxy server] + proxy_server --> destination +``` + +## :material-cellphone-link: Examples + +### Basic TUN usage for Chinese users + +=== ":material-numeric-4-box: IPv4 only" + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "address": "tls://8.8.8.8" + }, + { + "tag": "local", + "address": "223.5.5.5", + "detour": "direct" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + } + ], + "strategy": "ipv4_only" + }, + "inbounds": [ + { + "type": "tun", + "inet4_address": "172.19.0.1/30", + "auto_route": true, + "strict_route": false + } + ], + "outbounds": [ + // ... + { + "type": "direct", + "tag": "direct" + }, + { + "type": "dns", + "tag": "dns-out" + } + ], + "route": { + "rules": [ + { + "protocol": "dns", + "outbound": "dns-out" + }, + { + "geoip": [ + "private" + ], + "outbound": "direct" + } + ], + "auto_detect_interface": true + } + } + ``` + +=== ":material-numeric-6-box: IPv4 & IPv6" + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "address": "tls://8.8.8.8" + }, + { + "tag": "local", + "address": "223.5.5.5", + "detour": "direct" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + } + ] + }, + "inbounds": [ + { + "type": "tun", + "inet4_address": "172.19.0.1/30", + "inet6_address": "fdfe:dcba:9876::1/126", + "auto_route": true, + "strict_route": false + } + ], + "outbounds": [ + // ... + { + "type": "direct", + "tag": "direct" + }, + { + "type": "dns", + "tag": "dns-out" + } + ], + "route": { + "rules": [ + { + "protocol": "dns", + "outbound": "dns-out" + }, + { + "geoip": [ + "private" + ], + "outbound": "direct" + } + ], + "auto_detect_interface": true + } + } + ``` + +=== ":material-domain-switch: FakeIP" + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "address": "tls://8.8.8.8" + }, + { + "tag": "local", + "address": "223.5.5.5", + "detour": "direct" + }, + { + "tag": "remote", + "address": "fakeip" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + }, + { + "query_type": [ + "A", + "AAAA" + ], + "server": "remote" + } + ], + "fakeip": { + "enabled": true, + "inet4_range": "198.18.0.0/15", + "inet6_range": "fc00::/18" + }, + "independent_cache": true + }, + "inbounds": [ + { + "type": "tun", + "inet4_address": "172.19.0.1/30", + "inet6_address": "fdfe:dcba:9876::1/126", + "auto_route": true, + "strict_route": true + } + ], + "outbounds": [ + // ... + { + "type": "direct", + "tag": "direct" + }, + { + "type": "dns", + "tag": "dns-out" + } + ], + "route": { + "rules": [ + { + "protocol": "dns", + "outbound": "dns-out" + }, + { + "geoip": [ + "private" + ], + "outbound": "direct" + } + ], + "auto_detect_interface": true + } + } + ``` + +### Traffic bypass usage for Chinese users + +=== ":material-dns: DNS rules" + + !!! info + + DNS rules are optional if FakeIP is used. + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "address": "tls://8.8.8.8" + }, + { + "tag": "local", + "address": "223.5.5.5", + "detour": "direct" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + }, + { + "clash_mode": "Direct", + "server": "local" + }, + { + "clash_mode": "Global", + "server": "google" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "geosite": "geolocation-!cn", + "invert": true + }, + { + "geosite": [ + "cn", + "category-companies@cn" + ], + } + ], + "server": "local" + } + ] + } + } + ``` + +=== ":material-router-network: Route rules" + + ```json + { + "outbounds": [ + { + "type": "direct", + "tag": "direct" + }, + { + "type": "block", + "tag": "block" + } + ], + "route": { + "rules": [ + { + "type": "logical", + "mode": "or", + "rules": [ + { + "protocol": "dns" + }, + { + "port": 53 + } + ], + "outbound": "dns" + }, + { + "geoip": "private", + "outbound": "direct" + }, + { + "clash_mode": "Direct", + "outbound": "direct" + }, + { + "clash_mode": "Global", + "outbound": "default" + }, + { + "type": "logical", + "mode": "or", + "rules": [ + { + "port": 853 + }, + { + "network": "udp", + "port": 443 + }, + { + "protocol": "stun" + } + ], + "outbound": "block" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "geosite": "geolocation-!cn", + "invert": true + }, + { + "geosite": [ + "cn", + "category-companies@cn" + ], + "geoip": "cn" + } + ], + "outbound": "direct" + } + ] + } + } + ``` \ No newline at end of file diff --git a/docs/manual/proxy/server.md b/docs/manual/proxy/server.md new file mode 100644 index 0000000000..4d2667e039 --- /dev/null +++ b/docs/manual/proxy/server.md @@ -0,0 +1,10 @@ +--- +icon: material/server +--- + +# Server + +To use sing-box as a proxy protocol server, you pretty much only need to configure the inbound for that protocol. + +The Proxy Protocol menu below contains descriptions and configuration examples +of recommended protocols for bypassing GFW. diff --git a/docs/support.md b/docs/support.md index 257b343992..9ddb4ecfff 100644 --- a/docs/support.md +++ b/docs/support.md @@ -1,4 +1,13 @@ -Github Issue: [Issues · SagerNet/sing-box](https://github.com/SagerNet/sing-box/issues) -Telegram Notification channel: [@yapnc](https://t.me/yapnc) -Telegram User group: [@yapug](https://t.me/yapug) -Email: [contact@sagernet.org](mailto:contact@sagernet.org) +--- +icon: material/forum +--- + +# Support + +| Channel | Link | +|:------------------------------|:--------------------------------------------| +| Community | https://community.sagernet.org | +| GitHub Issues | https://github.com/SagerNet/sing-box/issues | +| Telegram notification channel | https://t.me/yapnc | +| Telegram user group | https://t.me/yapug | +| Email | contact@sagernet.org | diff --git a/docs/support.zh.md b/docs/support.zh.md index 031bf6f9a4..eb07ea8292 100644 --- a/docs/support.zh.md +++ b/docs/support.zh.md @@ -1,4 +1,14 @@ -Github 工单: [Issues · SagerNet/sing-box](https://github.com/SagerNet/sing-box/issues) -Telegram 通知频道: [@yapnc](https://t.me/yapnc) -Telegram 用户组: [@yapug](https://t.me/yapug) -Email: [contact@sagernet.org](mailto:contact@sagernet.org) +--- +icon: material/forum +--- + +# 支持 + +| 通道 | 链接 | +|:--------------|:--------------------------------------------| +| 社区 | https://community.sagernet.org | +| GitHub Issues | https://github.com/SagerNet/sing-box/issues | +| Telegram 通知频道 | https://t.me/yapnc | +| Telegram 用户组 | https://t.me/yapug | +| 邮件 | contact@sagernet.org | + diff --git a/mkdocs.yml b/mkdocs.yml index 98c46c6fb9..1d4b1d8b0e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,25 +29,39 @@ theme: - navigation.expand - navigation.sections - header.autohide + - content.code.copy + - content.code.select + - content.code.annotate nav: - Home: - index.md - - Features: features.md - Deprecated: deprecated.md - Support: support.md - Change Log: changelog.md - Installation: - - From source: installation/from-source.md - - Package Manager: - - macOS: installation/package-manager/macOS.md - - Windows: installation/package-manager/windows.md - - Android: installation/package-manager/android.md - - Clients: - - Specification: installation/clients/specification.md - - iOS: installation/clients/sfi.md - - macOS: installation/clients/sfm.md - - Apple tvOS: installation/clients/sft.md - - Android: installation/clients/sfa.md + - Package Manager: installation/package-manager.md + - Docker: installation/docker.md + - Build from source: installation/build-from-source.md + - Graphical Clients: + - clients/index.md + - Android: + - clients/android/index.md + - Features: clients/android/features.md + - Apple platforms: + - clients/apple/index.md + - Features: clients/apple/features.md + - General: clients/general.md + - Privacy policy: clients/privacy.md + - Manual: + - Proxy: + - Server: manual/proxy/server.md + - Client: manual/proxy/client.md +# - TUN: manual/proxy/tun.md + - Proxy Protocol: + - Shadowsocks: manual/proxy-protocol/shadowsocks.md + - Trojan: manual/proxy-protocol/trojan.md + - TUIC: manual/proxy-protocol/tuic.md + - Hysteria 2: manual/proxy-protocol/hysteria2.md - Configuration: - configuration/index.md - Log: @@ -115,24 +129,6 @@ nav: - DNS: configuration/outbound/dns.md - Selector: configuration/outbound/selector.md - URLTest: configuration/outbound/urltest.md - - FAQ: - - faq/index.md - - FakeIP: faq/fakeip.md - - Known Issues: faq/known-issues.md - - Examples: - - examples/index.md - - Linux Server Installation: examples/linux-server-installation.md - - Tun: examples/tun.md - - DNS Hijack: examples/dns-hijack.md - - Shadowsocks: examples/shadowsocks.md - - ShadowTLS: examples/shadowtls.md - - Clash API: examples/clash-api.md - - FakeIP: examples/fakeip.md - - Contributing: - - contributing/index.md - - Developing: - - Environment: contributing/environment.md - - Sub projects: contributing/sub-projects.md markdown_extensions: - pymdownx.inlinehilite - pymdownx.snippets @@ -143,6 +139,7 @@ markdown_extensions: - pymdownx.keys - pymdownx.mark - pymdownx.tilde + - pymdownx.magiclink - admonition - attr_list - md_in_html @@ -154,6 +151,14 @@ markdown_extensions: alternate_style: true - pymdownx.tasklist: custom_checkbox: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format extra: social: - icon: fontawesome/brands/github @@ -162,48 +167,58 @@ extra: plugins: - search - i18n: - default_language: en + docs_structure: suffix + fallback_to_default: true languages: - en: + - build: true + default: true + locale: en name: English - build: false - zh: + - build: true + default: false + locale: zh name: 简体中文 - material_alternate: true - nav_translations: - zh: - Getting Started: 开始 - Features: 特性 - Support: 支持 - Change Log: 更新日志 + nav_translations: + Home: 开始 + Deprecated: 废弃功能列表 + Support: 支持 + Change Log: 更新日志 - Installation: 安装 - From source: 从源代码 - Clients: 客户端 + Installation: 安装 + Package Manager: 包管理器 + Build from source: 从源代码构建 - Configuration: 配置 - Log: 日志 - DNS Server: DNS 服务器 - DNS Rule: DNS 规则 + Graphical Clients: 图形界面客户端 + Features: 特性 + Apple platforms: Apple 平台 + General: 通用 + Privacy policy: 隐私政策 - Route: 路由 - Route Rule: 路由规则 - Protocol Sniff: 协议探测 + Configuration: 配置 + Log: 日志 + DNS Server: DNS 服务器 + DNS Rule: DNS 规则 - Experimental: 实验性 + Route: 路由 + Route Rule: 路由规则 + Protocol Sniff: 协议探测 - Shared: 通用 - Listen Fields: 监听字段 - Dial Fields: 拨号字段 - DNS01 Challenge Fields: DNS01 验证字段 - Multiplex: 多路复用 - V2Ray Transport: V2Ray 传输层 + Experimental: 实验性 - Inbound: 入站 - Outbound: 出站 + Shared: 通用 + Listen Fields: 监听字段 + Dial Fields: 拨号字段 + DNS01 Challenge Fields: DNS01 验证字段 + Multiplex: 多路复用 + V2Ray Transport: V2Ray 传输层 - FAQ: 常见问题 - Known Issues: 已知问题 - Examples: 示例 - Linux Server Installation: Linux 服务器安装 - DNS Hijack: DNS 劫持 + Inbound: 入站 + Outbound: 出站 + + FAQ: 常见问题 + Known Issues: 已知问题 + Examples: 示例 + Linux Server Installation: Linux 服务器安装 + DNS Hijack: DNS 劫持 + reconfigure_material: true + reconfigure_search: true \ No newline at end of file From 737f47858a6f72aca2cc515600033f5236b1af98 Mon Sep 17 00:00:00 2001 From: Aximaris <73300175+kwfcfc@users.noreply.github.com> Date: Sun, 26 Nov 2023 00:17:10 +0800 Subject: [PATCH 009/103] feat: add TLS mutual authentication Introduce a feature to require and verify client certificate to provide mutual authentication in TLS. --- common/tls/std_client.go | 35 ++++++++++++++++++++++ common/tls/std_server.go | 26 +++++++++++++++++ option/tls.go | 63 +++++++++++++++++++++++----------------- 3 files changed, 97 insertions(+), 27 deletions(-) diff --git a/common/tls/std_client.go b/common/tls/std_client.go index 90f51821e8..c6be4d5719 100644 --- a/common/tls/std_client.go +++ b/common/tls/std_client.go @@ -128,5 +128,40 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb } tlsConfig.RootCAs = certPool } + if options.ClientAuth { + // explicitly call mutual verification + var cert []byte + var key []byte + + if len(options.ClientCertificate) > 0 { + cert = []byte(strings.Join(options.ClientCertificate, "\n")) + } else if options.ClientCertificatePath != "" { + content, err := os.ReadFile(options.ClientCertificatePath) + if err != nil { + return nil, E.Cause(err, "read client certificate") + } + cert = content + } + if len(options.ClientKey) > 0 { + key = []byte(strings.Join(options.ClientKey, "\n")) + } else if options.ClientKeyPath != "" { + content, err := os.ReadFile(options.ClientKeyPath) + if err != nil { + return nil, E.Cause(err, "read client key") + } + key = content + } + if cert == nil { + return nil, E.New("missing client certificate") + } else if key == nil { + return nil, E.New("missing client key") + } + + keyPair, err := tls.X509KeyPair(cert, key) + if err != nil { + return nil, E.New(err, "parse x509 key pair") + } + tlsConfig.Certificates = []tls.Certificate{keyPair} + } return &STDClientConfig{&tlsConfig}, nil } diff --git a/common/tls/std_server.go b/common/tls/std_server.go index 28a94cf15f..3ee25cf1a2 100644 --- a/common/tls/std_server.go +++ b/common/tls/std_server.go @@ -3,6 +3,7 @@ package tls import ( "context" "crypto/tls" + "crypto/x509" "net" "os" "strings" @@ -212,6 +213,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound } var certificate []byte var key []byte + var clientca []byte if acmeService == nil { if len(options.Certificate) > 0 { certificate = []byte(strings.Join(options.Certificate, "\n")) @@ -249,6 +251,29 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound tlsConfig.Certificates = []tls.Certificate{keyPair} } } + if options.ClientAuth { + // require client authentication + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + if len(options.ClientCA) > 0 { + clientca = []byte(strings.Join(options.ClientCA, "\n")) + } else if options.ClientCAPath != "" { + content, err := os.ReadFile(options.ClientCAPath) + if err != nil { + return nil, E.Cause(err, "read client CA") + } + clientca = content + } + // add client ca certpool + if len(clientca) > 0 { + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(clientca) { + return nil, E.New("failed to parse client CA:\n\n", clientca) + } + tlsConfig.ClientCAs = certPool + } + } else { + tlsConfig.ClientAuth = tls.NoClientCert + } return &STDServerConfig{ config: tlsConfig, logger: logger, @@ -257,5 +282,6 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound key: key, certificatePath: options.CertificatePath, keyPath: options.KeyPath, + watcher: &fsnotify.Watcher{}, }, nil } diff --git a/option/tls.go b/option/tls.go index a38b4ef596..9478814903 100644 --- a/option/tls.go +++ b/option/tls.go @@ -1,36 +1,45 @@ package option type InboundTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - ServerName string `json:"server_name,omitempty"` - Insecure bool `json:"insecure,omitempty"` - ALPN Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites Listable[string] `json:"cipher_suites,omitempty"` - Certificate Listable[string] `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - Key Listable[string] `json:"key,omitempty"` - KeyPath string `json:"key_path,omitempty"` - ACME *InboundACMEOptions `json:"acme,omitempty"` - ECH *InboundECHOptions `json:"ech,omitempty"` - Reality *InboundRealityOptions `json:"reality,omitempty"` + Enabled bool `json:"enabled,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites Listable[string] `json:"cipher_suites,omitempty"` + Certificate Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + Key Listable[string] `json:"key,omitempty"` + KeyPath string `json:"key_path,omitempty"` + ClientAuth bool `json:"clientAuth,omitempty"` + // , Requireandverify + ClientCA Listable[string] `json:"clientCA,omitempty"` + ClientCAPath string `json:"clientCA_path,omitempty"` + ACME *InboundACMEOptions `json:"acme,omitempty"` + ECH *InboundECHOptions `json:"ech,omitempty"` + Reality *InboundRealityOptions `json:"reality,omitempty"` } type OutboundTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - DisableSNI bool `json:"disable_sni,omitempty"` - ServerName string `json:"server_name,omitempty"` - Insecure bool `json:"insecure,omitempty"` - ALPN Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites Listable[string] `json:"cipher_suites,omitempty"` - Certificate Listable[string] `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - ECH *OutboundECHOptions `json:"ech,omitempty"` - UTLS *OutboundUTLSOptions `json:"utls,omitempty"` - Reality *OutboundRealityOptions `json:"reality,omitempty"` + Enabled bool `json:"enabled,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites Listable[string] `json:"cipher_suites,omitempty"` + Certificate Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + ClientAuth bool `json:"clientAuth,omitempty"` + ClientKey Listable[string] `json:"client_key,omitempty"` + ClientKeyPath string `json:"client_key_path,omitempty"` + ClientCertificate Listable[string] `json:"client_certificate,omitempty"` + ClientCertificatePath string `json:"client_certificate_path,omitempty"` + ECH *OutboundECHOptions `json:"ech,omitempty"` + UTLS *OutboundUTLSOptions `json:"utls,omitempty"` + Reality *OutboundRealityOptions `json:"reality,omitempty"` } type InboundRealityOptions struct { From 20088dc78f0612ea79de018caf42283c4faf249c Mon Sep 17 00:00:00 2001 From: Aximaris <73300175+kwfcfc@users.noreply.github.com> Date: Sun, 26 Nov 2023 00:18:29 +0800 Subject: [PATCH 010/103] docs: add documentation for TLS client authentication settings --- docs/configuration/shared/tls.md | 54 +++++++++++++++++++++++++++-- docs/configuration/shared/tls.zh.md | 52 +++++++++++++++++++++++++-- 2 files changed, 101 insertions(+), 5 deletions(-) diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index 9a02bbff2f..18b4cfe59e 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -12,6 +12,9 @@ "certificate_path": "", "key": [], "key_path": "", + "clientAuth": false, + "clientCA": [], + "clientCA_path": "", "acme": { "domain": [], "data_directory": "", @@ -64,8 +67,13 @@ "min_version": "", "max_version": "", "cipher_suites": [], - "certificate": "", + "certificate": [], "certificate_path": "", + "clientAuth": false, + "client_key": [], + "client_key_path": "", + "client_certificate": [], + "client_certificate_path": "", "ech": { "enabled": false, "pq_signature_schemes_enabled": false, @@ -189,6 +197,46 @@ The server private key line array, in PEM format. The path to the server private key, in PEM format. +#### clientAuth + +Enable TLS Client Authentication + +#### clientCA + +==Server only== + +The server's client CA Certificate line array, in PEM format. + +#### clientCA_path + +==Server only== + +The path to the server's client CA certificate, in PEM format. + +#### client_certificate + +==Client only== + +The client certificate line array, in PEM format. + +#### certificate_path + +==Client only== + +The path to the client certificate, in PEM format. + +#### client_key + +==Client only== + +The client private key line array, in PEM format. + +#### client_key_path + +==Client only== + +The path to the client private, in PEM format. + ## Custom TLS support !!! info "QUIC support" @@ -245,7 +293,7 @@ It is recommended to match the parameters of `sing-box generate ech-keypair`. Disables adaptive sizing of TLS records. -When true, the largest possible TLS record size is always used. +When true, the largest possible TLS record size is always used. When false, the size of TLS records may be adjusted in an attempt to improve latency. #### key @@ -405,4 +453,4 @@ Check disabled if empty. ### Reload -For server configuration, certificate, key and ECH key will be automatically reloaded if modified. \ No newline at end of file +For server configuration, certificate, key and ECH key will be automatically reloaded if modified. diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index bbb0871963..8756a2075d 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -12,6 +12,9 @@ "certificate_path": "", "key": [], "key_path": "", + "clientAuth": false, + "clientCA": [], + "clientCA_path": "", "acme": { "domain": [], "data_directory": "", @@ -66,9 +69,14 @@ "cipher_suites": [], "certificate": [], "certificate_path": "", + "clientAuth": false, + "client_key": [], + "client_key_path": "", + "client_certificate": [], + "client_certificate_path": "", "ech": { "enabled": false, - "pq_signature_schemes_enabled": false, + "pqignature_schemes_enabled": false, "dynamic_record_sizing_disabled": false, "config": [], "config_path": "" @@ -189,6 +197,46 @@ TLS 版本值: 服务器 PEM 私钥路径。 +#### clientAuth + +启用客户端验证 + +#### clientCA + +==仅服务器== + +服务器 PEM 验证客户端 CA 证书行数组 + +#### clientCA_path + +==仅服务器== + +服务器 PEM 验证客户端 CA 证书路径 + +#### client_certificate + +==仅客户端== + +客户端 PEM 证书行数组。 + +#### certificate_path + +==仅客户端== + +客户端 PEM 证书路径。 + +#### client_key + +==仅客户端== + +客户端 PEM 私钥行数组。 + +#### client_key_path + +==仅客户端== + +客户端 PEM 私钥路径。 + #### utls ==仅客户端== @@ -394,4 +442,4 @@ ACME DNS01 验证字段。如果配置,将禁用其他验证方法。 ### 重载 -对于服务器配置,如果修改,证书和密钥将自动重新加载。 \ No newline at end of file +对于服务器配置,如果修改,证书和密钥将自动重新加载。 From 1c1eb01fe6522e5d1b0b4ad633a8190de264a4fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 26 Nov 2023 22:07:23 +0800 Subject: [PATCH 011/103] fix dhcp 2 --- transport/dhcp/server.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/transport/dhcp/server.go b/transport/dhcp/server.go index b07412f42c..1a2c2938b2 100644 --- a/transport/dhcp/server.go +++ b/transport/dhcp/server.go @@ -87,9 +87,15 @@ func (t *Transport) Start() error { } func (t *Transport) Reset() { + for _, transport := range t.transports { + transport.Reset() + } } func (t *Transport) Close() error { + for _, transport := range t.transports { + transport.Close() + } if t.interfaceCallback != nil { t.router.InterfaceMonitor().UnregisterCallback(t.interfaceCallback) } @@ -266,6 +272,9 @@ func (t *Transport) recreateServers(iface *net.Interface, serverAddrs []netip.Ad } transports = append(transports, serverTransport) } + for _, transport := range t.transports { + transport.Close() + } t.transports = transports return nil } From edadbbadd81410d1c7f087569360ebff2344277f Mon Sep 17 00:00:00 2001 From: jose-C2OaWi <111356383+jose-C2OaWi@users.noreply.github.com> Date: Mon, 27 Nov 2023 17:47:55 +0800 Subject: [PATCH 012/103] docs: fix typo fix typo in tls.zh.md Signed-off-by: jose-C2OaWi <111356383+jose-C2OaWi@users.noreply.github.com> --- docs/configuration/shared/tls.zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index 8756a2075d..f1ba6d5d0c 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -76,7 +76,7 @@ "client_certificate_path": "", "ech": { "enabled": false, - "pqignature_schemes_enabled": false, + "pq_signature_schemes_enabled": false, "dynamic_record_sizing_disabled": false, "config": [], "config_path": "" From bbea3aa77ea68a3745d8325237446e160ecc975e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Nov 2023 12:00:28 +0800 Subject: [PATCH 013/103] Migrate to independent cache file --- adapter/experimental.go | 10 ++- box.go | 12 ++- .../{clashapi => }/cachefile/cache.go | 76 +++++++++++++------ .../{clashapi => }/cachefile/fakeip.go | 0 experimental/clashapi/cache.go | 11 ++- experimental/clashapi/server.go | 65 ++++------------ experimental/libbox/command_group.go | 21 ++--- experimental/libbox/command_urltest.go | 21 ++--- option/clash.go | 31 -------- option/experimental.go | 47 +++++++++++- option/group.go | 15 ++++ option/v2ray.go | 12 --- outbound/builder.go | 2 +- outbound/selector.go | 15 ++-- route/router.go | 2 +- transport/fakeip/store.go | 15 ++-- 16 files changed, 184 insertions(+), 171 deletions(-) rename experimental/{clashapi => }/cachefile/cache.go (83%) rename experimental/{clashapi => }/cachefile/fakeip.go (100%) delete mode 100644 option/clash.go create mode 100644 option/group.go diff --git a/adapter/experimental.go b/adapter/experimental.go index 697fa9f199..ac523e4e74 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -13,15 +13,17 @@ type ClashServer interface { PreStarter Mode() string ModeList() []string - StoreSelected() bool - StoreFakeIP() bool - CacheFile() ClashCacheFile HistoryStorage() *urltest.HistoryStorage RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker) } -type ClashCacheFile interface { +type CacheFile interface { + Service + PreStarter + + StoreFakeIP() bool + LoadMode() string StoreMode(mode string) error LoadSelected(group string) string diff --git a/box.go b/box.go index c10222ddf7..ae61bffe7c 100644 --- a/box.go +++ b/box.go @@ -10,6 +10,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/inbound" "github.com/sagernet/sing-box/log" @@ -45,17 +46,21 @@ type Options struct { } func New(options Options) (*Box, error) { + createdAt := time.Now() ctx := options.Context if ctx == nil { ctx = context.Background() } ctx = service.ContextWithDefaultRegistry(ctx) ctx = pause.ContextWithDefaultManager(ctx) - createdAt := time.Now() experimentalOptions := common.PtrValueOrDefault(options.Experimental) applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) + var needCacheFile bool var needClashAPI bool var needV2RayAPI bool + if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil { + needCacheFile = true + } if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil { needClashAPI = true } @@ -147,6 +152,11 @@ func New(options Options) (*Box, error) { } preServices := make(map[string]adapter.Service) postServices := make(map[string]adapter.Service) + if needCacheFile { + cacheFile := cachefile.NewCacheFile(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) + preServices["cache file"] = cacheFile + service.MustRegister[adapter.CacheFile](ctx, cacheFile) + } if needClashAPI { clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options) diff --git a/experimental/clashapi/cachefile/cache.go b/experimental/cachefile/cache.go similarity index 83% rename from experimental/clashapi/cachefile/cache.go rename to experimental/cachefile/cache.go index 0911829749..962e884f4a 100644 --- a/experimental/clashapi/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/bbolt" bboltErrors "github.com/sagernet/bbolt/errors" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service/filemanager" @@ -31,11 +32,15 @@ var ( cacheIDDefault = []byte("default") ) -var _ adapter.ClashCacheFile = (*CacheFile)(nil) +var _ adapter.CacheFile = (*CacheFile)(nil) type CacheFile struct { + ctx context.Context + path string + cacheID []byte + storeFakeIP bool + DB *bbolt.DB - cacheID []byte saveAccess sync.RWMutex saveDomain map[netip.Addr]string saveAddress4 map[string]netip.Addr @@ -43,7 +48,23 @@ type CacheFile struct { saveMetadataTimer *time.Timer } -func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) { +func NewCacheFile(ctx context.Context, options option.CacheFileOptions) *CacheFile { + var cacheIDBytes []byte + if options.CacheID != "" { + cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...) + } + return &CacheFile{ + ctx: ctx, + path: options.Path, + cacheID: cacheIDBytes, + storeFakeIP: options.StoreFakeIP, + saveDomain: make(map[netip.Addr]string), + saveAddress4: make(map[string]netip.Addr), + saveAddress6: make(map[string]netip.Addr), + } +} + +func (c *CacheFile) start() error { const fileMode = 0o666 options := bbolt.Options{Timeout: time.Second} var ( @@ -51,7 +72,7 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) err error ) for i := 0; i < 10; i++ { - db, err = bbolt.Open(path, fileMode, &options) + db, err = bbolt.Open(c.path, fileMode, &options) if err == nil { break } @@ -59,23 +80,20 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) continue } if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) { - rmErr := os.Remove(path) + rmErr := os.Remove(c.path) if rmErr != nil { - return nil, err + return err } } time.Sleep(100 * time.Millisecond) } if err != nil { - return nil, err + return err } - err = filemanager.Chown(ctx, path) + err = filemanager.Chown(c.ctx, c.path) if err != nil { - return nil, E.Cause(err, "platform chown") - } - var cacheIDBytes []byte - if cacheID != "" { - cacheIDBytes = append([]byte{0}, []byte(cacheID)...) + db.Close() + return E.Cause(err, "platform chown") } err = db.Batch(func(tx *bbolt.Tx) error { return tx.ForEach(func(name []byte, b *bbolt.Bucket) error { @@ -97,15 +115,27 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) }) }) if err != nil { - return nil, err + db.Close() + return err } - return &CacheFile{ - DB: db, - cacheID: cacheIDBytes, - saveDomain: make(map[netip.Addr]string), - saveAddress4: make(map[string]netip.Addr), - saveAddress6: make(map[string]netip.Addr), - }, nil + c.DB = db + return nil +} + +func (c *CacheFile) PreStart() error { + return c.start() +} + +func (c *CacheFile) Start() error { + return nil +} + +func (c *CacheFile) Close() error { + return c.DB.Close() +} + +func (c *CacheFile) StoreFakeIP() bool { + return c.storeFakeIP } func (c *CacheFile) LoadMode() string { @@ -218,7 +248,3 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { } }) } - -func (c *CacheFile) Close() error { - return c.DB.Close() -} diff --git a/experimental/clashapi/cachefile/fakeip.go b/experimental/cachefile/fakeip.go similarity index 100% rename from experimental/clashapi/cachefile/fakeip.go rename to experimental/cachefile/fakeip.go diff --git a/experimental/clashapi/cache.go b/experimental/clashapi/cache.go index 7582fde5ae..9c088a82f7 100644 --- a/experimental/clashapi/cache.go +++ b/experimental/clashapi/cache.go @@ -1,23 +1,26 @@ package clashapi import ( + "context" "net/http" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/service" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) -func cacheRouter(router adapter.Router) http.Handler { +func cacheRouter(ctx context.Context) http.Handler { r := chi.NewRouter() - r.Post("/fakeip/flush", flushFakeip(router)) + r.Post("/fakeip/flush", flushFakeip(ctx)) return r } -func flushFakeip(router adapter.Router) func(w http.ResponseWriter, r *http.Request) { +func flushFakeip(ctx context.Context) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if cacheFile := router.ClashServer().CacheFile(); cacheFile != nil { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile != nil { err := cacheFile.FakeIPReset() if err != nil { render.Status(r, http.StatusInternalServerError) diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 6a3d6f66f7..135ff3ef7d 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -15,7 +15,6 @@ import ( "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" - "github.com/sagernet/sing-box/experimental/clashapi/cachefile" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -49,12 +48,6 @@ type Server struct { mode string modeList []string modeUpdateHook chan<- struct{} - storeMode bool - storeSelected bool - storeFakeIP bool - cacheFilePath string - cacheID string - cacheFile adapter.ClashCacheFile externalController bool externalUI string @@ -76,9 +69,6 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ trafficManager: trafficManager, modeList: options.ModeList, externalController: options.ExternalController != "", - storeMode: options.StoreMode, - storeSelected: options.StoreSelected, - storeFakeIP: options.StoreFakeIP, externalUIDownloadURL: options.ExternalUIDownloadURL, externalUIDownloadDetour: options.ExternalUIDownloadDetour, } @@ -94,18 +84,9 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ server.modeList = append([]string{defaultMode}, server.modeList...) } server.mode = defaultMode - if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.ExternalController == "" { - cachePath := os.ExpandEnv(options.CacheFile) - if cachePath == "" { - cachePath = "cache.db" - } - if foundPath, loaded := C.FindPath(cachePath); loaded { - cachePath = foundPath - } else { - cachePath = filemanager.BasePath(ctx, cachePath) - } - server.cacheFilePath = cachePath - server.cacheID = options.CacheID + //goland:noinspection GoDeprecation + if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" { + return nil, E.New("cache_file and related fields in Clash API is deprecated in sing-box 1.8.0, use experimental.cache_file instead.") } cors := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, @@ -128,7 +109,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/script", scriptRouter()) r.Mount("/profile", profileRouter()) - r.Mount("/cache", cacheRouter(router)) + r.Mount("/cache", cacheRouter(ctx)) r.Mount("/dns", dnsRouter(router)) server.setupMetaAPI(r) @@ -147,19 +128,13 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ } func (s *Server) PreStart() error { - if s.cacheFilePath != "" { - cacheFile, err := cachefile.Open(s.ctx, s.cacheFilePath, s.cacheID) - if err != nil { - return E.Cause(err, "open cache file") - } - s.cacheFile = cacheFile - if s.storeMode { - mode := s.cacheFile.LoadMode() - if common.Any(s.modeList, func(it string) bool { - return strings.EqualFold(it, mode) - }) { - s.mode = mode - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + mode := cacheFile.LoadMode() + if common.Any(s.modeList, func(it string) bool { + return strings.EqualFold(it, mode) + }) { + s.mode = mode } } return nil @@ -187,7 +162,6 @@ func (s *Server) Close() error { return common.Close( common.PtrOrNil(s.httpServer), s.trafficManager, - s.cacheFile, s.urlTestHistory, ) } @@ -224,8 +198,9 @@ func (s *Server) SetMode(newMode string) { } } s.router.ClearDNSCache() - if s.storeMode { - err := s.cacheFile.StoreMode(newMode) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreMode(newMode) if err != nil { s.logger.Error(E.Cause(err, "save mode")) } @@ -233,18 +208,6 @@ func (s *Server) SetMode(newMode string) { s.logger.Info("updated mode: ", newMode) } -func (s *Server) StoreSelected() bool { - return s.storeSelected -} - -func (s *Server) StoreFakeIP() bool { - return s.storeFakeIP -} - -func (s *Server) CacheFile() adapter.ClashCacheFile { - return s.cacheFile -} - func (s *Server) HistoryStorage() *urltest.HistoryStorage { return s.urlTestHistory } diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go index 934820889e..2fc69b98b4 100644 --- a/experimental/libbox/command_group.go +++ b/experimental/libbox/command_group.go @@ -159,11 +159,7 @@ func readGroups(reader io.Reader) (OutboundGroupIterator, error) { func writeGroups(writer io.Writer, boxService *BoxService) error { historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx) - var cacheFile adapter.ClashCacheFile - if clashServer := boxService.instance.Router().ClashServer(); clashServer != nil { - cacheFile = clashServer.CacheFile() - } - + cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx) outbounds := boxService.instance.Router().Outbounds() var iGroups []adapter.OutboundGroup for _, it := range outbounds { @@ -288,16 +284,15 @@ func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return writeError(conn, E.New("service not ready")) } - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - err = cacheFile.StoreGroupExpand(groupTag, isExpand) - if err != nil { - return writeError(conn, err) - } + cacheFile := service.FromContext[adapter.CacheFile](serviceNow.ctx) + if cacheFile != nil { + err = cacheFile.StoreGroupExpand(groupTag, isExpand) + if err != nil { + return writeError(conn, err) } } return writeError(conn, nil) diff --git a/experimental/libbox/command_urltest.go b/experimental/libbox/command_urltest.go index 88e86a8f8c..3563d8c6a3 100644 --- a/experimental/libbox/command_urltest.go +++ b/experimental/libbox/command_urltest.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/service" ) func (c *CommandClient) URLTest(groupTag string) error { @@ -37,11 +38,11 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return nil } - abstractOutboundGroup, isLoaded := service.instance.Router().Outbound(groupTag) + abstractOutboundGroup, isLoaded := serviceNow.instance.Router().Outbound(groupTag) if !isLoaded { return writeError(conn, E.New("outbound group not found: ", groupTag)) } @@ -53,15 +54,9 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if isURLTest { go urlTest.CheckOutbounds() } else { - var historyStorage *urltest.HistoryStorage - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - historyStorage = clashServer.HistoryStorage() - } else { - return writeError(conn, E.New("Clash API is required for URLTest on non-URLTest group")) - } - + historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx) outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { - itOutbound, _ := service.instance.Router().Outbound(it) + itOutbound, _ := serviceNow.instance.Router().Outbound(it) return itOutbound }), func(it adapter.Outbound) bool { if it == nil { @@ -73,12 +68,12 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { } return true }) - b, _ := batch.New(service.ctx, batch.WithConcurrencyNum[any](10)) + b, _ := batch.New(serviceNow.ctx, batch.WithConcurrencyNum[any](10)) for _, detour := range outbounds { outboundToTest := detour outboundTag := outboundToTest.Tag() b.Go(outboundTag, func() (any, error) { - t, err := urltest.URLTest(service.ctx, "", outboundToTest) + t, err := urltest.URLTest(serviceNow.ctx, "", outboundToTest) if err != nil { historyStorage.DeleteURLTestHistory(outboundTag) } else { diff --git a/option/clash.go b/option/clash.go deleted file mode 100644 index 63ee2aebdf..0000000000 --- a/option/clash.go +++ /dev/null @@ -1,31 +0,0 @@ -package option - -type ClashAPIOptions struct { - ExternalController string `json:"external_controller,omitempty"` - ExternalUI string `json:"external_ui,omitempty"` - ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` - ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` - Secret string `json:"secret,omitempty"` - DefaultMode string `json:"default_mode,omitempty"` - StoreMode bool `json:"store_mode,omitempty"` - StoreSelected bool `json:"store_selected,omitempty"` - StoreFakeIP bool `json:"store_fakeip,omitempty"` - CacheFile string `json:"cache_file,omitempty"` - CacheID string `json:"cache_id,omitempty"` - - ModeList []string `json:"-"` -} - -type SelectorOutboundOptions struct { - Outbounds []string `json:"outbounds"` - Default string `json:"default,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} - -type URLTestOutboundOptions struct { - Outbounds []string `json:"outbounds"` - URL string `json:"url,omitempty"` - Interval Duration `json:"interval,omitempty"` - Tolerance uint16 `json:"tolerance,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} diff --git a/option/experimental.go b/option/experimental.go index a5b6acbd32..72751a590e 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -1,7 +1,48 @@ package option type ExperimentalOptions struct { - ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` - V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` - Debug *DebugOptions `json:"debug,omitempty"` + CacheFile *CacheFileOptions `json:"cache_file,omitempty"` + ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` + V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` + Debug *DebugOptions `json:"debug,omitempty"` +} + +type CacheFileOptions struct { + Enabled bool `json:"enabled,omitempty"` + Path string `json:"path,omitempty"` + CacheID string `json:"cache_id,omitempty"` + StoreFakeIP bool `json:"store_fakeip,omitempty"` +} + +type ClashAPIOptions struct { + ExternalController string `json:"external_controller,omitempty"` + ExternalUI string `json:"external_ui,omitempty"` + ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` + ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` + Secret string `json:"secret,omitempty"` + DefaultMode string `json:"default_mode,omitempty"` + ModeList []string `json:"-"` + + // Deprecated: migrated to global cache file + StoreMode bool `json:"store_mode,omitempty"` + // Deprecated: migrated to global cache file + StoreSelected bool `json:"store_selected,omitempty"` + // Deprecated: migrated to global cache file + StoreFakeIP bool `json:"store_fakeip,omitempty"` + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` +} + +type V2RayAPIOptions struct { + Listen string `json:"listen,omitempty"` + Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` +} + +type V2RayStatsServiceOptions struct { + Enabled bool `json:"enabled,omitempty"` + Inbounds []string `json:"inbounds,omitempty"` + Outbounds []string `json:"outbounds,omitempty"` + Users []string `json:"users,omitempty"` } diff --git a/option/group.go b/option/group.go new file mode 100644 index 0000000000..58824e808b --- /dev/null +++ b/option/group.go @@ -0,0 +1,15 @@ +package option + +type SelectorOutboundOptions struct { + Outbounds []string `json:"outbounds"` + Default string `json:"default,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} + +type URLTestOutboundOptions struct { + Outbounds []string `json:"outbounds"` + URL string `json:"url,omitempty"` + Interval Duration `json:"interval,omitempty"` + Tolerance uint16 `json:"tolerance,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} diff --git a/option/v2ray.go b/option/v2ray.go index 37f7b8c4b0..774a651d1d 100644 --- a/option/v2ray.go +++ b/option/v2ray.go @@ -1,13 +1 @@ package option - -type V2RayAPIOptions struct { - Listen string `json:"listen,omitempty"` - Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` -} - -type V2RayStatsServiceOptions struct { - Enabled bool `json:"enabled,omitempty"` - Inbounds []string `json:"inbounds,omitempty"` - Outbounds []string `json:"outbounds,omitempty"` - Users []string `json:"users,omitempty"` -} diff --git a/outbound/builder.go b/outbound/builder.go index 141758d870..e4d6a80e06 100644 --- a/outbound/builder.go +++ b/outbound/builder.go @@ -56,7 +56,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t case C.TypeHysteria2: return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options) case C.TypeSelector: - return NewSelector(router, logger, tag, options.SelectorOptions) + return NewSelector(ctx, router, logger, tag, options.SelectorOptions) case C.TypeURLTest: return NewURLTest(ctx, router, logger, tag, options.URLTestOptions) default: diff --git a/outbound/selector.go b/outbound/selector.go index c66591cdee..e801daeadc 100644 --- a/outbound/selector.go +++ b/outbound/selector.go @@ -12,6 +12,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" ) var ( @@ -21,6 +22,7 @@ var ( type Selector struct { myOutboundAdapter + ctx context.Context tags []string defaultTag string outbounds map[string]adapter.Outbound @@ -29,7 +31,7 @@ type Selector struct { interruptExternalConnections bool } -func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { +func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { outbound := &Selector{ myOutboundAdapter: myOutboundAdapter{ protocol: C.TypeSelector, @@ -38,6 +40,7 @@ func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, op tag: tag, dependencies: options.Outbounds, }, + ctx: ctx, tags: options.Outbounds, defaultTag: options.Default, outbounds: make(map[string]adapter.Outbound), @@ -67,8 +70,9 @@ func (s *Selector) Start() error { } if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - selected := clashServer.CacheFile().LoadSelected(s.tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + selected := cacheFile.LoadSelected(s.tag) if selected != "" { detour, loaded := s.outbounds[selected] if loaded { @@ -110,8 +114,9 @@ func (s *Selector) SelectOutbound(tag string) bool { } s.selected = detour if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - err := clashServer.CacheFile().StoreSelected(s.tag, tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreSelected(s.tag, tag) if err != nil { s.logger.Error("store selected: ", err) } diff --git a/route/router.go b/route/router.go index 972c4ec048..e50a51a955 100644 --- a/route/router.go +++ b/route/router.go @@ -261,7 +261,7 @@ func NewRouter( if fakeIPOptions.Inet6Range != nil { inet6Range = *fakeIPOptions.Inet6Range } - router.fakeIPStore = fakeip.NewStore(router, router.logger, inet4Range, inet6Range) + router.fakeIPStore = fakeip.NewStore(ctx, router.logger, inet4Range, inet6Range) } usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor() diff --git a/transport/fakeip/store.go b/transport/fakeip/store.go index 96f6bf031e..83677b0d05 100644 --- a/transport/fakeip/store.go +++ b/transport/fakeip/store.go @@ -1,17 +1,19 @@ package fakeip import ( + "context" "net/netip" "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" ) var _ adapter.FakeIPStore = (*Store)(nil) type Store struct { - router adapter.Router + ctx context.Context logger logger.Logger inet4Range netip.Prefix inet6Range netip.Prefix @@ -20,9 +22,9 @@ type Store struct { inet6Current netip.Addr } -func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { +func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { return &Store{ - router: router, + ctx: ctx, logger: logger, inet4Range: inet4Range, inet6Range: inet6Range, @@ -31,10 +33,9 @@ func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Pref func (s *Store) Start() error { var storage adapter.FakeIPStorage - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreFakeIP() { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - storage = cacheFile - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil && cacheFile.StoreFakeIP() { + storage = cacheFile } if storage == nil { storage = NewMemoryStorage() From 0f2e73fcd605a473cd15ba1482073176f804f4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Nov 2023 12:00:28 +0800 Subject: [PATCH 014/103] Migrate to independent cache file --- adapter/experimental.go | 10 ++- box.go | 12 ++- .../{clashapi => }/cachefile/cache.go | 76 +++++++++++++------ .../{clashapi => }/cachefile/fakeip.go | 0 experimental/clashapi/cache.go | 11 ++- experimental/clashapi/server.go | 66 ++++------------ experimental/libbox/command_group.go | 21 ++--- experimental/libbox/command_urltest.go | 21 ++--- option/clash.go | 31 -------- option/experimental.go | 47 +++++++++++- option/group.go | 15 ++++ option/v2ray.go | 12 --- outbound/builder.go | 2 +- outbound/selector.go | 15 ++-- route/router.go | 2 +- transport/fakeip/store.go | 15 ++-- 16 files changed, 185 insertions(+), 171 deletions(-) rename experimental/{clashapi => }/cachefile/cache.go (83%) rename experimental/{clashapi => }/cachefile/fakeip.go (100%) delete mode 100644 option/clash.go create mode 100644 option/group.go diff --git a/adapter/experimental.go b/adapter/experimental.go index 697fa9f199..ac523e4e74 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -13,15 +13,17 @@ type ClashServer interface { PreStarter Mode() string ModeList() []string - StoreSelected() bool - StoreFakeIP() bool - CacheFile() ClashCacheFile HistoryStorage() *urltest.HistoryStorage RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker) } -type ClashCacheFile interface { +type CacheFile interface { + Service + PreStarter + + StoreFakeIP() bool + LoadMode() string StoreMode(mode string) error LoadSelected(group string) string diff --git a/box.go b/box.go index c10222ddf7..ae61bffe7c 100644 --- a/box.go +++ b/box.go @@ -10,6 +10,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/inbound" "github.com/sagernet/sing-box/log" @@ -45,17 +46,21 @@ type Options struct { } func New(options Options) (*Box, error) { + createdAt := time.Now() ctx := options.Context if ctx == nil { ctx = context.Background() } ctx = service.ContextWithDefaultRegistry(ctx) ctx = pause.ContextWithDefaultManager(ctx) - createdAt := time.Now() experimentalOptions := common.PtrValueOrDefault(options.Experimental) applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) + var needCacheFile bool var needClashAPI bool var needV2RayAPI bool + if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil { + needCacheFile = true + } if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil { needClashAPI = true } @@ -147,6 +152,11 @@ func New(options Options) (*Box, error) { } preServices := make(map[string]adapter.Service) postServices := make(map[string]adapter.Service) + if needCacheFile { + cacheFile := cachefile.NewCacheFile(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) + preServices["cache file"] = cacheFile + service.MustRegister[adapter.CacheFile](ctx, cacheFile) + } if needClashAPI { clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options) diff --git a/experimental/clashapi/cachefile/cache.go b/experimental/cachefile/cache.go similarity index 83% rename from experimental/clashapi/cachefile/cache.go rename to experimental/cachefile/cache.go index 0911829749..962e884f4a 100644 --- a/experimental/clashapi/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/bbolt" bboltErrors "github.com/sagernet/bbolt/errors" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service/filemanager" @@ -31,11 +32,15 @@ var ( cacheIDDefault = []byte("default") ) -var _ adapter.ClashCacheFile = (*CacheFile)(nil) +var _ adapter.CacheFile = (*CacheFile)(nil) type CacheFile struct { + ctx context.Context + path string + cacheID []byte + storeFakeIP bool + DB *bbolt.DB - cacheID []byte saveAccess sync.RWMutex saveDomain map[netip.Addr]string saveAddress4 map[string]netip.Addr @@ -43,7 +48,23 @@ type CacheFile struct { saveMetadataTimer *time.Timer } -func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) { +func NewCacheFile(ctx context.Context, options option.CacheFileOptions) *CacheFile { + var cacheIDBytes []byte + if options.CacheID != "" { + cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...) + } + return &CacheFile{ + ctx: ctx, + path: options.Path, + cacheID: cacheIDBytes, + storeFakeIP: options.StoreFakeIP, + saveDomain: make(map[netip.Addr]string), + saveAddress4: make(map[string]netip.Addr), + saveAddress6: make(map[string]netip.Addr), + } +} + +func (c *CacheFile) start() error { const fileMode = 0o666 options := bbolt.Options{Timeout: time.Second} var ( @@ -51,7 +72,7 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) err error ) for i := 0; i < 10; i++ { - db, err = bbolt.Open(path, fileMode, &options) + db, err = bbolt.Open(c.path, fileMode, &options) if err == nil { break } @@ -59,23 +80,20 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) continue } if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) { - rmErr := os.Remove(path) + rmErr := os.Remove(c.path) if rmErr != nil { - return nil, err + return err } } time.Sleep(100 * time.Millisecond) } if err != nil { - return nil, err + return err } - err = filemanager.Chown(ctx, path) + err = filemanager.Chown(c.ctx, c.path) if err != nil { - return nil, E.Cause(err, "platform chown") - } - var cacheIDBytes []byte - if cacheID != "" { - cacheIDBytes = append([]byte{0}, []byte(cacheID)...) + db.Close() + return E.Cause(err, "platform chown") } err = db.Batch(func(tx *bbolt.Tx) error { return tx.ForEach(func(name []byte, b *bbolt.Bucket) error { @@ -97,15 +115,27 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) }) }) if err != nil { - return nil, err + db.Close() + return err } - return &CacheFile{ - DB: db, - cacheID: cacheIDBytes, - saveDomain: make(map[netip.Addr]string), - saveAddress4: make(map[string]netip.Addr), - saveAddress6: make(map[string]netip.Addr), - }, nil + c.DB = db + return nil +} + +func (c *CacheFile) PreStart() error { + return c.start() +} + +func (c *CacheFile) Start() error { + return nil +} + +func (c *CacheFile) Close() error { + return c.DB.Close() +} + +func (c *CacheFile) StoreFakeIP() bool { + return c.storeFakeIP } func (c *CacheFile) LoadMode() string { @@ -218,7 +248,3 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { } }) } - -func (c *CacheFile) Close() error { - return c.DB.Close() -} diff --git a/experimental/clashapi/cachefile/fakeip.go b/experimental/cachefile/fakeip.go similarity index 100% rename from experimental/clashapi/cachefile/fakeip.go rename to experimental/cachefile/fakeip.go diff --git a/experimental/clashapi/cache.go b/experimental/clashapi/cache.go index 7582fde5ae..9c088a82f7 100644 --- a/experimental/clashapi/cache.go +++ b/experimental/clashapi/cache.go @@ -1,23 +1,26 @@ package clashapi import ( + "context" "net/http" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/service" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) -func cacheRouter(router adapter.Router) http.Handler { +func cacheRouter(ctx context.Context) http.Handler { r := chi.NewRouter() - r.Post("/fakeip/flush", flushFakeip(router)) + r.Post("/fakeip/flush", flushFakeip(ctx)) return r } -func flushFakeip(router adapter.Router) func(w http.ResponseWriter, r *http.Request) { +func flushFakeip(ctx context.Context) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if cacheFile := router.ClashServer().CacheFile(); cacheFile != nil { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile != nil { err := cacheFile.FakeIPReset() if err != nil { render.Status(r, http.StatusInternalServerError) diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 6a3d6f66f7..c40ff93846 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -15,7 +15,6 @@ import ( "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" - "github.com/sagernet/sing-box/experimental/clashapi/cachefile" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -49,12 +48,6 @@ type Server struct { mode string modeList []string modeUpdateHook chan<- struct{} - storeMode bool - storeSelected bool - storeFakeIP bool - cacheFilePath string - cacheID string - cacheFile adapter.ClashCacheFile externalController bool externalUI string @@ -76,9 +69,6 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ trafficManager: trafficManager, modeList: options.ModeList, externalController: options.ExternalController != "", - storeMode: options.StoreMode, - storeSelected: options.StoreSelected, - storeFakeIP: options.StoreFakeIP, externalUIDownloadURL: options.ExternalUIDownloadURL, externalUIDownloadDetour: options.ExternalUIDownloadDetour, } @@ -94,18 +84,10 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ server.modeList = append([]string{defaultMode}, server.modeList...) } server.mode = defaultMode - if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.ExternalController == "" { - cachePath := os.ExpandEnv(options.CacheFile) - if cachePath == "" { - cachePath = "cache.db" - } - if foundPath, loaded := C.FindPath(cachePath); loaded { - cachePath = foundPath - } else { - cachePath = filemanager.BasePath(ctx, cachePath) - } - server.cacheFilePath = cachePath - server.cacheID = options.CacheID + //goland:noinspection GoDeprecation + //nolint:staticcheck + if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" { + return nil, E.New("cache_file and related fields in Clash API is deprecated in sing-box 1.8.0, use experimental.cache_file instead.") } cors := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, @@ -128,7 +110,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/script", scriptRouter()) r.Mount("/profile", profileRouter()) - r.Mount("/cache", cacheRouter(router)) + r.Mount("/cache", cacheRouter(ctx)) r.Mount("/dns", dnsRouter(router)) server.setupMetaAPI(r) @@ -147,19 +129,13 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ } func (s *Server) PreStart() error { - if s.cacheFilePath != "" { - cacheFile, err := cachefile.Open(s.ctx, s.cacheFilePath, s.cacheID) - if err != nil { - return E.Cause(err, "open cache file") - } - s.cacheFile = cacheFile - if s.storeMode { - mode := s.cacheFile.LoadMode() - if common.Any(s.modeList, func(it string) bool { - return strings.EqualFold(it, mode) - }) { - s.mode = mode - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + mode := cacheFile.LoadMode() + if common.Any(s.modeList, func(it string) bool { + return strings.EqualFold(it, mode) + }) { + s.mode = mode } } return nil @@ -187,7 +163,6 @@ func (s *Server) Close() error { return common.Close( common.PtrOrNil(s.httpServer), s.trafficManager, - s.cacheFile, s.urlTestHistory, ) } @@ -224,8 +199,9 @@ func (s *Server) SetMode(newMode string) { } } s.router.ClearDNSCache() - if s.storeMode { - err := s.cacheFile.StoreMode(newMode) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreMode(newMode) if err != nil { s.logger.Error(E.Cause(err, "save mode")) } @@ -233,18 +209,6 @@ func (s *Server) SetMode(newMode string) { s.logger.Info("updated mode: ", newMode) } -func (s *Server) StoreSelected() bool { - return s.storeSelected -} - -func (s *Server) StoreFakeIP() bool { - return s.storeFakeIP -} - -func (s *Server) CacheFile() adapter.ClashCacheFile { - return s.cacheFile -} - func (s *Server) HistoryStorage() *urltest.HistoryStorage { return s.urlTestHistory } diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go index 934820889e..2fc69b98b4 100644 --- a/experimental/libbox/command_group.go +++ b/experimental/libbox/command_group.go @@ -159,11 +159,7 @@ func readGroups(reader io.Reader) (OutboundGroupIterator, error) { func writeGroups(writer io.Writer, boxService *BoxService) error { historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx) - var cacheFile adapter.ClashCacheFile - if clashServer := boxService.instance.Router().ClashServer(); clashServer != nil { - cacheFile = clashServer.CacheFile() - } - + cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx) outbounds := boxService.instance.Router().Outbounds() var iGroups []adapter.OutboundGroup for _, it := range outbounds { @@ -288,16 +284,15 @@ func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return writeError(conn, E.New("service not ready")) } - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - err = cacheFile.StoreGroupExpand(groupTag, isExpand) - if err != nil { - return writeError(conn, err) - } + cacheFile := service.FromContext[adapter.CacheFile](serviceNow.ctx) + if cacheFile != nil { + err = cacheFile.StoreGroupExpand(groupTag, isExpand) + if err != nil { + return writeError(conn, err) } } return writeError(conn, nil) diff --git a/experimental/libbox/command_urltest.go b/experimental/libbox/command_urltest.go index 88e86a8f8c..3563d8c6a3 100644 --- a/experimental/libbox/command_urltest.go +++ b/experimental/libbox/command_urltest.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/service" ) func (c *CommandClient) URLTest(groupTag string) error { @@ -37,11 +38,11 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return nil } - abstractOutboundGroup, isLoaded := service.instance.Router().Outbound(groupTag) + abstractOutboundGroup, isLoaded := serviceNow.instance.Router().Outbound(groupTag) if !isLoaded { return writeError(conn, E.New("outbound group not found: ", groupTag)) } @@ -53,15 +54,9 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if isURLTest { go urlTest.CheckOutbounds() } else { - var historyStorage *urltest.HistoryStorage - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - historyStorage = clashServer.HistoryStorage() - } else { - return writeError(conn, E.New("Clash API is required for URLTest on non-URLTest group")) - } - + historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx) outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { - itOutbound, _ := service.instance.Router().Outbound(it) + itOutbound, _ := serviceNow.instance.Router().Outbound(it) return itOutbound }), func(it adapter.Outbound) bool { if it == nil { @@ -73,12 +68,12 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { } return true }) - b, _ := batch.New(service.ctx, batch.WithConcurrencyNum[any](10)) + b, _ := batch.New(serviceNow.ctx, batch.WithConcurrencyNum[any](10)) for _, detour := range outbounds { outboundToTest := detour outboundTag := outboundToTest.Tag() b.Go(outboundTag, func() (any, error) { - t, err := urltest.URLTest(service.ctx, "", outboundToTest) + t, err := urltest.URLTest(serviceNow.ctx, "", outboundToTest) if err != nil { historyStorage.DeleteURLTestHistory(outboundTag) } else { diff --git a/option/clash.go b/option/clash.go deleted file mode 100644 index 63ee2aebdf..0000000000 --- a/option/clash.go +++ /dev/null @@ -1,31 +0,0 @@ -package option - -type ClashAPIOptions struct { - ExternalController string `json:"external_controller,omitempty"` - ExternalUI string `json:"external_ui,omitempty"` - ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` - ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` - Secret string `json:"secret,omitempty"` - DefaultMode string `json:"default_mode,omitempty"` - StoreMode bool `json:"store_mode,omitempty"` - StoreSelected bool `json:"store_selected,omitempty"` - StoreFakeIP bool `json:"store_fakeip,omitempty"` - CacheFile string `json:"cache_file,omitempty"` - CacheID string `json:"cache_id,omitempty"` - - ModeList []string `json:"-"` -} - -type SelectorOutboundOptions struct { - Outbounds []string `json:"outbounds"` - Default string `json:"default,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} - -type URLTestOutboundOptions struct { - Outbounds []string `json:"outbounds"` - URL string `json:"url,omitempty"` - Interval Duration `json:"interval,omitempty"` - Tolerance uint16 `json:"tolerance,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} diff --git a/option/experimental.go b/option/experimental.go index a5b6acbd32..72751a590e 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -1,7 +1,48 @@ package option type ExperimentalOptions struct { - ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` - V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` - Debug *DebugOptions `json:"debug,omitempty"` + CacheFile *CacheFileOptions `json:"cache_file,omitempty"` + ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` + V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` + Debug *DebugOptions `json:"debug,omitempty"` +} + +type CacheFileOptions struct { + Enabled bool `json:"enabled,omitempty"` + Path string `json:"path,omitempty"` + CacheID string `json:"cache_id,omitempty"` + StoreFakeIP bool `json:"store_fakeip,omitempty"` +} + +type ClashAPIOptions struct { + ExternalController string `json:"external_controller,omitempty"` + ExternalUI string `json:"external_ui,omitempty"` + ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` + ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` + Secret string `json:"secret,omitempty"` + DefaultMode string `json:"default_mode,omitempty"` + ModeList []string `json:"-"` + + // Deprecated: migrated to global cache file + StoreMode bool `json:"store_mode,omitempty"` + // Deprecated: migrated to global cache file + StoreSelected bool `json:"store_selected,omitempty"` + // Deprecated: migrated to global cache file + StoreFakeIP bool `json:"store_fakeip,omitempty"` + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` +} + +type V2RayAPIOptions struct { + Listen string `json:"listen,omitempty"` + Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` +} + +type V2RayStatsServiceOptions struct { + Enabled bool `json:"enabled,omitempty"` + Inbounds []string `json:"inbounds,omitempty"` + Outbounds []string `json:"outbounds,omitempty"` + Users []string `json:"users,omitempty"` } diff --git a/option/group.go b/option/group.go new file mode 100644 index 0000000000..58824e808b --- /dev/null +++ b/option/group.go @@ -0,0 +1,15 @@ +package option + +type SelectorOutboundOptions struct { + Outbounds []string `json:"outbounds"` + Default string `json:"default,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} + +type URLTestOutboundOptions struct { + Outbounds []string `json:"outbounds"` + URL string `json:"url,omitempty"` + Interval Duration `json:"interval,omitempty"` + Tolerance uint16 `json:"tolerance,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} diff --git a/option/v2ray.go b/option/v2ray.go index 37f7b8c4b0..774a651d1d 100644 --- a/option/v2ray.go +++ b/option/v2ray.go @@ -1,13 +1 @@ package option - -type V2RayAPIOptions struct { - Listen string `json:"listen,omitempty"` - Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` -} - -type V2RayStatsServiceOptions struct { - Enabled bool `json:"enabled,omitempty"` - Inbounds []string `json:"inbounds,omitempty"` - Outbounds []string `json:"outbounds,omitempty"` - Users []string `json:"users,omitempty"` -} diff --git a/outbound/builder.go b/outbound/builder.go index 141758d870..e4d6a80e06 100644 --- a/outbound/builder.go +++ b/outbound/builder.go @@ -56,7 +56,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t case C.TypeHysteria2: return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options) case C.TypeSelector: - return NewSelector(router, logger, tag, options.SelectorOptions) + return NewSelector(ctx, router, logger, tag, options.SelectorOptions) case C.TypeURLTest: return NewURLTest(ctx, router, logger, tag, options.URLTestOptions) default: diff --git a/outbound/selector.go b/outbound/selector.go index c66591cdee..e801daeadc 100644 --- a/outbound/selector.go +++ b/outbound/selector.go @@ -12,6 +12,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" ) var ( @@ -21,6 +22,7 @@ var ( type Selector struct { myOutboundAdapter + ctx context.Context tags []string defaultTag string outbounds map[string]adapter.Outbound @@ -29,7 +31,7 @@ type Selector struct { interruptExternalConnections bool } -func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { +func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { outbound := &Selector{ myOutboundAdapter: myOutboundAdapter{ protocol: C.TypeSelector, @@ -38,6 +40,7 @@ func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, op tag: tag, dependencies: options.Outbounds, }, + ctx: ctx, tags: options.Outbounds, defaultTag: options.Default, outbounds: make(map[string]adapter.Outbound), @@ -67,8 +70,9 @@ func (s *Selector) Start() error { } if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - selected := clashServer.CacheFile().LoadSelected(s.tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + selected := cacheFile.LoadSelected(s.tag) if selected != "" { detour, loaded := s.outbounds[selected] if loaded { @@ -110,8 +114,9 @@ func (s *Selector) SelectOutbound(tag string) bool { } s.selected = detour if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - err := clashServer.CacheFile().StoreSelected(s.tag, tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreSelected(s.tag, tag) if err != nil { s.logger.Error("store selected: ", err) } diff --git a/route/router.go b/route/router.go index 972c4ec048..e50a51a955 100644 --- a/route/router.go +++ b/route/router.go @@ -261,7 +261,7 @@ func NewRouter( if fakeIPOptions.Inet6Range != nil { inet6Range = *fakeIPOptions.Inet6Range } - router.fakeIPStore = fakeip.NewStore(router, router.logger, inet4Range, inet6Range) + router.fakeIPStore = fakeip.NewStore(ctx, router.logger, inet4Range, inet6Range) } usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor() diff --git a/transport/fakeip/store.go b/transport/fakeip/store.go index 96f6bf031e..83677b0d05 100644 --- a/transport/fakeip/store.go +++ b/transport/fakeip/store.go @@ -1,17 +1,19 @@ package fakeip import ( + "context" "net/netip" "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" ) var _ adapter.FakeIPStore = (*Store)(nil) type Store struct { - router adapter.Router + ctx context.Context logger logger.Logger inet4Range netip.Prefix inet6Range netip.Prefix @@ -20,9 +22,9 @@ type Store struct { inet6Current netip.Addr } -func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { +func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { return &Store{ - router: router, + ctx: ctx, logger: logger, inet4Range: inet4Range, inet6Range: inet6Range, @@ -31,10 +33,9 @@ func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Pref func (s *Store) Start() error { var storage adapter.FakeIPStorage - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreFakeIP() { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - storage = cacheFile - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil && cacheFile.StoreFakeIP() { + storage = cacheFile } if storage == nil { storage = NewMemoryStorage() From 927865ebfacdb809dda90cf114f5c3f7431e602b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Nov 2023 23:47:32 +0800 Subject: [PATCH 015/103] Allow nested logical rules --- experimental/clashapi.go | 42 ++++++++++++++++++++++------------- option/rule.go | 21 +++++++++++++----- option/rule_dns.go | 25 +++++++++++++++------ route/router_geo_resources.go | 12 ++++------ route/rule_default.go | 2 +- route/rule_dns.go | 2 +- 6 files changed, 67 insertions(+), 37 deletions(-) diff --git a/experimental/clashapi.go b/experimental/clashapi.go index 894d40a74a..8f2e18fe82 100644 --- a/experimental/clashapi.go +++ b/experimental/clashapi.go @@ -5,6 +5,7 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" @@ -27,24 +28,35 @@ func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.O func CalculateClashModeList(options option.Options) []string { var clashMode []string - for _, dnsRule := range common.PtrValueOrDefault(options.DNS).Rules { - if dnsRule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, dnsRule.DefaultOptions.ClashMode) { - clashMode = append(clashMode, dnsRule.DefaultOptions.ClashMode) - } - for _, defaultRule := range dnsRule.LogicalOptions.Rules { - if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) { - clashMode = append(clashMode, defaultRule.ClashMode) - } - } - } - for _, rule := range common.PtrValueOrDefault(options.Route).Rules { - if rule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, rule.DefaultOptions.ClashMode) { + clashMode = append(clashMode, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...) + clashMode = append(clashMode, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...) + clashMode = common.Uniq(clashMode) + return clashMode +} + +func extraClashModeFromRule(rules []option.Rule) []string { + var clashMode []string + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: clashMode = append(clashMode, rule.DefaultOptions.ClashMode) + case C.RuleTypeLogical: + clashMode = append(clashMode, extraClashModeFromRule(rule.LogicalOptions.Rules)...) } - for _, defaultRule := range rule.LogicalOptions.Rules { - if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) { - clashMode = append(clashMode, defaultRule.ClashMode) + } + return clashMode +} + +func extraClashModeFromDNSRule(rules []option.DNSRule) []string { + var clashMode []string + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if rule.DefaultOptions.ClashMode != "" { + clashMode = append(clashMode, rule.DefaultOptions.ClashMode) } + case C.RuleTypeLogical: + clashMode = append(clashMode, extraClashModeFromDNSRule(rule.LogicalOptions.Rules)...) } } return clashMode diff --git a/option/rule.go b/option/rule.go index 8caba96e57..4f4042025f 100644 --- a/option/rule.go +++ b/option/rule.go @@ -53,6 +53,17 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error { return nil } +func (r Rule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + type DefaultRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` @@ -92,12 +103,12 @@ func (r DefaultRule) IsValid() bool { } type LogicalRule struct { - Mode string `json:"mode"` - Rules []DefaultRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Mode string `json:"mode"` + Rules []Rule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` } func (r LogicalRule) IsValid() bool { - return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid) + return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid) } diff --git a/option/rule_dns.go b/option/rule_dns.go index ba572b9aa2..fca3432293 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -53,6 +53,17 @@ func (r *DNSRule) UnmarshalJSON(bytes []byte) error { return nil } +func (r DNSRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown DNS rule type: " + r.Type) + } +} + type DefaultDNSRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` @@ -96,14 +107,14 @@ func (r DefaultDNSRule) IsValid() bool { } type LogicalDNSRule struct { - Mode string `json:"mode"` - Rules []DefaultDNSRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + Mode string `json:"mode"` + Rules []DNSRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Server string `json:"server,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` } func (r LogicalDNSRule) IsValid() bool { - return len(r.Rules) > 0 && common.All(r.Rules, DefaultDNSRule.IsValid) + return len(r.Rules) > 0 && common.All(r.Rules, DNSRule.IsValid) } diff --git a/route/router_geo_resources.go b/route/router_geo_resources.go index 8715cf922c..638d00df62 100644 --- a/route/router_geo_resources.go +++ b/route/router_geo_resources.go @@ -252,10 +252,8 @@ func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool return true } case C.RuleTypeLogical: - for _, subRule := range rule.LogicalOptions.Rules { - if cond(subRule) { - return true - } + if hasRule(rule.LogicalOptions.Rules, cond) { + return true } } } @@ -270,10 +268,8 @@ func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bo return true } case C.RuleTypeLogical: - for _, subRule := range rule.LogicalOptions.Rules { - if cond(subRule) { - return true - } + if hasDNSRule(rule.LogicalOptions.Rules, cond) { + return true } } } diff --git a/route/rule_default.go b/route/rule_default.go index 2d62f97a46..758aa34704 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -220,7 +220,7 @@ func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options opt return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDefaultRule(router, logger, subRule) + rule, err := NewRule(router, logger, subRule) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } diff --git a/route/rule_dns.go b/route/rule_dns.go index 5132f02456..934c45c622 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -228,7 +228,7 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDefaultDNSRule(router, logger, subRule) + rule, err := NewDNSRule(router, logger, subRule) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } From 5fd9422a47e3cf992fbbcbcabf701c43ef51f468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Nov 2023 12:00:28 +0800 Subject: [PATCH 016/103] Migrate to independent cache file --- adapter/experimental.go | 10 ++- box.go | 12 ++- .../{clashapi => }/cachefile/cache.go | 85 +++++++++++++------ .../{clashapi => }/cachefile/fakeip.go | 0 experimental/clashapi/cache.go | 11 ++- experimental/clashapi/server.go | 66 ++++---------- experimental/libbox/command_group.go | 21 ++--- experimental/libbox/command_urltest.go | 21 ++--- option/clash.go | 31 ------- option/experimental.go | 47 +++++++++- option/group.go | 15 ++++ option/v2ray.go | 12 --- outbound/builder.go | 2 +- outbound/selector.go | 15 ++-- route/router.go | 2 +- transport/fakeip/store.go | 15 ++-- 16 files changed, 194 insertions(+), 171 deletions(-) rename experimental/{clashapi => }/cachefile/cache.go (81%) rename experimental/{clashapi => }/cachefile/fakeip.go (100%) delete mode 100644 option/clash.go create mode 100644 option/group.go diff --git a/adapter/experimental.go b/adapter/experimental.go index 697fa9f199..ac523e4e74 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -13,15 +13,17 @@ type ClashServer interface { PreStarter Mode() string ModeList() []string - StoreSelected() bool - StoreFakeIP() bool - CacheFile() ClashCacheFile HistoryStorage() *urltest.HistoryStorage RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker) } -type ClashCacheFile interface { +type CacheFile interface { + Service + PreStarter + + StoreFakeIP() bool + LoadMode() string StoreMode(mode string) error LoadSelected(group string) string diff --git a/box.go b/box.go index c10222ddf7..ae61bffe7c 100644 --- a/box.go +++ b/box.go @@ -10,6 +10,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/inbound" "github.com/sagernet/sing-box/log" @@ -45,17 +46,21 @@ type Options struct { } func New(options Options) (*Box, error) { + createdAt := time.Now() ctx := options.Context if ctx == nil { ctx = context.Background() } ctx = service.ContextWithDefaultRegistry(ctx) ctx = pause.ContextWithDefaultManager(ctx) - createdAt := time.Now() experimentalOptions := common.PtrValueOrDefault(options.Experimental) applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) + var needCacheFile bool var needClashAPI bool var needV2RayAPI bool + if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil { + needCacheFile = true + } if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil { needClashAPI = true } @@ -147,6 +152,11 @@ func New(options Options) (*Box, error) { } preServices := make(map[string]adapter.Service) postServices := make(map[string]adapter.Service) + if needCacheFile { + cacheFile := cachefile.NewCacheFile(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) + preServices["cache file"] = cacheFile + service.MustRegister[adapter.CacheFile](ctx, cacheFile) + } if needClashAPI { clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options) diff --git a/experimental/clashapi/cachefile/cache.go b/experimental/cachefile/cache.go similarity index 81% rename from experimental/clashapi/cachefile/cache.go rename to experimental/cachefile/cache.go index 0911829749..89d238822a 100644 --- a/experimental/clashapi/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/bbolt" bboltErrors "github.com/sagernet/bbolt/errors" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service/filemanager" @@ -31,11 +32,15 @@ var ( cacheIDDefault = []byte("default") ) -var _ adapter.ClashCacheFile = (*CacheFile)(nil) +var _ adapter.CacheFile = (*CacheFile)(nil) type CacheFile struct { + ctx context.Context + path string + cacheID []byte + storeFakeIP bool + DB *bbolt.DB - cacheID []byte saveAccess sync.RWMutex saveDomain map[netip.Addr]string saveAddress4 map[string]netip.Addr @@ -43,7 +48,29 @@ type CacheFile struct { saveMetadataTimer *time.Timer } -func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) { +func NewCacheFile(ctx context.Context, options option.CacheFileOptions) *CacheFile { + var path string + if options.Path != "" { + path = filemanager.BasePath(ctx, options.Path) + } else { + path = "cache.db" + } + var cacheIDBytes []byte + if options.CacheID != "" { + cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...) + } + return &CacheFile{ + ctx: ctx, + path: path, + cacheID: cacheIDBytes, + storeFakeIP: options.StoreFakeIP, + saveDomain: make(map[netip.Addr]string), + saveAddress4: make(map[string]netip.Addr), + saveAddress6: make(map[string]netip.Addr), + } +} + +func (c *CacheFile) start() error { const fileMode = 0o666 options := bbolt.Options{Timeout: time.Second} var ( @@ -51,7 +78,7 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) err error ) for i := 0; i < 10; i++ { - db, err = bbolt.Open(path, fileMode, &options) + db, err = bbolt.Open(c.path, fileMode, &options) if err == nil { break } @@ -59,23 +86,20 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) continue } if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) { - rmErr := os.Remove(path) + rmErr := os.Remove(c.path) if rmErr != nil { - return nil, err + return err } } time.Sleep(100 * time.Millisecond) } if err != nil { - return nil, err + return err } - err = filemanager.Chown(ctx, path) + err = filemanager.Chown(c.ctx, c.path) if err != nil { - return nil, E.Cause(err, "platform chown") - } - var cacheIDBytes []byte - if cacheID != "" { - cacheIDBytes = append([]byte{0}, []byte(cacheID)...) + db.Close() + return E.Cause(err, "platform chown") } err = db.Batch(func(tx *bbolt.Tx) error { return tx.ForEach(func(name []byte, b *bbolt.Bucket) error { @@ -97,15 +121,30 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) }) }) if err != nil { - return nil, err + db.Close() + return err } - return &CacheFile{ - DB: db, - cacheID: cacheIDBytes, - saveDomain: make(map[netip.Addr]string), - saveAddress4: make(map[string]netip.Addr), - saveAddress6: make(map[string]netip.Addr), - }, nil + c.DB = db + return nil +} + +func (c *CacheFile) PreStart() error { + return c.start() +} + +func (c *CacheFile) Start() error { + return nil +} + +func (c *CacheFile) Close() error { + if c.DB == nil { + return nil + } + return c.DB.Close() +} + +func (c *CacheFile) StoreFakeIP() bool { + return c.storeFakeIP } func (c *CacheFile) LoadMode() string { @@ -218,7 +257,3 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { } }) } - -func (c *CacheFile) Close() error { - return c.DB.Close() -} diff --git a/experimental/clashapi/cachefile/fakeip.go b/experimental/cachefile/fakeip.go similarity index 100% rename from experimental/clashapi/cachefile/fakeip.go rename to experimental/cachefile/fakeip.go diff --git a/experimental/clashapi/cache.go b/experimental/clashapi/cache.go index 7582fde5ae..9c088a82f7 100644 --- a/experimental/clashapi/cache.go +++ b/experimental/clashapi/cache.go @@ -1,23 +1,26 @@ package clashapi import ( + "context" "net/http" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/service" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) -func cacheRouter(router adapter.Router) http.Handler { +func cacheRouter(ctx context.Context) http.Handler { r := chi.NewRouter() - r.Post("/fakeip/flush", flushFakeip(router)) + r.Post("/fakeip/flush", flushFakeip(ctx)) return r } -func flushFakeip(router adapter.Router) func(w http.ResponseWriter, r *http.Request) { +func flushFakeip(ctx context.Context) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if cacheFile := router.ClashServer().CacheFile(); cacheFile != nil { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile != nil { err := cacheFile.FakeIPReset() if err != nil { render.Status(r, http.StatusInternalServerError) diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 6a3d6f66f7..c40ff93846 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -15,7 +15,6 @@ import ( "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" - "github.com/sagernet/sing-box/experimental/clashapi/cachefile" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -49,12 +48,6 @@ type Server struct { mode string modeList []string modeUpdateHook chan<- struct{} - storeMode bool - storeSelected bool - storeFakeIP bool - cacheFilePath string - cacheID string - cacheFile adapter.ClashCacheFile externalController bool externalUI string @@ -76,9 +69,6 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ trafficManager: trafficManager, modeList: options.ModeList, externalController: options.ExternalController != "", - storeMode: options.StoreMode, - storeSelected: options.StoreSelected, - storeFakeIP: options.StoreFakeIP, externalUIDownloadURL: options.ExternalUIDownloadURL, externalUIDownloadDetour: options.ExternalUIDownloadDetour, } @@ -94,18 +84,10 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ server.modeList = append([]string{defaultMode}, server.modeList...) } server.mode = defaultMode - if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.ExternalController == "" { - cachePath := os.ExpandEnv(options.CacheFile) - if cachePath == "" { - cachePath = "cache.db" - } - if foundPath, loaded := C.FindPath(cachePath); loaded { - cachePath = foundPath - } else { - cachePath = filemanager.BasePath(ctx, cachePath) - } - server.cacheFilePath = cachePath - server.cacheID = options.CacheID + //goland:noinspection GoDeprecation + //nolint:staticcheck + if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" { + return nil, E.New("cache_file and related fields in Clash API is deprecated in sing-box 1.8.0, use experimental.cache_file instead.") } cors := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, @@ -128,7 +110,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/script", scriptRouter()) r.Mount("/profile", profileRouter()) - r.Mount("/cache", cacheRouter(router)) + r.Mount("/cache", cacheRouter(ctx)) r.Mount("/dns", dnsRouter(router)) server.setupMetaAPI(r) @@ -147,19 +129,13 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ } func (s *Server) PreStart() error { - if s.cacheFilePath != "" { - cacheFile, err := cachefile.Open(s.ctx, s.cacheFilePath, s.cacheID) - if err != nil { - return E.Cause(err, "open cache file") - } - s.cacheFile = cacheFile - if s.storeMode { - mode := s.cacheFile.LoadMode() - if common.Any(s.modeList, func(it string) bool { - return strings.EqualFold(it, mode) - }) { - s.mode = mode - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + mode := cacheFile.LoadMode() + if common.Any(s.modeList, func(it string) bool { + return strings.EqualFold(it, mode) + }) { + s.mode = mode } } return nil @@ -187,7 +163,6 @@ func (s *Server) Close() error { return common.Close( common.PtrOrNil(s.httpServer), s.trafficManager, - s.cacheFile, s.urlTestHistory, ) } @@ -224,8 +199,9 @@ func (s *Server) SetMode(newMode string) { } } s.router.ClearDNSCache() - if s.storeMode { - err := s.cacheFile.StoreMode(newMode) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreMode(newMode) if err != nil { s.logger.Error(E.Cause(err, "save mode")) } @@ -233,18 +209,6 @@ func (s *Server) SetMode(newMode string) { s.logger.Info("updated mode: ", newMode) } -func (s *Server) StoreSelected() bool { - return s.storeSelected -} - -func (s *Server) StoreFakeIP() bool { - return s.storeFakeIP -} - -func (s *Server) CacheFile() adapter.ClashCacheFile { - return s.cacheFile -} - func (s *Server) HistoryStorage() *urltest.HistoryStorage { return s.urlTestHistory } diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go index 934820889e..2fc69b98b4 100644 --- a/experimental/libbox/command_group.go +++ b/experimental/libbox/command_group.go @@ -159,11 +159,7 @@ func readGroups(reader io.Reader) (OutboundGroupIterator, error) { func writeGroups(writer io.Writer, boxService *BoxService) error { historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx) - var cacheFile adapter.ClashCacheFile - if clashServer := boxService.instance.Router().ClashServer(); clashServer != nil { - cacheFile = clashServer.CacheFile() - } - + cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx) outbounds := boxService.instance.Router().Outbounds() var iGroups []adapter.OutboundGroup for _, it := range outbounds { @@ -288,16 +284,15 @@ func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return writeError(conn, E.New("service not ready")) } - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - err = cacheFile.StoreGroupExpand(groupTag, isExpand) - if err != nil { - return writeError(conn, err) - } + cacheFile := service.FromContext[adapter.CacheFile](serviceNow.ctx) + if cacheFile != nil { + err = cacheFile.StoreGroupExpand(groupTag, isExpand) + if err != nil { + return writeError(conn, err) } } return writeError(conn, nil) diff --git a/experimental/libbox/command_urltest.go b/experimental/libbox/command_urltest.go index 88e86a8f8c..3563d8c6a3 100644 --- a/experimental/libbox/command_urltest.go +++ b/experimental/libbox/command_urltest.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/service" ) func (c *CommandClient) URLTest(groupTag string) error { @@ -37,11 +38,11 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return nil } - abstractOutboundGroup, isLoaded := service.instance.Router().Outbound(groupTag) + abstractOutboundGroup, isLoaded := serviceNow.instance.Router().Outbound(groupTag) if !isLoaded { return writeError(conn, E.New("outbound group not found: ", groupTag)) } @@ -53,15 +54,9 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if isURLTest { go urlTest.CheckOutbounds() } else { - var historyStorage *urltest.HistoryStorage - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - historyStorage = clashServer.HistoryStorage() - } else { - return writeError(conn, E.New("Clash API is required for URLTest on non-URLTest group")) - } - + historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx) outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { - itOutbound, _ := service.instance.Router().Outbound(it) + itOutbound, _ := serviceNow.instance.Router().Outbound(it) return itOutbound }), func(it adapter.Outbound) bool { if it == nil { @@ -73,12 +68,12 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { } return true }) - b, _ := batch.New(service.ctx, batch.WithConcurrencyNum[any](10)) + b, _ := batch.New(serviceNow.ctx, batch.WithConcurrencyNum[any](10)) for _, detour := range outbounds { outboundToTest := detour outboundTag := outboundToTest.Tag() b.Go(outboundTag, func() (any, error) { - t, err := urltest.URLTest(service.ctx, "", outboundToTest) + t, err := urltest.URLTest(serviceNow.ctx, "", outboundToTest) if err != nil { historyStorage.DeleteURLTestHistory(outboundTag) } else { diff --git a/option/clash.go b/option/clash.go deleted file mode 100644 index 63ee2aebdf..0000000000 --- a/option/clash.go +++ /dev/null @@ -1,31 +0,0 @@ -package option - -type ClashAPIOptions struct { - ExternalController string `json:"external_controller,omitempty"` - ExternalUI string `json:"external_ui,omitempty"` - ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` - ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` - Secret string `json:"secret,omitempty"` - DefaultMode string `json:"default_mode,omitempty"` - StoreMode bool `json:"store_mode,omitempty"` - StoreSelected bool `json:"store_selected,omitempty"` - StoreFakeIP bool `json:"store_fakeip,omitempty"` - CacheFile string `json:"cache_file,omitempty"` - CacheID string `json:"cache_id,omitempty"` - - ModeList []string `json:"-"` -} - -type SelectorOutboundOptions struct { - Outbounds []string `json:"outbounds"` - Default string `json:"default,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} - -type URLTestOutboundOptions struct { - Outbounds []string `json:"outbounds"` - URL string `json:"url,omitempty"` - Interval Duration `json:"interval,omitempty"` - Tolerance uint16 `json:"tolerance,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} diff --git a/option/experimental.go b/option/experimental.go index a5b6acbd32..72751a590e 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -1,7 +1,48 @@ package option type ExperimentalOptions struct { - ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` - V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` - Debug *DebugOptions `json:"debug,omitempty"` + CacheFile *CacheFileOptions `json:"cache_file,omitempty"` + ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` + V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` + Debug *DebugOptions `json:"debug,omitempty"` +} + +type CacheFileOptions struct { + Enabled bool `json:"enabled,omitempty"` + Path string `json:"path,omitempty"` + CacheID string `json:"cache_id,omitempty"` + StoreFakeIP bool `json:"store_fakeip,omitempty"` +} + +type ClashAPIOptions struct { + ExternalController string `json:"external_controller,omitempty"` + ExternalUI string `json:"external_ui,omitempty"` + ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` + ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` + Secret string `json:"secret,omitempty"` + DefaultMode string `json:"default_mode,omitempty"` + ModeList []string `json:"-"` + + // Deprecated: migrated to global cache file + StoreMode bool `json:"store_mode,omitempty"` + // Deprecated: migrated to global cache file + StoreSelected bool `json:"store_selected,omitempty"` + // Deprecated: migrated to global cache file + StoreFakeIP bool `json:"store_fakeip,omitempty"` + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` +} + +type V2RayAPIOptions struct { + Listen string `json:"listen,omitempty"` + Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` +} + +type V2RayStatsServiceOptions struct { + Enabled bool `json:"enabled,omitempty"` + Inbounds []string `json:"inbounds,omitempty"` + Outbounds []string `json:"outbounds,omitempty"` + Users []string `json:"users,omitempty"` } diff --git a/option/group.go b/option/group.go new file mode 100644 index 0000000000..58824e808b --- /dev/null +++ b/option/group.go @@ -0,0 +1,15 @@ +package option + +type SelectorOutboundOptions struct { + Outbounds []string `json:"outbounds"` + Default string `json:"default,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} + +type URLTestOutboundOptions struct { + Outbounds []string `json:"outbounds"` + URL string `json:"url,omitempty"` + Interval Duration `json:"interval,omitempty"` + Tolerance uint16 `json:"tolerance,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} diff --git a/option/v2ray.go b/option/v2ray.go index 37f7b8c4b0..774a651d1d 100644 --- a/option/v2ray.go +++ b/option/v2ray.go @@ -1,13 +1 @@ package option - -type V2RayAPIOptions struct { - Listen string `json:"listen,omitempty"` - Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` -} - -type V2RayStatsServiceOptions struct { - Enabled bool `json:"enabled,omitempty"` - Inbounds []string `json:"inbounds,omitempty"` - Outbounds []string `json:"outbounds,omitempty"` - Users []string `json:"users,omitempty"` -} diff --git a/outbound/builder.go b/outbound/builder.go index 141758d870..e4d6a80e06 100644 --- a/outbound/builder.go +++ b/outbound/builder.go @@ -56,7 +56,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t case C.TypeHysteria2: return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options) case C.TypeSelector: - return NewSelector(router, logger, tag, options.SelectorOptions) + return NewSelector(ctx, router, logger, tag, options.SelectorOptions) case C.TypeURLTest: return NewURLTest(ctx, router, logger, tag, options.URLTestOptions) default: diff --git a/outbound/selector.go b/outbound/selector.go index c66591cdee..e801daeadc 100644 --- a/outbound/selector.go +++ b/outbound/selector.go @@ -12,6 +12,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" ) var ( @@ -21,6 +22,7 @@ var ( type Selector struct { myOutboundAdapter + ctx context.Context tags []string defaultTag string outbounds map[string]adapter.Outbound @@ -29,7 +31,7 @@ type Selector struct { interruptExternalConnections bool } -func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { +func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { outbound := &Selector{ myOutboundAdapter: myOutboundAdapter{ protocol: C.TypeSelector, @@ -38,6 +40,7 @@ func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, op tag: tag, dependencies: options.Outbounds, }, + ctx: ctx, tags: options.Outbounds, defaultTag: options.Default, outbounds: make(map[string]adapter.Outbound), @@ -67,8 +70,9 @@ func (s *Selector) Start() error { } if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - selected := clashServer.CacheFile().LoadSelected(s.tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + selected := cacheFile.LoadSelected(s.tag) if selected != "" { detour, loaded := s.outbounds[selected] if loaded { @@ -110,8 +114,9 @@ func (s *Selector) SelectOutbound(tag string) bool { } s.selected = detour if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - err := clashServer.CacheFile().StoreSelected(s.tag, tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreSelected(s.tag, tag) if err != nil { s.logger.Error("store selected: ", err) } diff --git a/route/router.go b/route/router.go index 972c4ec048..e50a51a955 100644 --- a/route/router.go +++ b/route/router.go @@ -261,7 +261,7 @@ func NewRouter( if fakeIPOptions.Inet6Range != nil { inet6Range = *fakeIPOptions.Inet6Range } - router.fakeIPStore = fakeip.NewStore(router, router.logger, inet4Range, inet6Range) + router.fakeIPStore = fakeip.NewStore(ctx, router.logger, inet4Range, inet6Range) } usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor() diff --git a/transport/fakeip/store.go b/transport/fakeip/store.go index 96f6bf031e..83677b0d05 100644 --- a/transport/fakeip/store.go +++ b/transport/fakeip/store.go @@ -1,17 +1,19 @@ package fakeip import ( + "context" "net/netip" "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" ) var _ adapter.FakeIPStore = (*Store)(nil) type Store struct { - router adapter.Router + ctx context.Context logger logger.Logger inet4Range netip.Prefix inet6Range netip.Prefix @@ -20,9 +22,9 @@ type Store struct { inet6Current netip.Addr } -func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { +func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { return &Store{ - router: router, + ctx: ctx, logger: logger, inet4Range: inet4Range, inet6Range: inet6Range, @@ -31,10 +33,9 @@ func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Pref func (s *Store) Start() error { var storage adapter.FakeIPStorage - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreFakeIP() { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - storage = cacheFile - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil && cacheFile.StoreFakeIP() { + storage = cacheFile } if storage == nil { storage = NewMemoryStorage() From d26cbfb570a65b464b37ba86463c606e8b270e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Nov 2023 23:47:32 +0800 Subject: [PATCH 017/103] Allow nested logical rules --- experimental/clashapi.go | 42 ++++++++++++++++++++++------------- option/rule.go | 21 +++++++++++++----- option/rule_dns.go | 25 +++++++++++++++------ route/router.go | 4 ++-- route/router_geo_resources.go | 12 ++++------ route/rule_default.go | 8 +++---- route/rule_dns.go | 8 +++---- 7 files changed, 75 insertions(+), 45 deletions(-) diff --git a/experimental/clashapi.go b/experimental/clashapi.go index 894d40a74a..8f2e18fe82 100644 --- a/experimental/clashapi.go +++ b/experimental/clashapi.go @@ -5,6 +5,7 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" @@ -27,24 +28,35 @@ func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.O func CalculateClashModeList(options option.Options) []string { var clashMode []string - for _, dnsRule := range common.PtrValueOrDefault(options.DNS).Rules { - if dnsRule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, dnsRule.DefaultOptions.ClashMode) { - clashMode = append(clashMode, dnsRule.DefaultOptions.ClashMode) - } - for _, defaultRule := range dnsRule.LogicalOptions.Rules { - if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) { - clashMode = append(clashMode, defaultRule.ClashMode) - } - } - } - for _, rule := range common.PtrValueOrDefault(options.Route).Rules { - if rule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, rule.DefaultOptions.ClashMode) { + clashMode = append(clashMode, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...) + clashMode = append(clashMode, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...) + clashMode = common.Uniq(clashMode) + return clashMode +} + +func extraClashModeFromRule(rules []option.Rule) []string { + var clashMode []string + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: clashMode = append(clashMode, rule.DefaultOptions.ClashMode) + case C.RuleTypeLogical: + clashMode = append(clashMode, extraClashModeFromRule(rule.LogicalOptions.Rules)...) } - for _, defaultRule := range rule.LogicalOptions.Rules { - if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) { - clashMode = append(clashMode, defaultRule.ClashMode) + } + return clashMode +} + +func extraClashModeFromDNSRule(rules []option.DNSRule) []string { + var clashMode []string + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if rule.DefaultOptions.ClashMode != "" { + clashMode = append(clashMode, rule.DefaultOptions.ClashMode) } + case C.RuleTypeLogical: + clashMode = append(clashMode, extraClashModeFromDNSRule(rule.LogicalOptions.Rules)...) } } return clashMode diff --git a/option/rule.go b/option/rule.go index 8caba96e57..4f4042025f 100644 --- a/option/rule.go +++ b/option/rule.go @@ -53,6 +53,17 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error { return nil } +func (r Rule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + type DefaultRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` @@ -92,12 +103,12 @@ func (r DefaultRule) IsValid() bool { } type LogicalRule struct { - Mode string `json:"mode"` - Rules []DefaultRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Mode string `json:"mode"` + Rules []Rule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` } func (r LogicalRule) IsValid() bool { - return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid) + return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid) } diff --git a/option/rule_dns.go b/option/rule_dns.go index ba572b9aa2..fca3432293 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -53,6 +53,17 @@ func (r *DNSRule) UnmarshalJSON(bytes []byte) error { return nil } +func (r DNSRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown DNS rule type: " + r.Type) + } +} + type DefaultDNSRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` @@ -96,14 +107,14 @@ func (r DefaultDNSRule) IsValid() bool { } type LogicalDNSRule struct { - Mode string `json:"mode"` - Rules []DefaultDNSRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + Mode string `json:"mode"` + Rules []DNSRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Server string `json:"server,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` } func (r LogicalDNSRule) IsValid() bool { - return len(r.Rules) > 0 && common.All(r.Rules, DefaultDNSRule.IsValid) + return len(r.Rules) > 0 && common.All(r.Rules, DNSRule.IsValid) } diff --git a/route/router.go b/route/router.go index e50a51a955..b48d2346d9 100644 --- a/route/router.go +++ b/route/router.go @@ -127,14 +127,14 @@ func NewRouter( Logger: router.dnsLogger, }) for i, ruleOptions := range options.Rules { - routeRule, err := NewRule(router, router.logger, ruleOptions) + routeRule, err := NewRule(router, router.logger, ruleOptions, true) if err != nil { return nil, E.Cause(err, "parse rule[", i, "]") } router.rules = append(router.rules, routeRule) } for i, dnsRuleOptions := range dnsOptions.Rules { - dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions) + dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions, true) if err != nil { return nil, E.Cause(err, "parse dns rule[", i, "]") } diff --git a/route/router_geo_resources.go b/route/router_geo_resources.go index 8715cf922c..638d00df62 100644 --- a/route/router_geo_resources.go +++ b/route/router_geo_resources.go @@ -252,10 +252,8 @@ func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool return true } case C.RuleTypeLogical: - for _, subRule := range rule.LogicalOptions.Rules { - if cond(subRule) { - return true - } + if hasRule(rule.LogicalOptions.Rules, cond) { + return true } } } @@ -270,10 +268,8 @@ func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bo return true } case C.RuleTypeLogical: - for _, subRule := range rule.LogicalOptions.Rules { - if cond(subRule) { - return true - } + if hasDNSRule(rule.LogicalOptions.Rules, cond) { + return true } } } diff --git a/route/rule_default.go b/route/rule_default.go index 2d62f97a46..8c8473abf5 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -8,13 +8,13 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule) (adapter.Rule, error) { +func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule, checkOutbound bool) (adapter.Rule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - if options.DefaultOptions.Outbound == "" { + if options.DefaultOptions.Outbound == "" && checkOutbound { return nil, E.New("missing outbound field") } return NewDefaultRule(router, logger, options.DefaultOptions) @@ -22,7 +22,7 @@ func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rul if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - if options.LogicalOptions.Outbound == "" { + if options.LogicalOptions.Outbound == "" && checkOutbound { return nil, E.New("missing outbound field") } return NewLogicalRule(router, logger, options.LogicalOptions) @@ -220,7 +220,7 @@ func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options opt return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDefaultRule(router, logger, subRule) + rule, err := NewRule(router, logger, subRule, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } diff --git a/route/rule_dns.go b/route/rule_dns.go index 5132f02456..b444932582 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -8,13 +8,13 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule) (adapter.DNSRule, error) { +func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule, checkServer bool) (adapter.DNSRule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - if options.DefaultOptions.Server == "" { + if options.DefaultOptions.Server == "" && checkServer { return nil, E.New("missing server field") } return NewDefaultDNSRule(router, logger, options.DefaultOptions) @@ -22,7 +22,7 @@ func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option. if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - if options.LogicalOptions.Server == "" { + if options.LogicalOptions.Server == "" && checkServer { return nil, E.New("missing server field") } return NewLogicalDNSRule(router, logger, options.LogicalOptions) @@ -228,7 +228,7 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDefaultDNSRule(router, logger, subRule) + rule, err := NewDNSRule(router, logger, subRule, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } From 1bb40934f73c6e788e6f1717c9f868e25c676ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 29 Nov 2023 17:35:40 +0800 Subject: [PATCH 018/103] Add rule set --- .gitignore | 1 + adapter/experimental.go | 66 ++- adapter/inbound.go | 1 + adapter/router.go | 15 +- cmd/sing-box/cmd_format.go | 39 -- cmd/sing-box/cmd_geoip.go | 43 ++ cmd/sing-box/cmd_geoip_export.go | 98 ++++ cmd/sing-box/cmd_geoip_list.go | 31 ++ cmd/sing-box/cmd_geoip_lookup.go | 47 ++ cmd/sing-box/cmd_geosite.go | 41 ++ cmd/sing-box/cmd_geosite_export.go | 81 +++ cmd/sing-box/cmd_geosite_list.go | 50 ++ cmd/sing-box/cmd_geosite_lookup.go | 97 ++++ cmd/sing-box/cmd_geosite_matcher.go | 56 ++ cmd/sing-box/cmd_merge.go | 2 +- cmd/sing-box/cmd_rule_set.go | 14 + cmd/sing-box/cmd_rule_set_compile.go | 80 +++ cmd/sing-box/cmd_rule_set_format.go | 87 ++++ cmd/sing-box/cmd_tools.go | 6 +- cmd/sing-box/cmd_tools_connect.go | 2 +- common/dialer/router.go | 12 +- common/srs/binary.go | 485 ++++++++++++++++++ common/srs/ip_set.go | 116 +++++ constant/rule.go | 8 + experimental/cachefile/cache.go | 35 ++ experimental/cachefile/fakeip.go | 2 +- experimental/clashapi/proxies.go | 6 +- experimental/clashapi/server_resources.go | 6 +- .../clashapi/trafficontrol/tracker.go | 8 +- go.mod | 2 +- go.sum | 4 +- option/experimental.go | 8 +- option/route.go | 1 + option/rule.go | 58 ++- option/rule_dns.go | 1 + option/rule_set.go | 230 +++++++++ option/types.go | 8 + route/router.go | 34 +- route/rule_abstract.go | 22 +- route/rule_default.go | 7 +- route/rule_dns.go | 7 +- route/rule_headless.go | 173 +++++++ route/rule_item_cidr.go | 19 +- route/rule_item_domain.go | 7 + route/rule_item_rule_set.go | 55 ++ route/rule_set.go | 22 + route/rule_set_local.go | 69 +++ route/rule_set_remote.go | 218 ++++++++ 48 files changed, 2375 insertions(+), 105 deletions(-) create mode 100644 cmd/sing-box/cmd_geoip.go create mode 100644 cmd/sing-box/cmd_geoip_export.go create mode 100644 cmd/sing-box/cmd_geoip_list.go create mode 100644 cmd/sing-box/cmd_geoip_lookup.go create mode 100644 cmd/sing-box/cmd_geosite.go create mode 100644 cmd/sing-box/cmd_geosite_export.go create mode 100644 cmd/sing-box/cmd_geosite_list.go create mode 100644 cmd/sing-box/cmd_geosite_lookup.go create mode 100644 cmd/sing-box/cmd_geosite_matcher.go create mode 100644 cmd/sing-box/cmd_rule_set.go create mode 100644 cmd/sing-box/cmd_rule_set_compile.go create mode 100644 cmd/sing-box/cmd_rule_set_format.go create mode 100644 common/srs/binary.go create mode 100644 common/srs/ip_set.go create mode 100644 option/rule_set.go create mode 100644 route/rule_headless.go create mode 100644 route/rule_item_rule_set.go create mode 100644 route/rule_set.go create mode 100644 route/rule_set_local.go create mode 100644 route/rule_set_remote.go diff --git a/.gitignore b/.gitignore index 6630f428ce..55bdab3a0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.idea/ /vendor/ /*.json +/*.srs /*.db /site/ /bin/ diff --git a/adapter/experimental.go b/adapter/experimental.go index ac523e4e74..c26ee8c1cf 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -1,11 +1,16 @@ package adapter import ( + "bytes" "context" + "encoding/binary" + "io" "net" + "time" "github.com/sagernet/sing-box/common/urltest" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" ) type ClashServer interface { @@ -23,6 +28,7 @@ type CacheFile interface { PreStarter StoreFakeIP() bool + FakeIPStorage LoadMode() string StoreMode(mode string) error @@ -30,7 +36,65 @@ type CacheFile interface { StoreSelected(group string, selected string) error LoadGroupExpand(group string) (isExpand bool, loaded bool) StoreGroupExpand(group string, expand bool) error - FakeIPStorage + LoadRuleSet(tag string) *SavedRuleSet + SaveRuleSet(tag string, set *SavedRuleSet) error +} + +type SavedRuleSet struct { + Content []byte + LastUpdated time.Time + LastEtag string +} + +func (s *SavedRuleSet) MarshalBinary() ([]byte, error) { + var buffer bytes.Buffer + err := binary.Write(&buffer, binary.BigEndian, uint8(1)) + if err != nil { + return nil, err + } + err = rw.WriteUVariant(&buffer, uint64(len(s.Content))) + if err != nil { + return nil, err + } + buffer.Write(s.Content) + err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix()) + if err != nil { + return nil, err + } + err = rw.WriteVString(&buffer, s.LastEtag) + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func (s *SavedRuleSet) UnmarshalBinary(data []byte) error { + reader := bytes.NewReader(data) + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return err + } + contentLen, err := rw.ReadUVariant(reader) + if err != nil { + return err + } + s.Content = make([]byte, contentLen) + _, err = io.ReadFull(reader, s.Content) + if err != nil { + return err + } + var lastUpdated int64 + err = binary.Read(reader, binary.BigEndian, &lastUpdated) + if err != nil { + return err + } + s.LastUpdated = time.Unix(lastUpdated, 0) + s.LastEtag, err = rw.ReadVString(reader) + if err != nil { + return err + } + return nil } type Tracker interface { diff --git a/adapter/inbound.go b/adapter/inbound.go index 2d24083c4a..30dec9d1f7 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -47,6 +47,7 @@ type InboundContext struct { GeoIPCode string ProcessInfo *process.Info FakeIP bool + IPCIDRMatchSource bool // dns cache diff --git a/adapter/router.go b/adapter/router.go index e4c3904d51..ca4d6547c4 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -18,7 +18,7 @@ type Router interface { Outbounds() []Outbound Outbound(tag string) (Outbound, bool) - DefaultOutbound(network string) Outbound + DefaultOutbound(network string) (Outbound, error) FakeIPStore() FakeIPStore @@ -27,6 +27,8 @@ type Router interface { GeoIPReader() *geoip.Reader LoadGeosite(code string) (Rule, error) + RuleSet(tag string) (RuleSet, bool) + Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) @@ -61,11 +63,15 @@ func RouterFromContext(ctx context.Context) Router { return service.FromContext[Router](ctx) } +type HeadlessRule interface { + Match(metadata *InboundContext) bool +} + type Rule interface { + HeadlessRule Service Type() string UpdateGeosite() error - Match(metadata *InboundContext) bool Outbound() string String() string } @@ -76,6 +82,11 @@ type DNSRule interface { RewriteTTL() *uint32 } +type RuleSet interface { + HeadlessRule + Service +} + type InterfaceUpdateListener interface { InterfaceUpdated() } diff --git a/cmd/sing-box/cmd_format.go b/cmd/sing-box/cmd_format.go index 10a5497cd4..c5e939e4a9 100644 --- a/cmd/sing-box/cmd_format.go +++ b/cmd/sing-box/cmd_format.go @@ -7,7 +7,6 @@ import ( "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/spf13/cobra" @@ -69,41 +68,3 @@ func format() error { } return nil } - -func formatOne(configPath string) error { - configContent, err := os.ReadFile(configPath) - if err != nil { - return E.Cause(err, "read config") - } - var options option.Options - err = options.UnmarshalJSON(configContent) - if err != nil { - return E.Cause(err, "decode config") - } - buffer := new(bytes.Buffer) - encoder := json.NewEncoder(buffer) - encoder.SetIndent("", " ") - err = encoder.Encode(options) - if err != nil { - return E.Cause(err, "encode config") - } - if !commandFormatFlagWrite { - os.Stdout.WriteString(buffer.String() + "\n") - return nil - } - if bytes.Equal(configContent, buffer.Bytes()) { - return nil - } - output, err := os.Create(configPath) - if err != nil { - return E.Cause(err, "open output") - } - _, err = output.Write(buffer.Bytes()) - output.Close() - if err != nil { - return E.Cause(err, "write output") - } - outputPath, _ := filepath.Abs(configPath) - os.Stderr.WriteString(outputPath + "\n") - return nil -} diff --git a/cmd/sing-box/cmd_geoip.go b/cmd/sing-box/cmd_geoip.go new file mode 100644 index 0000000000..dbbbff135e --- /dev/null +++ b/cmd/sing-box/cmd_geoip.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var ( + geoipReader *maxminddb.Reader + commandGeoIPFlagFile string +) + +var commandGeoip = &cobra.Command{ + Use: "geoip", + Short: "GeoIP tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geoipPreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.PersistentFlags().StringVarP(&commandGeoIPFlagFile, "file", "f", "geoip.db", "geoip file") + mainCommand.AddCommand(commandGeoip) +} + +func geoipPreRun() error { + reader, err := maxminddb.Open(commandGeoIPFlagFile) + if err != nil { + return err + } + if reader.Metadata.DatabaseType != "sing-geoip" { + reader.Close() + return E.New("incorrect database type, expected sing-geoip, got ", reader.Metadata.DatabaseType) + } + geoipReader = reader + return nil +} diff --git a/cmd/sing-box/cmd_geoip_export.go b/cmd/sing-box/cmd_geoip_export.go new file mode 100644 index 0000000000..d170d10b72 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_export.go @@ -0,0 +1,98 @@ +package main + +import ( + "io" + "net" + "os" + "strings" + + "github.com/sagernet/sing-box/common/json" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var flagGeoipExportOutput string + +const flagGeoipExportDefaultOutput = "geoip-.srs" + +var commandGeoipExport = &cobra.Command{ + Use: "export ", + Short: "Export geoip country as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoipExport.Flags().StringVarP(&flagGeoipExportOutput, "output", "o", flagGeoipExportDefaultOutput, "Output path") + commandGeoip.AddCommand(commandGeoipExport) +} + +func geoipExport(countryCode string) error { + networks := geoipReader.Networks(maxminddb.SkipAliasedNetworks) + countryMap := make(map[string][]*net.IPNet) + var ( + ipNet *net.IPNet + nextCountryCode string + err error + ) + for networks.Next() { + ipNet, err = networks.Network(&nextCountryCode) + if err != nil { + return err + } + countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet) + } + ipNets := countryMap[strings.ToLower(countryCode)] + if len(ipNets) == 0 { + return E.New("country code not found: ", countryCode) + } + + var ( + outputFile *os.File + outputWriter io.Writer + ) + if flagGeoipExportOutput == "stdout" { + outputWriter = os.Stdout + } else if flagGeoipExportOutput == flagGeoipExportDefaultOutput { + outputFile, err = os.Create("geoip-" + countryCode + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(flagGeoipExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + headlessRule.IPCIDR = make([]string, 0, len(ipNets)) + for _, cidr := range ipNets { + headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String()) + } + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geoip_list.go b/cmd/sing-box/cmd_geoip_list.go new file mode 100644 index 0000000000..54dd426ea9 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_list.go @@ -0,0 +1,31 @@ +package main + +import ( + "os" + + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var commandGeoipList = &cobra.Command{ + Use: "list", + Short: "List geoip country codes", + Run: func(cmd *cobra.Command, args []string) { + err := listGeoip() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipList) +} + +func listGeoip() error { + for _, code := range geoipReader.Metadata.Languages { + os.Stdout.WriteString(code + "\n") + } + return nil +} diff --git a/cmd/sing-box/cmd_geoip_lookup.go b/cmd/sing-box/cmd_geoip_lookup.go new file mode 100644 index 0000000000..d5157bb404 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_lookup.go @@ -0,0 +1,47 @@ +package main + +import ( + "net/netip" + "os" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + + "github.com/spf13/cobra" +) + +var commandGeoipLookup = &cobra.Command{ + Use: "lookup
", + Short: "Lookup if an IP address is contained in the GeoIP database", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipLookup(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipLookup) +} + +func geoipLookup(address string) error { + addr, err := netip.ParseAddr(address) + if err != nil { + return E.Cause(err, "parse address") + } + if !N.IsPublicAddr(addr) { + os.Stdout.WriteString("private\n") + return nil + } + var code string + _ = geoipReader.Lookup(addr.AsSlice(), &code) + if code != "" { + os.Stdout.WriteString(code + "\n") + return nil + } + os.Stdout.WriteString("unknown\n") + return nil +} diff --git a/cmd/sing-box/cmd_geosite.go b/cmd/sing-box/cmd_geosite.go new file mode 100644 index 0000000000..95db935797 --- /dev/null +++ b/cmd/sing-box/cmd_geosite.go @@ -0,0 +1,41 @@ +package main + +import ( + "github.com/sagernet/sing-box/common/geosite" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var ( + commandGeoSiteFlagFile string + geositeReader *geosite.Reader + geositeCodeList []string +) + +var commandGeoSite = &cobra.Command{ + Use: "geosite", + Short: "Geosite tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geositePreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.PersistentFlags().StringVarP(&commandGeoSiteFlagFile, "file", "f", "geosite.db", "geosite file") + mainCommand.AddCommand(commandGeoSite) +} + +func geositePreRun() error { + reader, codeList, err := geosite.Open(commandGeoSiteFlagFile) + if err != nil { + return E.Cause(err, "open geosite file") + } + geositeReader = reader + geositeCodeList = codeList + return nil +} diff --git a/cmd/sing-box/cmd_geosite_export.go b/cmd/sing-box/cmd_geosite_export.go new file mode 100644 index 0000000000..71f1018d8f --- /dev/null +++ b/cmd/sing-box/cmd_geosite_export.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/json" + "io" + "os" + + "github.com/sagernet/sing-box/common/geosite" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "github.com/spf13/cobra" +) + +var commandGeositeExportOutput string + +const commandGeositeExportDefaultOutput = "geosite-.json" + +var commandGeositeExport = &cobra.Command{ + Use: "export ", + Short: "Export geosite category as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geositeExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeositeExport.Flags().StringVarP(&commandGeositeExportOutput, "output", "o", commandGeositeExportDefaultOutput, "Output path") + commandGeoSite.AddCommand(commandGeositeExport) +} + +func geositeExport(category string) error { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + var ( + outputFile *os.File + outputWriter io.Writer + ) + if commandGeositeExportOutput == "stdout" { + outputWriter = os.Stdout + } else if commandGeositeExportOutput == commandGeositeExportDefaultOutput { + outputFile, err = os.Create("geosite-" + category + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(commandGeositeExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + defaultRule := geosite.Compile(sourceSet) + headlessRule.Domain = defaultRule.Domain + headlessRule.DomainSuffix = defaultRule.DomainSuffix + headlessRule.DomainKeyword = defaultRule.DomainKeyword + headlessRule.DomainRegex = defaultRule.DomainRegex + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geosite_list.go b/cmd/sing-box/cmd_geosite_list.go new file mode 100644 index 0000000000..cedb7adfd2 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_list.go @@ -0,0 +1,50 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + F "github.com/sagernet/sing/common/format" + + "github.com/spf13/cobra" +) + +var commandGeositeList = &cobra.Command{ + Use: "list ", + Short: "List geosite categories", + Run: func(cmd *cobra.Command, args []string) { + err := geositeList() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeList) +} + +func geositeList() error { + var geositeEntry []struct { + category string + items int + } + for _, category := range geositeCodeList { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + geositeEntry = append(geositeEntry, struct { + category string + items int + }{category, len(sourceSet)}) + } + sort.SliceStable(geositeEntry, func(i, j int) bool { + return geositeEntry[i].items < geositeEntry[j].items + }) + for _, entry := range geositeEntry { + os.Stdout.WriteString(F.ToString(entry.category, " (", entry.items, ")\n")) + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_lookup.go b/cmd/sing-box/cmd_geosite_lookup.go new file mode 100644 index 0000000000..f648ce62a4 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_lookup.go @@ -0,0 +1,97 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandGeositeLookup = &cobra.Command{ + Use: "lookup [category] ", + Short: "Check if a domain is in the geosite", + Args: cobra.RangeArgs(1, 2), + Run: func(cmd *cobra.Command, args []string) { + var ( + source string + target string + ) + switch len(args) { + case 1: + target = args[0] + case 2: + source = args[0] + target = args[1] + } + err := geositeLookup(source, target) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeLookup) +} + +func geositeLookup(source string, target string) error { + var sourceMatcherList []struct { + code string + matcher *searchGeositeMatcher + } + if source != "" { + sourceSet, err := geositeReader.Read(source) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+source) + } + sourceMatcherList = []struct { + code string + matcher *searchGeositeMatcher + }{ + { + code: source, + matcher: sourceMatcher, + }, + } + + } else { + for _, code := range geositeCodeList { + sourceSet, err := geositeReader.Read(code) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+code) + } + sourceMatcherList = append(sourceMatcherList, struct { + code string + matcher *searchGeositeMatcher + }{ + code: code, + matcher: sourceMatcher, + }) + } + } + sort.SliceStable(sourceMatcherList, func(i, j int) bool { + return sourceMatcherList[i].code < sourceMatcherList[j].code + }) + + for _, matcherItem := range sourceMatcherList { + if matchRule := matcherItem.matcher.Match(target); matchRule != "" { + os.Stdout.WriteString("Match code (") + os.Stdout.WriteString(matcherItem.code) + os.Stdout.WriteString(") ") + os.Stdout.WriteString(matchRule) + os.Stdout.WriteString("\n") + } + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_matcher.go b/cmd/sing-box/cmd_geosite_matcher.go new file mode 100644 index 0000000000..791dba2499 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_matcher.go @@ -0,0 +1,56 @@ +package main + +import ( + "regexp" + "strings" + + "github.com/sagernet/sing-box/common/geosite" +) + +type searchGeositeMatcher struct { + domainMap map[string]bool + suffixList []string + keywordList []string + regexList []string +} + +func newSearchGeositeMatcher(items []geosite.Item) (*searchGeositeMatcher, error) { + options := geosite.Compile(items) + domainMap := make(map[string]bool) + for _, domain := range options.Domain { + domainMap[domain] = true + } + rule := &searchGeositeMatcher{ + domainMap: domainMap, + suffixList: options.DomainSuffix, + keywordList: options.DomainKeyword, + regexList: options.DomainRegex, + } + return rule, nil +} + +func (r *searchGeositeMatcher) Match(domain string) string { + if r.domainMap[domain] { + return "domain=" + domain + } + for _, suffix := range r.suffixList { + if strings.HasSuffix(domain, suffix) { + return "domain_suffix=" + suffix + } + } + for _, keyword := range r.keywordList { + if strings.Contains(domain, keyword) { + return "domain_keyword=" + keyword + } + } + for _, regexStr := range r.regexList { + regex, err := regexp.Compile(regexStr) + if err != nil { + continue + } + if regex.MatchString(domain) { + return "domain_regex=" + regexStr + } + } + return "" +} diff --git a/cmd/sing-box/cmd_merge.go b/cmd/sing-box/cmd_merge.go index 0aff750182..4fb07b8688 100644 --- a/cmd/sing-box/cmd_merge.go +++ b/cmd/sing-box/cmd_merge.go @@ -18,7 +18,7 @@ import ( ) var commandMerge = &cobra.Command{ - Use: "merge [output]", + Use: "merge ", Short: "Merge configurations", Run: func(cmd *cobra.Command, args []string) { err := merge(args[0]) diff --git a/cmd/sing-box/cmd_rule_set.go b/cmd/sing-box/cmd_rule_set.go new file mode 100644 index 0000000000..f4112a087b --- /dev/null +++ b/cmd/sing-box/cmd_rule_set.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +var commandRuleSet = &cobra.Command{ + Use: "rule-set", + Short: "Manage rule sets", +} + +func init() { + mainCommand.AddCommand(commandRuleSet) +} diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go new file mode 100644 index 0000000000..de318095ac --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -0,0 +1,80 @@ +package main + +import ( + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "github.com/spf13/cobra" +) + +var flagRuleSetCompileOutput string + +const flagRuleSetCompileDefaultOutput = ".srs" + +var commandRuleSetCompile = &cobra.Command{ + Use: "compile [source-path]", + Short: "Compile rule-set json to binary", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := compileRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSet.AddCommand(commandRuleSetCompile) + commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file") +} + +func compileRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + decoder := json.NewDecoder(json.NewCommentFilter(reader)) + decoder.DisallowUnknownFields() + var plainRuleSet option.PlainRuleSetCompat + err = decoder.Decode(&plainRuleSet) + if err != nil { + return err + } + ruleSet := plainRuleSet.Upgrade() + var outputPath string + if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { + if strings.HasSuffix(sourcePath, ".json") { + outputPath = sourcePath[:len(sourcePath)-5] + ".srs" + } else { + outputPath = sourcePath + ".srs" + } + } else { + outputPath = flagRuleSetCompileOutput + } + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + err = srs.Write(outputFile, ruleSet) + if err != nil { + outputFile.Close() + os.Remove(outputPath) + return err + } + outputFile.Close() + return nil +} diff --git a/cmd/sing-box/cmd_rule_set_format.go b/cmd/sing-box/cmd_rule_set_format.go new file mode 100644 index 0000000000..dc3ee6aabd --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_format.go @@ -0,0 +1,87 @@ +package main + +import ( + "bytes" + "io" + "os" + "path/filepath" + + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandRuleSetFormatFlagWrite bool + +var commandRuleSetFormat = &cobra.Command{ + Use: "format ", + Short: "Format rule-set json", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := formatRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") + commandRuleSet.AddCommand(commandRuleSetFormat) +} + +func formatRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + content, err := io.ReadAll(reader) + if err != nil { + return err + } + decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content))) + decoder.DisallowUnknownFields() + var plainRuleSet option.PlainRuleSetCompat + err = decoder.Decode(&plainRuleSet) + if err != nil { + return err + } + ruleSet := plainRuleSet.Upgrade() + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(ruleSet) + if err != nil { + return E.Cause(err, "encode config") + } + outputPath, _ := filepath.Abs(sourcePath) + if !commandRuleSetFormatFlagWrite || sourcePath == "stdin" { + os.Stdout.WriteString(buffer.String() + "\n") + return nil + } + if bytes.Equal(content, buffer.Bytes()) { + return nil + } + output, err := os.Create(sourcePath) + if err != nil { + return E.Cause(err, "open output") + } + _, err = output.Write(buffer.Bytes()) + output.Close() + if err != nil { + return E.Cause(err, "write output") + } + os.Stderr.WriteString(outputPath + "\n") + return nil +} diff --git a/cmd/sing-box/cmd_tools.go b/cmd/sing-box/cmd_tools.go index 460a50cd1c..c45f585576 100644 --- a/cmd/sing-box/cmd_tools.go +++ b/cmd/sing-box/cmd_tools.go @@ -38,11 +38,7 @@ func createPreStartedClient() (*box.Box, error) { func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) { if outboundTag == "" { - outbound := instance.Router().DefaultOutbound(N.NetworkName(network)) - if outbound == nil { - return nil, E.New("missing default outbound") - } - return outbound, nil + return instance.Router().DefaultOutbound(N.NetworkName(network)) } else { outbound, loaded := instance.Router().Outbound(outboundTag) if !loaded { diff --git a/cmd/sing-box/cmd_tools_connect.go b/cmd/sing-box/cmd_tools_connect.go index b904ebc9f5..3ea04bcd40 100644 --- a/cmd/sing-box/cmd_tools_connect.go +++ b/cmd/sing-box/cmd_tools_connect.go @@ -18,7 +18,7 @@ import ( var commandConnectFlagNetwork string var commandConnect = &cobra.Command{ - Use: "connect [address]", + Use: "connect
", Short: "Connect to an address", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { diff --git a/common/dialer/router.go b/common/dialer/router.go index 1d5586546e..2531607753 100644 --- a/common/dialer/router.go +++ b/common/dialer/router.go @@ -18,11 +18,19 @@ func NewRouter(router adapter.Router) N.Dialer { } func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - return d.router.DefaultOutbound(network).DialContext(ctx, network, destination) + dialer, err := d.router.DefaultOutbound(network) + if err != nil { + return nil, err + } + return dialer.DialContext(ctx, network, destination) } func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return d.router.DefaultOutbound(N.NetworkUDP).ListenPacket(ctx, destination) + dialer, err := d.router.DefaultOutbound(N.NetworkUDP) + if err != nil { + return nil, err + } + return dialer.ListenPacket(ctx, destination) } func (d *RouterDialer) Upstream() any { diff --git a/common/srs/binary.go b/common/srs/binary.go new file mode 100644 index 0000000000..dd994c2c29 --- /dev/null +++ b/common/srs/binary.go @@ -0,0 +1,485 @@ +package srs + +import ( + "compress/zlib" + "encoding/binary" + "io" + "net/netip" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/rw" + + "go4.org/netipx" +) + +var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS + +const ( + ruleItemQueryType uint8 = iota + ruleItemNetwork + ruleItemDomain + ruleItemDomainKeyword + ruleItemDomainRegex + ruleItemSourceIPCIDR + ruleItemIPCIDR + ruleItemSourcePort + ruleItemSourcePortRange + ruleItemPort + ruleItemPortRange + ruleItemProcessName + ruleItemProcessPath + ruleItemPackageName + ruleItemWIFISSID + ruleItemWIFIBSSID + ruleItemFinal uint8 = 0xFF +) + +func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) { + var magicBytes [3]byte + _, err = io.ReadFull(reader, magicBytes[:]) + if err != nil { + return + } + if magicBytes != MagicBytes { + err = E.New("invalid sing-box rule set file") + return + } + var version uint8 + err = binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return ruleSet, err + } + if version != 1 { + return ruleSet, E.New("unsupported version: ", version) + } + zReader, err := zlib.NewReader(reader) + if err != nil { + return + } + length, err := rw.ReadUVariant(zReader) + if err != nil { + return + } + ruleSet.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + ruleSet.Rules[i], err = readRule(zReader, recovery) + if err != nil { + err = E.Cause(err, "read rule[", i, "]") + return + } + } + return +} + +func Write(writer io.Writer, ruleSet option.PlainRuleSet) error { + _, err := writer.Write(MagicBytes[:]) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) + if err != nil { + return err + } + err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules))) + if err != nil { + return err + } + for _, rule := range ruleSet.Rules { + err = writeRule(zWriter, rule) + if err != nil { + return err + } + } + return zWriter.Close() +} + +func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) { + var ruleType uint8 + err = binary.Read(reader, binary.BigEndian, &ruleType) + if err != nil { + return + } + switch ruleType { + case 0: + rule.DefaultOptions, err = readDefaultRule(reader, recovery) + case 1: + rule.LogicalOptions, err = readLogicalRule(reader, recovery) + default: + err = E.New("unknown rule type: ", ruleType) + } + return +} + +func writeRule(writer io.Writer, rule option.HeadlessRule) error { + switch rule.Type { + case C.RuleTypeDefault: + return writeDefaultRule(writer, rule.DefaultOptions) + case C.RuleTypeLogical: + return writeLogicalRule(writer, rule.LogicalOptions) + default: + panic("unknown rule type: " + rule.Type) + } +} + +func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) { + var lastItemType uint8 + for { + var itemType uint8 + err = binary.Read(reader, binary.BigEndian, &itemType) + if err != nil { + return + } + switch itemType { + case ruleItemQueryType: + var rawQueryType []uint16 + rawQueryType, err = readRuleItemUint16(reader) + if err != nil { + return + } + rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType { + return option.DNSQueryType(it) + }) + case ruleItemNetwork: + rule.Network, err = readRuleItemString(reader) + case ruleItemDomain: + var matcher *domain.Matcher + matcher, err = domain.ReadMatcher(reader) + if err != nil { + return + } + rule.DomainMatcher = matcher + case ruleItemDomainKeyword: + rule.DomainKeyword, err = readRuleItemString(reader) + case ruleItemDomainRegex: + rule.DomainRegex, err = readRuleItemString(reader) + case ruleItemSourceIPCIDR: + rule.SourceIPSet, err = readIPSet(reader) + if err != nil { + return + } + if recovery { + rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemIPCIDR: + rule.IPSet, err = readIPSet(reader) + if err != nil { + return + } + if recovery { + rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemSourcePort: + rule.SourcePort, err = readRuleItemUint16(reader) + case ruleItemSourcePortRange: + rule.SourcePortRange, err = readRuleItemString(reader) + case ruleItemPort: + rule.Port, err = readRuleItemUint16(reader) + case ruleItemPortRange: + rule.PortRange, err = readRuleItemString(reader) + case ruleItemProcessName: + rule.ProcessName, err = readRuleItemString(reader) + case ruleItemProcessPath: + rule.ProcessPath, err = readRuleItemString(reader) + case ruleItemPackageName: + rule.PackageName, err = readRuleItemString(reader) + case ruleItemWIFISSID: + rule.WIFISSID, err = readRuleItemString(reader) + case ruleItemWIFIBSSID: + rule.WIFIBSSID, err = readRuleItemString(reader) + case ruleItemFinal: + err = binary.Read(reader, binary.BigEndian, &rule.Invert) + return + default: + err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType) + } + if err != nil { + return + } + lastItemType = itemType + } +} + +func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error { + err := binary.Write(writer, binary.BigEndian, uint8(0)) + if err != nil { + return err + } + if len(rule.QueryType) > 0 { + err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 { + return uint16(it) + })) + if err != nil { + return err + } + } + if len(rule.Network) > 0 { + err = writeRuleItemString(writer, ruleItemNetwork, rule.Network) + if err != nil { + return err + } + } + if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 { + err = binary.Write(writer, binary.BigEndian, ruleItemDomain) + if err != nil { + return err + } + err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer) + if err != nil { + return err + } + } + if len(rule.DomainKeyword) > 0 { + err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword) + if err != nil { + return err + } + } + if len(rule.DomainRegex) > 0 { + err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex) + if err != nil { + return err + } + } + if len(rule.SourceIPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR) + if err != nil { + return E.Cause(err, "source_ipcidr") + } + } + if len(rule.IPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR) + if err != nil { + return E.Cause(err, "ipcidr") + } + } + if len(rule.SourcePort) > 0 { + err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort) + if err != nil { + return err + } + } + if len(rule.SourcePortRange) > 0 { + err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange) + if err != nil { + return err + } + } + if len(rule.Port) > 0 { + err = writeRuleItemUint16(writer, ruleItemPort, rule.Port) + if err != nil { + return err + } + } + if len(rule.PortRange) > 0 { + err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange) + if err != nil { + return err + } + } + if len(rule.ProcessName) > 0 { + err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName) + if err != nil { + return err + } + } + if len(rule.ProcessPath) > 0 { + err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath) + if err != nil { + return err + } + } + if len(rule.PackageName) > 0 { + err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName) + if err != nil { + return err + } + } + if len(rule.WIFISSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) + if err != nil { + return err + } + } + if len(rule.WIFIBSSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, ruleItemFinal) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, rule.Invert) + if err != nil { + return err + } + return nil +} + +func readRuleItemString(reader io.Reader) ([]string, error) { + length, err := rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + value := make([]string, length) + for i := uint64(0); i < length; i++ { + value[i], err = rw.ReadVString(reader) + if err != nil { + return nil, err + } + } + return value, nil +} + +func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error { + err := binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(value))) + if err != nil { + return err + } + for _, item := range value { + err = rw.WriteVString(writer, item) + if err != nil { + return err + } + } + return nil +} + +func readRuleItemUint16(reader io.Reader) ([]uint16, error) { + length, err := rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + value := make([]uint16, length) + for i := uint64(0); i < length; i++ { + err = binary.Read(reader, binary.BigEndian, &value[i]) + if err != nil { + return nil, err + } + } + return value, nil +} + +func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error { + err := binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(value))) + if err != nil { + return err + } + for _, item := range value { + err = binary.Write(writer, binary.BigEndian, item) + if err != nil { + return err + } + } + return nil +} + +func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error { + var builder netipx.IPSetBuilder + for i, prefixString := range value { + prefix, err := netip.ParsePrefix(prefixString) + if err == nil { + builder.AddPrefix(prefix) + continue + } + addr, addrErr := netip.ParseAddr(prefixString) + if addrErr == nil { + builder.Add(addr) + continue + } + return E.Cause(err, "parse [", i, "]") + } + ipSet, err := builder.IPSet() + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + return writeIPSet(writer, ipSet) +} + +func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { + var mode uint8 + err = binary.Read(reader, binary.BigEndian, &mode) + if err != nil { + return + } + switch mode { + case 0: + logicalRule.Mode = C.LogicalTypeAnd + case 1: + logicalRule.Mode = C.LogicalTypeOr + default: + err = E.New("unknown logical mode: ", mode) + return + } + length, err := rw.ReadUVariant(reader) + if err != nil { + return + } + logicalRule.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + logicalRule.Rules[i], err = readRule(reader, recovery) + if err != nil { + err = E.Cause(err, "read logical rule [", i, "]") + return + } + } + err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert) + if err != nil { + return + } + return +} + +func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + switch logicalRule.Mode { + case C.LogicalTypeAnd: + err = binary.Write(writer, binary.BigEndian, uint8(0)) + case C.LogicalTypeOr: + err = binary.Write(writer, binary.BigEndian, uint8(1)) + default: + panic("unknown logical mode: " + logicalRule.Mode) + } + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules))) + if err != nil { + return err + } + for _, rule := range logicalRule.Rules { + err = writeRule(writer, rule) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, logicalRule.Invert) + if err != nil { + return err + } + return nil +} diff --git a/common/srs/ip_set.go b/common/srs/ip_set.go new file mode 100644 index 0000000000..b346da26f6 --- /dev/null +++ b/common/srs/ip_set.go @@ -0,0 +1,116 @@ +package srs + +import ( + "encoding/binary" + "io" + "net/netip" + "unsafe" + + "github.com/sagernet/sing/common/rw" + + "go4.org/netipx" +) + +type myIPSet struct { + rr []myIPRange +} + +type myIPRange struct { + from netip.Addr + to netip.Addr +} + +func readIPSet(reader io.Reader) (*netipx.IPSet, error) { + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return nil, err + } + var length uint64 + err = binary.Read(reader, binary.BigEndian, &length) + if err != nil { + return nil, err + } + mySet := &myIPSet{ + rr: make([]myIPRange, length), + } + for i := uint64(0); i < length; i++ { + var ( + fromLen uint64 + toLen uint64 + fromAddr netip.Addr + toAddr netip.Addr + ) + fromLen, err = rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + fromBytes := make([]byte, fromLen) + _, err = io.ReadFull(reader, fromBytes) + if err != nil { + return nil, err + } + err = fromAddr.UnmarshalBinary(fromBytes) + if err != nil { + return nil, err + } + toLen, err = rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + toBytes := make([]byte, toLen) + _, err = io.ReadFull(reader, toBytes) + if err != nil { + return nil, err + } + err = toAddr.UnmarshalBinary(toBytes) + if err != nil { + return nil, err + } + mySet.rr[i] = myIPRange{fromAddr, toAddr} + } + return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil +} + +func writeIPSet(writer io.Writer, set *netipx.IPSet) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + mySet := (*myIPSet)(unsafe.Pointer(set)) + err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr))) + if err != nil { + return err + } + for _, rr := range mySet.rr { + var ( + fromBinary []byte + toBinary []byte + ) + fromBinary, err = rr.from.MarshalBinary() + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(fromBinary))) + if err != nil { + return err + } + _, err = writer.Write(fromBinary) + if err != nil { + return err + } + toBinary, err = rr.to.MarshalBinary() + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(toBinary))) + if err != nil { + return err + } + _, err = writer.Write(toBinary) + if err != nil { + return err + } + } + return nil +} diff --git a/constant/rule.go b/constant/rule.go index 3c741995f8..5a8eaf127f 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -9,3 +9,11 @@ const ( LogicalTypeAnd = "and" LogicalTypeOr = "or" ) + +const ( + RuleSetTypeLocal = "local" + RuleSetTypeRemote = "remote" + RuleSetVersion1 = 1 + RuleSetFormatSource = "source" + RuleSetFormatBinary = "binary" +) diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index 89d238822a..daf48d7236 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -22,11 +22,13 @@ var ( bucketSelected = []byte("selected") bucketExpand = []byte("group_expand") bucketMode = []byte("clash_mode") + bucketRuleSet = []byte("rule_set") bucketNameList = []string{ string(bucketSelected), string(bucketExpand), string(bucketMode), + string(bucketRuleSet), } cacheIDDefault = []byte("default") @@ -257,3 +259,36 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { } }) } + +func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedRuleSet { + var savedSet adapter.SavedRuleSet + err := c.DB.View(func(t *bbolt.Tx) error { + bucket := c.bucket(t, bucketRuleSet) + if bucket == nil { + return os.ErrNotExist + } + setBinary := bucket.Get([]byte(tag)) + if len(setBinary) == 0 { + return os.ErrInvalid + } + return savedSet.UnmarshalBinary(setBinary) + }) + if err != nil { + return nil + } + return &savedSet +} + +func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedRuleSet) error { + return c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := c.createBucket(t, bucketRuleSet) + if err != nil { + return err + } + setBinary, err := set.MarshalBinary() + if err != nil { + return err + } + return bucket.Put([]byte(tag), setBinary) + }) +} diff --git a/experimental/cachefile/fakeip.go b/experimental/cachefile/fakeip.go index 2242342a36..e998ebb859 100644 --- a/experimental/cachefile/fakeip.go +++ b/experimental/cachefile/fakeip.go @@ -25,7 +25,7 @@ func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata { err := c.DB.Batch(func(tx *bbolt.Tx) error { bucket := tx.Bucket(bucketFakeIP) if bucket == nil { - return nil + return os.ErrNotExist } metadataBinary := bucket.Get(keyMetadata) if len(metadataBinary) == 0 { diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go index 050efd8d17..cf96931a85 100644 --- a/experimental/clashapi/proxies.go +++ b/experimental/clashapi/proxies.go @@ -100,8 +100,10 @@ func getProxies(server *Server, router adapter.Router) func(w http.ResponseWrite allProxies = append(allProxies, detour.Tag()) } - defaultTag := router.DefaultOutbound(N.NetworkTCP).Tag() - if defaultTag == "" { + var defaultTag string + if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + defaultTag = defaultOutbound.Tag() + } else { defaultTag = allProxies[0] } diff --git a/experimental/clashapi/server_resources.go b/experimental/clashapi/server_resources.go index ad36641e05..d6d22b5390 100644 --- a/experimental/clashapi/server_resources.go +++ b/experimental/clashapi/server_resources.go @@ -51,7 +51,11 @@ func (s *Server) downloadExternalUI() error { } detour = outbound } else { - detour = s.router.DefaultOutbound(N.NetworkTCP) + outbound, err := s.router.DefaultOutbound(N.NetworkTCP) + if err != nil { + return err + } + detour = outbound } httpClient := &http.Client{ Transport: &http.Transport{ diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go index 3dc5a367e7..b7c20eb075 100644 --- a/experimental/clashapi/trafficontrol/tracker.go +++ b/experimental/clashapi/trafficontrol/tracker.go @@ -94,7 +94,9 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad var chain []string var next string if rule == nil { - next = router.DefaultOutbound(N.NetworkTCP).Tag() + if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + next = defaultOutbound.Tag() + } } else { next = rule.Outbound() } @@ -181,7 +183,9 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route var chain []string var next string if rule == nil { - next = router.DefaultOutbound(N.NetworkUDP).Tag() + if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil { + next = defaultOutbound.Tag() + } } else { next = rule.Outbound() } diff --git a/go.mod b/go.mod index 7efe241ef6..095326d7f9 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 github.com/sagernet/quic-go v0.40.0 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc + github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be github.com/sagernet/sing-dns v0.1.11 github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 github.com/sagernet/sing-quic v0.1.5-0.20231123150216-00957d136203 diff --git a/go.sum b/go.sum index 4db37c0bfe..6d8d994a8c 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= -github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc h1:vESVuxHgbd2EzHxd+TYTpNACIEGBOhp5n3KG7bgbcws= -github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= +github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be h1:FigAM9kq7RRXmHvgn8w2a8tqCY5CMV5GIk0id84dI0o= +github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE= github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE= github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM= diff --git a/option/experimental.go b/option/experimental.go index 72751a590e..c685f51f54 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -23,16 +23,16 @@ type ClashAPIOptions struct { DefaultMode string `json:"default_mode,omitempty"` ModeList []string `json:"-"` + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` // Deprecated: migrated to global cache file StoreMode bool `json:"store_mode,omitempty"` // Deprecated: migrated to global cache file StoreSelected bool `json:"store_selected,omitempty"` // Deprecated: migrated to global cache file StoreFakeIP bool `json:"store_fakeip,omitempty"` - // Deprecated: migrated to global cache file - CacheFile string `json:"cache_file,omitempty"` - // Deprecated: migrated to global cache file - CacheID string `json:"cache_id,omitempty"` } type V2RayAPIOptions struct { diff --git a/option/route.go b/option/route.go index 43150576e2..e313fcf242 100644 --- a/option/route.go +++ b/option/route.go @@ -4,6 +4,7 @@ type RouteOptions struct { GeoIP *GeoIPOptions `json:"geoip,omitempty"` Geosite *GeositeOptions `json:"geosite,omitempty"` Rules []Rule `json:"rules,omitempty"` + RuleSet []RuleSet `json:"rule_set,omitempty"` Final string `json:"final,omitempty"` FindProcess bool `json:"find_process,omitempty"` AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` diff --git a/option/rule.go b/option/rule.go index 4f4042025f..bad605a0fc 100644 --- a/option/rule.go +++ b/option/rule.go @@ -65,34 +65,36 @@ func (r Rule) IsValid() bool { } type DefaultRule struct { - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - GeoIP Listable[string] `json:"geoip,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - IPCIDR Listable[string] `json:"ip_cidr,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + Network Listable[string] `json:"network,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + Protocol Listable[string] `json:"protocol,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + Geosite Listable[string] `json:"geosite,omitempty"` + SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` + GeoIP Listable[string] `json:"geoip,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + User Listable[string] `json:"user,omitempty"` + UserID Listable[int32] `json:"user_id,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` } func (r DefaultRule) IsValid() bool { diff --git a/option/rule_dns.go b/option/rule_dns.go index fca3432293..c02d09f761 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -91,6 +91,7 @@ type DefaultDNSRule struct { ClashMode string `json:"clash_mode,omitempty"` WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` Invert bool `json:"invert,omitempty"` Server string `json:"server,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` diff --git a/option/rule_set.go b/option/rule_set.go new file mode 100644 index 0000000000..7a6f7e9282 --- /dev/null +++ b/option/rule_set.go @@ -0,0 +1,230 @@ +package option + +import ( + "reflect" + + "github.com/sagernet/sing-box/common/json" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + + "go4.org/netipx" +) + +type _RuleSet struct { + Type string `json:"type"` + Tag string `json:"tag"` + Format string `json:"format"` + LocalOptions LocalRuleSet `json:"-"` + RemoteOptions RemoteRuleSet `json:"-"` +} + +type RuleSet _RuleSet + +func (r RuleSet) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleSetTypeLocal: + v = r.LocalOptions + case C.RuleSetTypeRemote: + v = r.RemoteOptions + default: + return nil, E.New("unknown rule set type: " + r.Type) + } + return MarshallObjects((_RuleSet)(r), v) +} + +func (r *RuleSet) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_RuleSet)(r)) + if err != nil { + return err + } + if r.Tag == "" { + return E.New("missing rule_set.[].tag") + } + switch r.Format { + case "": + return E.New("missing rule_set.[].format") + case C.RuleSetFormatSource, C.RuleSetFormatBinary: + default: + return E.New("unknown rule set format: " + r.Format) + } + var v any + switch r.Type { + case C.RuleSetTypeLocal: + v = &r.LocalOptions + case C.RuleSetTypeRemote: + v = &r.RemoteOptions + case "": + return E.New("missing rule_set.[].type") + default: + return E.New("unknown rule set type: " + r.Type) + } + err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v) + if err != nil { + return E.Cause(err, "rule set") + } + return nil +} + +type LocalRuleSet struct { + Path string `json:"path,omitempty"` +} + +type RemoteRuleSet struct { + URL string `json:"url"` + DownloadDetour string `json:"download_detour,omitempty"` + UpdateInterval Duration `json:"update_interval,omitempty"` +} + +type _HeadlessRule struct { + Type string `json:"type,omitempty"` + DefaultOptions DefaultHeadlessRule `json:"-"` + LogicalOptions LogicalHeadlessRule `json:"-"` +} + +type HeadlessRule _HeadlessRule + +func (r HeadlessRule) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleTypeDefault: + r.Type = "" + v = r.DefaultOptions + case C.RuleTypeLogical: + v = r.LogicalOptions + default: + return nil, E.New("unknown rule type: " + r.Type) + } + return MarshallObjects((_HeadlessRule)(r), v) +} + +func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_HeadlessRule)(r)) + if err != nil { + return err + } + var v any + switch r.Type { + case "", C.RuleTypeDefault: + r.Type = C.RuleTypeDefault + v = &r.DefaultOptions + case C.RuleTypeLogical: + v = &r.LogicalOptions + default: + return E.New("unknown rule type: " + r.Type) + } + err = UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v) + if err != nil { + return E.Cause(err, "route rule-set rule") + } + return nil +} + +func (r HeadlessRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault, "": + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + +type DefaultHeadlessRule struct { + QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` + Network Listable[string] `json:"network,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + Invert bool `json:"invert,omitempty"` + + DomainMatcher *domain.Matcher `json:"-"` + SourceIPSet *netipx.IPSet `json:"-"` + IPSet *netipx.IPSet `json:"-"` +} + +func (r DefaultHeadlessRule) IsValid() bool { + var defaultValue DefaultHeadlessRule + defaultValue.Invert = r.Invert + return !reflect.DeepEqual(r, defaultValue) +} + +type LogicalHeadlessRule struct { + Mode string `json:"mode"` + Rules []HeadlessRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` +} + +func (r LogicalHeadlessRule) IsValid() bool { + return len(r.Rules) > 0 && common.All(r.Rules, HeadlessRule.IsValid) +} + +type _PlainRuleSetCompat struct { + Version int `json:"version"` + Options PlainRuleSet `json:"-"` +} + +type PlainRuleSetCompat _PlainRuleSetCompat + +func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { + var v any + switch r.Version { + case C.RuleSetVersion1: + v = r.Options + default: + return nil, E.New("unknown rule set version: ", r.Version) + } + return MarshallObjects((_PlainRuleSetCompat)(r), v) +} + +func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_PlainRuleSetCompat)(r)) + if err != nil { + return err + } + var v any + switch r.Version { + case C.RuleSetVersion1: + v = &r.Options + case 0: + return E.New("missing rule set version") + default: + return E.New("unknown rule set version: ", r.Version) + } + err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) + if err != nil { + return E.Cause(err, "rule set") + } + return nil +} + +func (r PlainRuleSetCompat) Upgrade() PlainRuleSet { + var result PlainRuleSet + switch r.Version { + case C.RuleSetVersion1: + result = r.Options + default: + panic("unknown rule set version: " + F.ToString(r.Version)) + } + return result +} + +type PlainRuleSet struct { + Rules []HeadlessRule `json:"rules,omitempty"` +} diff --git a/option/types.go b/option/types.go index f2fed66309..520c3503aa 100644 --- a/option/types.go +++ b/option/types.go @@ -174,6 +174,14 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error { type DNSQueryType uint16 +func (t DNSQueryType) String() string { + typeName, loaded := mDNS.TypeToString[uint16(t)] + if loaded { + return typeName + } + return F.ToString(uint16(t)) +} + func (t DNSQueryType) MarshalJSON() ([]byte, error) { typeName, loaded := mDNS.TypeToString[uint16(t)] if loaded { diff --git a/route/router.go b/route/router.go index b48d2346d9..967061952e 100644 --- a/route/router.go +++ b/route/router.go @@ -67,6 +67,8 @@ type Router struct { dnsClient *dns.Client defaultDomainStrategy dns.DomainStrategy dnsRules []adapter.DNSRule + ruleSets []adapter.RuleSet + ruleSetMap map[string]adapter.RuleSet defaultTransport dns.Transport transports []dns.Transport transportMap map[string]dns.Transport @@ -106,6 +108,7 @@ func NewRouter( outboundByTag: make(map[string]adapter.Outbound), rules: make([]adapter.Rule, 0, len(options.Rules)), dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)), + ruleSetMap: make(map[string]adapter.RuleSet), needGeoIPDatabase: hasRule(options.Rules, isGeoIPRule) || hasDNSRule(dnsOptions.Rules, isGeoIPDNSRule), needGeositeDatabase: hasRule(options.Rules, isGeositeRule) || hasDNSRule(dnsOptions.Rules, isGeositeDNSRule), geoIPOptions: common.PtrValueOrDefault(options.GeoIP), @@ -140,6 +143,14 @@ func NewRouter( } router.dnsRules = append(router.dnsRules, dnsRule) } + for i, ruleSetOptions := range options.RuleSet { + ruleSet, err := NewRuleSet(ctx, router, router.logger, ruleSetOptions) + if err != nil { + return nil, E.Cause(err, "parse rule-set[", i, "]") + } + router.ruleSets = append(router.ruleSets, ruleSet) + router.ruleSetMap[ruleSetOptions.Tag] = ruleSet + } transports := make([]dns.Transport, len(dnsOptions.Servers)) dummyTransportMap := make(map[string]dns.Transport) @@ -479,6 +490,12 @@ func (r *Router) Start() error { if r.needWIFIState { r.updateWIFIState() } + for i, ruleSet := range r.ruleSets { + err := ruleSet.Start() + if err != nil { + return E.Cause(err, "initialize rule-set[", i, "]") + } + } for i, rule := range r.rules { err := rule.Start() if err != nil { @@ -576,11 +593,17 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { return outbound, loaded } -func (r *Router) DefaultOutbound(network string) adapter.Outbound { +func (r *Router) DefaultOutbound(network string) (adapter.Outbound, error) { if network == N.NetworkTCP { - return r.defaultOutboundForConnection + if r.defaultOutboundForConnection == nil { + return nil, E.New("missing default outbound for TCP connections") + } + return r.defaultOutboundForConnection, nil } else { - return r.defaultOutboundForPacketConnection + if r.defaultOutboundForPacketConnection == nil { + return nil, E.New("missing default outbound for UDP connections") + } + return r.defaultOutboundForPacketConnection, nil } } @@ -588,6 +611,11 @@ func (r *Router) FakeIPStore() adapter.FakeIPStore { return r.fakeIPStore } +func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) { + ruleSet, loaded := r.ruleSetMap[tag] + return ruleSet, loaded +} + func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { diff --git a/route/rule_abstract.go b/route/rule_abstract.go index 38d4d57d41..312caaee73 100644 --- a/route/rule_abstract.go +++ b/route/rule_abstract.go @@ -1,6 +1,7 @@ package route import ( + "io" "strings" "github.com/sagernet/sing-box/adapter" @@ -135,7 +136,7 @@ func (r *abstractDefaultRule) String() string { } type abstractLogicalRule struct { - rules []adapter.Rule + rules []adapter.HeadlessRule mode string invert bool outbound string @@ -146,7 +147,10 @@ func (r *abstractLogicalRule) Type() string { } func (r *abstractLogicalRule) UpdateGeosite() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (adapter.Rule, bool) { + rule, loaded := it.(adapter.Rule) + return rule, loaded + }) { err := rule.UpdateGeosite() if err != nil { return err @@ -156,7 +160,10 @@ func (r *abstractLogicalRule) UpdateGeosite() error { } func (r *abstractLogicalRule) Start() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (common.Starter, bool) { + rule, loaded := it.(common.Starter) + return rule, loaded + }) { err := rule.Start() if err != nil { return err @@ -166,7 +173,10 @@ func (r *abstractLogicalRule) Start() error { } func (r *abstractLogicalRule) Close() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (io.Closer, bool) { + rule, loaded := it.(io.Closer) + return rule, loaded + }) { err := rule.Close() if err != nil { return err @@ -177,11 +187,11 @@ func (r *abstractLogicalRule) Close() error { func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { if r.mode == C.LogicalTypeAnd { - return common.All(r.rules, func(it adapter.Rule) bool { + return common.All(r.rules, func(it adapter.HeadlessRule) bool { return it.Match(metadata) }) != r.invert } else { - return common.Any(r.rules, func(it adapter.Rule) bool { + return common.Any(r.rules, func(it adapter.HeadlessRule) bool { return it.Match(metadata) }) != r.invert } diff --git a/route/rule_default.go b/route/rule_default.go index 8c8473abf5..c0ef9eef60 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -194,6 +194,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.RuleSet) > 0 { + item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } @@ -206,7 +211,7 @@ type LogicalRule struct { func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { r := &LogicalRule{ abstractLogicalRule{ - rules: make([]adapter.Rule, len(options.Rules)), + rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, outbound: options.Outbound, }, diff --git a/route/rule_dns.go b/route/rule_dns.go index b444932582..f5f9fd3583 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -190,6 +190,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.RuleSet) > 0 { + item := NewRuleSetItem(router, options.RuleSet, false) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } @@ -212,7 +217,7 @@ type LogicalDNSRule struct { func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { r := &LogicalDNSRule{ abstractLogicalRule: abstractLogicalRule{ - rules: make([]adapter.Rule, len(options.Rules)), + rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, outbound: options.Server, }, diff --git a/route/rule_headless.go b/route/rule_headless.go new file mode 100644 index 0000000000..9df2ee3036 --- /dev/null +++ b/route/rule_headless.go @@ -0,0 +1,173 @@ +package route + +import ( + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func NewHeadlessRule(router adapter.Router, options option.HeadlessRule) (adapter.HeadlessRule, error) { + switch options.Type { + case "", C.RuleTypeDefault: + if !options.DefaultOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewDefaultHeadlessRule(router, options.DefaultOptions) + case C.RuleTypeLogical: + if !options.LogicalOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewLogicalHeadlessRule(router, options.LogicalOptions) + default: + return nil, E.New("unknown rule type: ", options.Type) + } +} + +var _ adapter.HeadlessRule = (*DefaultHeadlessRule)(nil) + +type DefaultHeadlessRule struct { + abstractDefaultRule +} + +func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) { + rule := &DefaultHeadlessRule{ + abstractDefaultRule{ + invert: options.Invert, + }, + } + if len(options.Network) > 0 { + item := NewNetworkItem(options.Network) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 { + item := NewDomainItem(options.Domain, options.DomainSuffix) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.DomainMatcher != nil { + item := NewRawDomainItem(options.DomainMatcher) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainKeyword) > 0 { + item := NewDomainKeywordItem(options.DomainKeyword) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainRegex) > 0 { + item, err := NewDomainRegexItem(options.DomainRegex) + if err != nil { + return nil, E.Cause(err, "domain_regex") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourceIPCIDR) > 0 { + item, err := NewIPCIDRItem(true, options.SourceIPCIDR) + if err != nil { + return nil, E.Cause(err, "source_ipcidr") + } + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.SourceIPSet != nil { + item := NewRawIPCIDRItem(true, options.SourceIPSet) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.IPCIDR) > 0 { + item, err := NewIPCIDRItem(false, options.IPCIDR) + if err != nil { + return nil, E.Cause(err, "ipcidr") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.IPSet != nil { + item := NewRawIPCIDRItem(false, options.IPSet) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePort) > 0 { + item := NewPortItem(true, options.SourcePort) + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePortRange) > 0 { + item, err := NewPortRangeItem(true, options.SourcePortRange) + if err != nil { + return nil, E.Cause(err, "source_port_range") + } + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Port) > 0 { + item := NewPortItem(false, options.Port) + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PortRange) > 0 { + item, err := NewPortRangeItem(false, options.PortRange) + if err != nil { + return nil, E.Cause(err, "port_range") + } + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessName) > 0 { + item := NewProcessItem(options.ProcessName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessPath) > 0 { + item := NewProcessPathItem(options.ProcessPath) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PackageName) > 0 { + item := NewPackageNameItem(options.PackageName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(router, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFIBSSID) > 0 { + item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + return rule, nil +} + +var _ adapter.HeadlessRule = (*LogicalHeadlessRule)(nil) + +type LogicalHeadlessRule struct { + abstractLogicalRule +} + +func NewLogicalHeadlessRule(router adapter.Router, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) { + r := &LogicalHeadlessRule{ + abstractLogicalRule{ + rules: make([]adapter.HeadlessRule, len(options.Rules)), + invert: options.Invert, + }, + } + switch options.Mode { + case C.LogicalTypeAnd: + r.mode = C.LogicalTypeAnd + case C.LogicalTypeOr: + r.mode = C.LogicalTypeOr + default: + return nil, E.New("unknown logical mode: ", options.Mode) + } + for i, subRule := range options.Rules { + rule, err := NewHeadlessRule(router, subRule) + if err != nil { + return nil, E.Cause(err, "sub rule[", i, "]") + } + r.rules[i] = rule + } + return r, nil +} diff --git a/route/rule_item_cidr.go b/route/rule_item_cidr.go index b72d1e10b1..e17d87def8 100644 --- a/route/rule_item_cidr.go +++ b/route/rule_item_cidr.go @@ -31,7 +31,7 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { builder.Add(addr) continue } - return nil, E.Cause(err, "parse ip_cidr [", i, "]") + return nil, E.Cause(err, "parse [", i, "]") } var description string if isSource { @@ -57,8 +57,23 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { }, nil } +func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem { + var description string + if isSource { + description = "source_ipcidr=" + } else { + description = "ipcidr=" + } + description += "" + return &IPCIDRItem{ + ipSet: ipSet, + isSource: isSource, + description: description, + } +} + func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool { - if r.isSource { + if r.isSource || metadata.QueryType != 0 || metadata.IPCIDRMatchSource { return r.ipSet.Contains(metadata.Source.Addr) } else { if metadata.Destination.IsIP() { diff --git a/route/rule_item_domain.go b/route/rule_item_domain.go index 6602441deb..d2a11181b0 100644 --- a/route/rule_item_domain.go +++ b/route/rule_item_domain.go @@ -43,6 +43,13 @@ func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem { } } +func NewRawDomainItem(matcher *domain.Matcher) *DomainItem { + return &DomainItem{ + matcher, + "domain/domain_suffix=", + } +} + func (r *DomainItem) Match(metadata *adapter.InboundContext) bool { var domainHost string if metadata.Domain != "" { diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go new file mode 100644 index 0000000000..959b2f6110 --- /dev/null +++ b/route/rule_item_rule_set.go @@ -0,0 +1,55 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*RuleSetItem)(nil) + +type RuleSetItem struct { + router adapter.Router + tagList []string + setList []adapter.HeadlessRule + ipcidrMatchSource bool +} + +func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool) *RuleSetItem { + return &RuleSetItem{ + router: router, + tagList: tagList, + ipcidrMatchSource: ipCIDRMatchSource, + } +} + +func (r *RuleSetItem) Start() error { + for _, tag := range r.tagList { + ruleSet, loaded := r.router.RuleSet(tag) + if !loaded { + return E.New("rule-set not found: ", tag) + } + r.setList = append(r.setList, ruleSet) + } + return nil +} + +func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { + metadata.IPCIDRMatchSource = r.ipcidrMatchSource + for _, ruleSet := range r.setList { + if ruleSet.Match(metadata) { + return true + } + } + return false +} + +func (r *RuleSetItem) String() string { + if len(r.tagList) == 1 { + return F.ToString("rule_set=", r.tagList[0]) + } else { + return F.ToString("rule_set=[", strings.Join(r.tagList, " "), "]") + } +} diff --git a/route/rule_set.go b/route/rule_set.go new file mode 100644 index 0000000000..76c78c6214 --- /dev/null +++ b/route/rule_set.go @@ -0,0 +1,22 @@ +package route + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + 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" +) + +func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { + switch options.Type { + case C.RuleSetTypeLocal: + return NewLocalRuleSet(router, options) + case C.RuleSetTypeRemote: + return NewRemoteRuleSet(ctx, router, logger, options), nil + default: + return nil, E.New("unknown rule set type: ", options.Type) + } +} diff --git a/route/rule_set_local.go b/route/rule_set_local.go new file mode 100644 index 0000000000..ccdb1704a8 --- /dev/null +++ b/route/rule_set_local.go @@ -0,0 +1,69 @@ +package route + +import ( + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ adapter.RuleSet = (*LocalRuleSet)(nil) + +type LocalRuleSet struct { + rules []adapter.HeadlessRule +} + +func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { + setFile, err := os.Open(options.LocalOptions.Path) + if err != nil { + return nil, err + } + var plainRuleSet option.PlainRuleSet + switch options.Format { + case C.RuleSetFormatSource, "": + var compat option.PlainRuleSetCompat + decoder := json.NewDecoder(json.NewCommentFilter(setFile)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&compat) + if err != nil { + return nil, err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + plainRuleSet, err = srs.Read(setFile, false) + if err != nil { + return nil, err + } + default: + return nil, E.New("unknown rule set format: ", options.Format) + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(router, ruleOptions) + if err != nil { + return nil, E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + return &LocalRuleSet{rules}, nil +} + +func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} + +func (s *LocalRuleSet) Start() error { + return nil +} + +func (s *LocalRuleSet) Close() error { + return nil +} diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go new file mode 100644 index 0000000000..d9102320dd --- /dev/null +++ b/route/rule_set_remote.go @@ -0,0 +1,218 @@ +package route + +import ( + "bytes" + "context" + "io" + "net" + "net/http" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + 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" + "github.com/sagernet/sing/service" +) + +var _ adapter.RuleSet = (*RemoteRuleSet)(nil) + +type RemoteRuleSet struct { + ctx context.Context + cancel context.CancelFunc + router adapter.Router + logger logger.ContextLogger + options option.RuleSet + updateInterval time.Duration + dialer N.Dialer + rules []adapter.HeadlessRule + lastUpdated time.Time + lastEtag string + updateTicker *time.Ticker +} + +func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { + ctx, cancel := context.WithCancel(ctx) + var updateInterval time.Duration + if options.RemoteOptions.UpdateInterval > 0 { + updateInterval = time.Duration(options.RemoteOptions.UpdateInterval) + } else { + updateInterval = 12 * time.Hour + } + return &RemoteRuleSet{ + ctx: ctx, + cancel: cancel, + router: router, + logger: logger, + options: options, + updateInterval: updateInterval, + } +} + +func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} + +func (s *RemoteRuleSet) Start() error { + var dialer N.Dialer + if s.options.RemoteOptions.DownloadDetour != "" { + outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour) + if !loaded { + return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour) + } + dialer = outbound + } else { + outbound, err := s.router.DefaultOutbound(N.NetworkTCP) + if err != nil { + return err + } + dialer = outbound + } + s.dialer = dialer + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + if savedSet := cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil { + err := s.loadBytes(savedSet.Content) + if err != nil { + return E.Cause(err, "restore cached rule-set") + } + s.lastUpdated = savedSet.LastUpdated + s.lastEtag = savedSet.LastEtag + } + } + if s.lastUpdated.IsZero() || time.Since(s.lastUpdated) > s.updateInterval { + err := s.fetchOnce() + if err != nil { + return E.Cause(err, "fetch rule-set ", s.options.Tag) + } + } + s.updateTicker = time.NewTicker(s.updateInterval) + go s.loopUpdate() + return nil +} + +func (s *RemoteRuleSet) loadBytes(content []byte) error { + var ( + plainRuleSet option.PlainRuleSet + err error + ) + switch s.options.Format { + case C.RuleSetFormatSource, "": + var compat option.PlainRuleSetCompat + decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content))) + decoder.DisallowUnknownFields() + err = decoder.Decode(&compat) + if err != nil { + return err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + plainRuleSet, err = srs.Read(bytes.NewReader(content), false) + if err != nil { + return err + } + default: + return E.New("unknown rule set format: ", s.options.Format) + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(s.router, ruleOptions) + if err != nil { + return E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + s.rules = rules + return nil +} + +func (s *RemoteRuleSet) loopUpdate() { + for { + select { + case <-s.ctx.Done(): + return + case <-s.updateTicker.C: + err := s.fetchOnce() + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } + } + } +} + +func (s *RemoteRuleSet) fetchOnce() error { + s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL) + httpClient := &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + }, + } + defer httpClient.CloseIdleConnections() + request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil) + if err != nil { + return err + } + if s.lastEtag != "" { + request.Header.Set("If-None-Match", s.lastEtag) + } + response, err := httpClient.Do(request.WithContext(s.ctx)) + if err != nil { + return err + } + switch response.StatusCode { + case http.StatusOK: + case http.StatusNotModified: + s.logger.Info("update rule-set ", s.options.Tag, ": not modified") + return nil + default: + return E.New("unexpected status: ", response.Status) + } + content, err := io.ReadAll(response.Body) + if err != nil { + response.Body.Close() + return err + } + err = s.loadBytes(content) + if err != nil { + response.Body.Close() + return err + } + response.Body.Close() + eTagHeader := response.Header.Get("Etag") + if eTagHeader != "" { + s.lastEtag = eTagHeader + } + s.lastUpdated = time.Now() + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err = cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedRuleSet{ + LastUpdated: s.lastUpdated, + Content: content, + LastEtag: s.lastEtag, + }) + if err != nil { + s.logger.Error("save rule-set cache: ", err) + } + } + s.logger.Info("updated rule-set ", s.options.Tag) + return nil +} + +func (s *RemoteRuleSet) Close() error { + s.updateTicker.Stop() + s.cancel() + return nil +} From 65c9254c6b465fa10dc1f7002bdd133a918346f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 29 Nov 2023 17:35:56 +0800 Subject: [PATCH 019/103] documentation: Add rule set --- docs/configuration/dns/rule.md | 32 ++- docs/configuration/dns/rule.zh.md | 32 ++- docs/configuration/experimental/cache-file.md | 34 +++ docs/configuration/experimental/clash-api.md | 121 ++++++++++ docs/configuration/experimental/index.md | 145 ++---------- docs/configuration/experimental/index.zh.md | 137 ------------ docs/configuration/experimental/v2ray-api.md | 50 +++++ docs/configuration/route/geoip.md | 8 + docs/configuration/route/geoip.zh.md | 33 --- docs/configuration/route/geosite.md | 8 + docs/configuration/route/geosite.zh.md | 33 --- docs/configuration/route/index.md | 30 ++- docs/configuration/route/index.zh.md | 32 ++- docs/configuration/route/rule.md | 46 +++- docs/configuration/route/rule.zh.md | 44 +++- docs/configuration/rule-set/headless-rule.md | 207 ++++++++++++++++++ docs/configuration/rule-set/index.md | 97 ++++++++ docs/configuration/rule-set/source-format.md | 34 +++ docs/migration.md | 187 ++++++++++++++++ mkdocs.yml | 28 ++- 20 files changed, 980 insertions(+), 358 deletions(-) create mode 100644 docs/configuration/experimental/cache-file.md create mode 100644 docs/configuration/experimental/clash-api.md delete mode 100644 docs/configuration/experimental/index.zh.md create mode 100644 docs/configuration/experimental/v2ray-api.md delete mode 100644 docs/configuration/route/geoip.zh.md delete mode 100644 docs/configuration/route/geosite.zh.md create mode 100644 docs/configuration/rule-set/headless-rule.md create mode 100644 docs/configuration/rule-set/index.md create mode 100644 docs/configuration/rule-set/source-format.md create mode 100644 docs/migration.md diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 18e352b407..896a3b4468 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -1,3 +1,13 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -85,6 +95,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": [ "direct" @@ -166,15 +180,23 @@ Match domain using regular expression. #### geosite +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + Match geosite. #### source_geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + Match source geoip. #### source_ip_cidr -Match source ip cidr. +Match source IP CIDR. #### source_port @@ -250,6 +272,12 @@ Match WiFi SSID. Match WiFi BSSID. +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [Rule Set](/configuration/route/#rule_set). + #### invert Invert match result. @@ -286,4 +314,4 @@ Rewrite TTL in DNS responses. #### rules -Included default rules. \ No newline at end of file +Included rules. \ No newline at end of file diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 98bfa8ab9a..f990ed3ec5 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -1,3 +1,13 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -84,6 +94,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": [ "direct" @@ -163,10 +177,18 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### geosite -匹配 GeoSite。 +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-set)。 + +匹配 Geosite。 #### source_geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + 匹配源 GeoIP。 #### source_ip_cidr @@ -245,6 +267,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配 WiFi BSSID。 +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + #### invert 反选匹配结果。 @@ -281,4 +309,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### rules -包括的默认规则。 \ No newline at end of file +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md new file mode 100644 index 0000000000..66e30ef9b0 --- /dev/null +++ b/docs/configuration/experimental/cache-file.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "enabled": true, + "path": "", + "cache_id": "", + "store_fakeip": false +} +``` + +### Fields + +#### enabled + +Enable cache file. + +#### path + +Path to the cache file. + +`cache.db` will be used if empty. + +#### cache_id + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. diff --git a/docs/configuration/experimental/clash-api.md b/docs/configuration/experimental/clash-api.md new file mode 100644 index 0000000000..a06fe15480 --- /dev/null +++ b/docs/configuration/experimental/clash-api.md @@ -0,0 +1,121 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-delete-alert: [store_mode](#store_mode) + :material-delete-alert: [store_selected](#store_selected) + :material-delete-alert: [store_fakeip](#store_fakeip) + :material-delete-alert: [cache_file](#cache_file) + :material-delete-alert: [cache_id](#cache_id) + + +!!! quote "" + + Clash API is not included by default, see [Installation](./#installation). + +### Structure + +```json +{ + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" +} +``` + +### Fields + +#### external_controller + +RESTful web API listening address. Clash API will be disabled if empty. + +#### external_ui + +A relative path to the configuration directory or an absolute path to a +directory in which you put some static web resource. sing-box will then +serve it at `http://{{external-controller}}/ui`. + + + +#### external_ui_download_url + +ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. + +`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. + +#### external_ui_download_detour + +The tag of the outbound to download the external UI. + +Default outbound will be used if empty. + +#### secret + +Secret for the RESTful API (optional) +Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` +ALWAYS set a secret if RESTful API is listening on 0.0.0.0 + +#### default_mode + +Default mode in clash, `Rule` will be used if empty. + +This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. + +#### store_mode + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_mode` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +Store Clash mode in cache file. + +#### store_selected + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +!!! note "" + + The tag must be set for target outbounds. + +Store selected outbound for the `Selector` outbound in cache file. + +#### store_fakeip + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and migrated to `cache_file.store_fakeip`. + +Store fakeip in cache file. + +#### cache_file + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_file` is deprecated in Clash API and migrated to `cache_file.enabled` and `cache_file.path`. + +Cache file path, `cache.db` will be used if empty. + +#### cache_id + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_id` is deprecated in Clash API and migrated to `cache_file.cache_id`. + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. \ No newline at end of file diff --git a/docs/configuration/experimental/index.md b/docs/configuration/experimental/index.md index 308e851c3b..1057e59b36 100644 --- a/docs/configuration/experimental/index.md +++ b/docs/configuration/experimental/index.md @@ -1,139 +1,30 @@ +--- +icon: material/alert-decagram +--- + # Experimental +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [cache_file](#cache_file) + :material-alert-decagram: [clash_api](#clash_api) + ### Structure ```json { "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" - }, - "v2ray_api": { - "listen": "127.0.0.1:8080", - "stats": { - "enabled": true, - "inbounds": [ - "socks-in" - ], - "outbounds": [ - "proxy", - "direct" - ], - "users": [ - "sekai" - ] - } - } + "cache_file": {}, + "clash_api": {}, + "v2ray_api": {} } } ``` -!!! note "" - - Traffic statistics and connection management can degrade performance. - -### Clash API Fields - -!!! quote "" - - Clash API is not included by default, see [Installation](./#installation). - -#### external_controller - -RESTful web API listening address. Clash API will be disabled if empty. - -#### external_ui - -A relative path to the configuration directory or an absolute path to a -directory in which you put some static web resource. sing-box will then -serve it at `http://{{external-controller}}/ui`. - -#### external_ui_download_url - -ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. - -`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. - -#### external_ui_download_detour - -The tag of the outbound to download the external UI. - -Default outbound will be used if empty. - -#### secret - -Secret for the RESTful API (optional) -Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` -ALWAYS set a secret if RESTful API is listening on 0.0.0.0 - -#### default_mode - -Default mode in clash, `Rule` will be used if empty. - -This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. - -#### store_mode - -Store Clash mode in cache file. - -#### store_selected - -!!! note "" - - The tag must be set for target outbounds. - -Store selected outbound for the `Selector` outbound in cache file. - -#### store_fakeip - -Store fakeip in cache file. - -#### cache_file - -Cache file path, `cache.db` will be used if empty. - -#### cache_id - -Cache ID. - -If not empty, `store_selected` will use a separate store keyed by it. - -### V2Ray API Fields - -!!! quote "" - - V2Ray API is not included by default, see [Installation](./#installation). - -#### listen - -gRPC API listening address. V2Ray API will be disabled if empty. - -#### stats - -Traffic statistics service settings. - -#### stats.enabled - -Enable statistics service. - -#### stats.inbounds - -Inbound list to count traffic. - -#### stats.outbounds - -Outbound list to count traffic. - -#### stats.users +### Fields -User list to count traffic. \ No newline at end of file +| Key | Format | +|--------------|----------------------------| +| `cache_file` | [Cache File](./cache-file) | +| `clash_api` | [Clash API](./clash-api) | +| `v2ray_api` | [V2Ray API](./v2ray-api) | \ No newline at end of file diff --git a/docs/configuration/experimental/index.zh.md b/docs/configuration/experimental/index.zh.md deleted file mode 100644 index 88a95852c9..0000000000 --- a/docs/configuration/experimental/index.zh.md +++ /dev/null @@ -1,137 +0,0 @@ -# 实验性 - -### 结构 - -```json -{ - "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" - }, - "v2ray_api": { - "listen": "127.0.0.1:8080", - "stats": { - "enabled": true, - "inbounds": [ - "socks-in" - ], - "outbounds": [ - "proxy", - "direct" - ], - "users": [ - "sekai" - ] - } - } - } -} -``` - -!!! note "" - - 流量统计和连接管理会降低性能。 - -### Clash API 字段 - -!!! quote "" - - 默认安装不包含 Clash API,参阅 [安装](/zh/#_2)。 - -#### external_controller - -RESTful web API 监听地址。如果为空,则禁用 Clash API。 - -#### external_ui - -到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。 - -#### external_ui_download_url - -静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。 - -默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。 - -#### external_ui_download_detour - -用于下载静态网页资源的出站的标签。 - -如果为空,将使用默认出站。 - -#### secret - -RESTful API 的密钥(可选) -通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证 -如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。 - -#### default_mode - -Clash 中的默认模式,默认使用 `Rule`。 - -此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。 - -#### store_mode - -将 Clash 模式存储在缓存文件中。 - -#### store_selected - -!!! note "" - - 必须为目标出站设置标签。 - -将 `Selector` 中出站的选定的目标出站存储在缓存文件中。 - -#### store_fakeip - -将 fakeip 存储在缓存文件中。 - -#### cache_file - -缓存文件路径,默认使用`cache.db`。 - -#### cache_id - -缓存 ID。 - -如果不为空,`store_selected` 将会使用以此为键的独立存储。 - -### V2Ray API 字段 - -!!! quote "" - - 默认安装不包含 V2Ray API,参阅 [安装](/zh/#_2)。 - -#### listen - -gRPC API 监听地址。如果为空,则禁用 V2Ray API。 - -#### stats - -流量统计服务设置。 - -#### stats.enabled - -启用统计服务。 - -#### stats.inbounds - -统计流量的入站列表。 - -#### stats.outbounds - -统计流量的出站列表。 - -#### stats.users - -统计流量的用户列表。 \ No newline at end of file diff --git a/docs/configuration/experimental/v2ray-api.md b/docs/configuration/experimental/v2ray-api.md new file mode 100644 index 0000000000..398884242e --- /dev/null +++ b/docs/configuration/experimental/v2ray-api.md @@ -0,0 +1,50 @@ +### Structure + +!!! quote "" + + V2Ray API is not included by default, see [Installation](./#installation). + +```json +{ + "listen": "127.0.0.1:8080", + "stats": { + "enabled": true, + "inbounds": [ + "socks-in" + ], + "outbounds": [ + "proxy", + "direct" + ], + "users": [ + "sekai" + ] + } +} +``` + +### Fields + +#### listen + +gRPC API listening address. V2Ray API will be disabled if empty. + +#### stats + +Traffic statistics service settings. + +#### stats.enabled + +Enable statistics service. + +#### stats.inbounds + +Inbound list to count traffic. + +#### stats.outbounds + +Outbound list to count traffic. + +#### stats.users + +User list to count traffic. \ No newline at end of file diff --git a/docs/configuration/route/geoip.md b/docs/configuration/route/geoip.md index b966a292a5..d6dfd23289 100644 --- a/docs/configuration/route/geoip.md +++ b/docs/configuration/route/geoip.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + ### Structure ```json diff --git a/docs/configuration/route/geoip.zh.md b/docs/configuration/route/geoip.zh.md deleted file mode 100644 index 3ee7042728..0000000000 --- a/docs/configuration/route/geoip.zh.md +++ /dev/null @@ -1,33 +0,0 @@ -### 结构 - -```json -{ - "route": { - "geoip": { - "path": "", - "download_url": "", - "download_detour": "" - } - } -} -``` - -### 字段 - -#### path - -指定 GeoIP 资源的路径。 - -默认 `geoip.db`。 - -#### download_url - -指定 GeoIP 资源的下载链接。 - -默认为 `https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db`。 - -#### download_detour - -用于下载 GeoIP 资源的出站的标签。 - -如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/geosite.md b/docs/configuration/route/geosite.md index db700c6af1..515e86b1ec 100644 --- a/docs/configuration/route/geosite.md +++ b/docs/configuration/route/geosite.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + ### Structure ```json diff --git a/docs/configuration/route/geosite.zh.md b/docs/configuration/route/geosite.zh.md deleted file mode 100644 index bee81fbf71..0000000000 --- a/docs/configuration/route/geosite.zh.md +++ /dev/null @@ -1,33 +0,0 @@ -### 结构 - -```json -{ - "route": { - "geosite": { - "path": "", - "download_url": "", - "download_detour": "" - } - } -} -``` - -### 字段 - -#### path - -指定 GeoSite 资源的路径。 - -默认 `geosite.db`。 - -#### download_url - -指定 GeoSite 资源的下载链接。 - -默认为 `https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db`。 - -#### download_detour - -用于下载 GeoSite 资源的出站的标签。 - -如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 846d497330..7c1787eaea 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -1,5 +1,15 @@ +--- +icon: material/alert-decagram +--- + # Route +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -8,6 +18,7 @@ "geoip": {}, "geosite": {}, "rules": [], + "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, @@ -19,11 +30,20 @@ ### Fields -| Key | Format | -|------------|------------------------------------| -| `geoip` | [GeoIP](./geoip) | -| `geosite` | [Geosite](./geosite) | -| `rules` | List of [Route Rule](./rule) | +| Key | Format | +|-----------|----------------------| +| `geoip` | [GeoIP](./geoip) | +| `geosite` | [Geosite](./geosite) | + +#### rules + +List of [Route Rule](./rule) + +#### rule_set + +!!! question "Since sing-box 1.8.0" + +List of [Rule Set](/configuration/rule-set) #### final diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index 8bef5bea9a..b5302727a3 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -1,5 +1,15 @@ +--- +icon: material/alert-decagram +--- + # 路由 +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -7,8 +17,8 @@ "route": { "geoip": {}, "geosite": {}, - "ip_rules": [], "rules": [], + "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, @@ -20,11 +30,21 @@ ### 字段 -| 键 | 格式 | -|------------|-------------------------| -| `geoip` | [GeoIP](./geoip) | -| `geosite` | [GeoSite](./geosite) | -| `rules` | 一组 [路由规则](./rule) | +| 键 | 格式 | +|------------|-----------------------------------| +| `geoip` | [GeoIP](./geoip) | +| `geosite` | [Geosite](./geosite) | + + +#### rule + +一组 [路由规则](./rule)。 + +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +一组 [规则集](/configuration/rule-set)。 #### final diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index abbfde6fdc..35c1d8fc24 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -1,3 +1,15 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -89,6 +101,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": "direct" }, @@ -160,23 +176,35 @@ Match domain using regular expression. #### geosite +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + Match geosite. #### source_geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + Match source geoip. #### geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + Match geoip. #### source_ip_cidr -Match source ip cidr. +Match source IP CIDR. #### ip_cidr -Match ip cidr. +Match IP CIDR. #### source_port @@ -250,6 +278,18 @@ Match WiFi SSID. Match WiFi BSSID. +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [Rule Set](/configuration/route/#rule_set). + +#### rule_set_ipcidr_match_source + +!!! question "Since sing-box 1.8.0" + +Make `ipcidr` in rule sets match the source IP. + #### invert Invert match result. @@ -276,4 +316,4 @@ Tag of the target outbound. ==Required== -Included default rules. +Included rules. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index f4ab7890a7..31ee08d13a 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -1,3 +1,15 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -87,6 +99,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": "direct" }, @@ -158,14 +174,26 @@ #### geosite -匹配 GeoSite。 +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-set)。 + +匹配 Geosite。 #### source_geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + 匹配源 GeoIP。 #### geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + 匹配 GeoIP。 #### source_ip_cidr @@ -248,6 +276,18 @@ 匹配 WiFi BSSID。 +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + +#### rule_set_ipcidr_match_source + +!!! question "自 sing-box 1.8.0 起" + +使规则集中的 `ipcidr` 规则匹配源 IP。 + #### invert 反选匹配结果。 @@ -274,4 +314,4 @@ ==必填== -包括的默认规则。 \ No newline at end of file +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md new file mode 100644 index 0000000000..6ab62eb2e3 --- /dev/null +++ b/docs/configuration/rule-set/headless-rule.md @@ -0,0 +1,207 @@ +--- +icon: material/new-box +--- + +### Structure + +!!! question "Since sing-box 1.8.0" + +```json +{ + "rules": [ + { + "query_type": [ + "A", + "HTTPS", + 32768 + ], + "network": [ + "tcp" + ], + "domain": [ + "test.com" + ], + "domain_suffix": [ + ".cn" + ], + "domain_keyword": [ + "test" + ], + "domain_regex": [ + "^stun\\..+" + ], + "source_ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "source_port": [ + 12345 + ], + "source_port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "port": [ + 80, + 443 + ], + "port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "process_name": [ + "curl" + ], + "process_path": [ + "/usr/bin/curl" + ], + "package_name": [ + "com.termux" + ], + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], + "invert": false + }, + { + "type": "logical", + "mode": "and", + "rules": [], + "invert": false + } + ] +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Default Fields + +!!! note "" + + The default rule uses the following matching logic: + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) && + (`port` || `port_range`) && + (`source_port` || `source_port_range`) && + `other fields` + +#### query_type + +DNS query type. Values can be integers or type name strings. + +#### network + +`tcp` or `udp`. + +#### domain + +Match full domain. + +#### domain_suffix + +Match domain suffix. + +#### domain_keyword + +Match domain using keyword. + +#### domain_regex + +Match domain using regular expression. + +#### source_ip_cidr + +Match source IP CIDR. + +#### ip_cidr + +!!! info "" + + `ip_cidr` is an alias for `source_ip_cidr` when the Rule Set is used in DNS rules or `rule_set_ipcidr_match_source` enabled in route rules. + +Match IP CIDR. + +#### source_port + +Match source port. + +#### source_port_range + +Match source port range. + +#### port + +Match port. + +#### port_range + +Match port range. + +#### process_name + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process name. + +#### process_path + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path. + +#### package_name + +Match android package name. + +#### wifi_ssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi SSID. + +#### wifi_bssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi BSSID. + +#### invert + +Invert match result. + +### Logical Fields + +#### type + +`logical` + +#### mode + +==Required== + +`and` or `or` + +#### rules + +==Required== + +Included rules. diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md new file mode 100644 index 0000000000..5aff55b371 --- /dev/null +++ b/docs/configuration/rule-set/index.md @@ -0,0 +1,97 @@ +--- +icon: material/new-box +--- + +# Rule Set + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "type": "", + "tag": "", + "format": "", + + ... // Typed Fields +} +``` + +#### Local Structure + +```json +{ + "type": "local", + + ... + + "path": "" +} +``` + +#### Remote Structure + +!!! info "" + + Remote rule-set will be cached if `experimental.cache_file.enabled`. + +```json +{ + "type": "remote", + + ..., + + "url": "", + "download_detour": "", + "update_interval": "" +} +``` + +### Fields + +#### type + +==Required== + +Type of Rule Set, `local` or `remote`. + +#### tag + +==Required== + +Tag of Rule Set. + +#### format + +==Required== + +Format of Rule Set, `source` or `binary`. + +### Local Fields + +#### path + +==Required== + +File path of Rule Set. + +### Remote Fields + +#### url + +==Required== + +Download URL of Rule Set. + +#### download_detour + +Tag of the outbound to download rule-set. + +Default outbound will be used if empty. + +#### update_interval + +Update interval of Rule Set. + +`1d` will be used if empty. diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md new file mode 100644 index 0000000000..116c1ee66f --- /dev/null +++ b/docs/configuration/rule-set/source-format.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +# Source Format + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "version": 1, + "rules": [] +} +``` + +### Compile + +Use `sing-box rule-set compile [--output .srs] .json` to compile source to binary rule-set. + +### Fields + +#### version + +==Required== + +Version of Rule Set, must be `1`. + +#### rules + +==Required== + +List of [Headless Rule](./headless-rule.md). diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000000..8eb3daee42 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,187 @@ +--- +icon: material/arrange-bring-forward +--- + +# Migration + +## 1.8.0 + +!!! warning "Unstable" + + This version is still under development, and the following migration guide may be changed in the future. + +### :material-close-box: Migrate cache file from Clash API to independent options + +!!! info "Reference" + + [Clash API](/configuration/experimental/clash-api) / + [Cache File](/configuration/experimental/cache-file) + +=== ":material-card-remove: Deprecated" + + ```json + { + "experimental": { + "clash_api": { + "cache_file": "cache.db", // default value + "cahce_id": "my_profile2", + "store_mode": true, + "store_selected": true, + "store_fakeip": true + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "experimental" : { + "cache_file": { + "enabled": true, + "path": "cache.db", // default value + "cache_id": "my_profile2", + "store_fakeip": true + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate GeoIP to rule sets + +!!! info "Reference" + + [GeoIP](/configuration/route/geoip) / + [Route](/configuration/route) / + [Route Rule](/configuration/route/rule) / + [DNS Rule](/configuration/dns/rule) / + [Rule Set](/configuration/rule-set) + +!!! tip + + `sing-box geoip` commands can help you convert custom GeoIP into rule sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geoip": "cn", + "outbound": "direct" + }, + { + "source_geoip": "cn", + "outbound": "block" + } + ], + "geoip": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geoip-cn", + "outbound": "direct" + }, + { + "rule_set": "geoip-us", + "rule_set_ipcidr_match_source": true, + "outbound": "block" + } + ], + "rule_set": [ + { + "tag": "geoip-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", + "download_detour": "proxy" + }, + { + "tag": "geoip-us", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate Geosite to rule sets + +!!! info "Reference" + + [Geosite](/configuration/route/geosite) / + [Route](/configuration/route) / + [Route Rule](/configuration/route/rule) / + [DNS Rule](/configuration/dns/rule) / + [Rule Set](/configuration/rule-set) + +!!! tip + + `sing-box geosite` commands can help you convert custom Geosite into rule sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geosite": "cn", + "outbound": "direct" + } + ], + "geosite": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geosite-cn", + "outbound": "direct" + } + ], + "rule_set": [ + { + "tag": "geosite-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 1d4b1d8b0e..c5dd7df335 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,12 +32,16 @@ theme: - content.code.copy - content.code.select - content.code.annotate + icon: + admonition: + question: material/new-box nav: - Home: - index.md + - Change Log: changelog.md + - Migration: migration.md - Deprecated: deprecated.md - Support: support.md - - Change Log: changelog.md - Installation: - Package Manager: installation/package-manager.md - Docker: installation/docker.md @@ -56,7 +60,7 @@ nav: - Proxy: - Server: manual/proxy/server.md - Client: manual/proxy/client.md -# - TUN: manual/proxy/tun.md + # - TUN: manual/proxy/tun.md - Proxy Protocol: - Shadowsocks: manual/proxy-protocol/shadowsocks.md - Trojan: manual/proxy-protocol/trojan.md @@ -79,8 +83,15 @@ nav: - Geosite: configuration/route/geosite.md - Route Rule: configuration/route/rule.md - Protocol Sniff: configuration/route/sniff.md + - Rule Set: + - configuration/rule-set/index.md + - Source Format: configuration/rule-set/source-format.md + - Headless Rule: configuration/rule-set/headless-rule.md - Experimental: - configuration/experimental/index.md + - Cache File: configuration/experimental/cache-file.md + - Clash API: configuration/experimental/clash-api.md + - V2Ray API: configuration/experimental/v2ray-api.md - Shared: - Listen Fields: configuration/shared/listen.md - Dial Fields: configuration/shared/dial.md @@ -180,9 +191,10 @@ plugins: name: 简体中文 nav_translations: Home: 开始 + Change Log: 更新日志 + Migration: 迁移指南 Deprecated: 废弃功能列表 Support: 支持 - Change Log: 更新日志 Installation: 安装 Package Manager: 包管理器 @@ -203,6 +215,10 @@ plugins: Route Rule: 路由规则 Protocol Sniff: 协议探测 + Rule Set: 规则集 + Source Format: 源文件格式 + Headless Rule: 无头规则 + Experimental: 实验性 Shared: 通用 @@ -215,10 +231,6 @@ plugins: Inbound: 入站 Outbound: 出站 - FAQ: 常见问题 - Known Issues: 已知问题 - Examples: 示例 - Linux Server Installation: Linux 服务器安装 - DNS Hijack: DNS 劫持 + Manual: 手册 reconfigure_material: true reconfigure_search: true \ No newline at end of file From 40e364fb059e83438bcdde2f36d345e0d59240e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Nov 2023 12:00:28 +0800 Subject: [PATCH 020/103] Migrate to independent cache file --- adapter/experimental.go | 10 ++- box.go | 53 ++++++++++-- .../{clashapi => }/cachefile/cache.go | 85 +++++++++++++------ .../{clashapi => }/cachefile/fakeip.go | 0 experimental/clashapi/cache.go | 11 ++- experimental/clashapi/server.go | 66 ++++---------- experimental/libbox/command_group.go | 21 ++--- experimental/libbox/command_urltest.go | 21 ++--- option/clash.go | 31 ------- option/experimental.go | 47 +++++++++- option/group.go | 15 ++++ option/v2ray.go | 12 --- outbound/builder.go | 2 +- outbound/selector.go | 15 ++-- route/router.go | 2 +- transport/fakeip/store.go | 15 ++-- 16 files changed, 227 insertions(+), 179 deletions(-) rename experimental/{clashapi => }/cachefile/cache.go (81%) rename experimental/{clashapi => }/cachefile/fakeip.go (100%) delete mode 100644 option/clash.go create mode 100644 option/group.go diff --git a/adapter/experimental.go b/adapter/experimental.go index 697fa9f199..ac523e4e74 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -13,15 +13,17 @@ type ClashServer interface { PreStarter Mode() string ModeList() []string - StoreSelected() bool - StoreFakeIP() bool - CacheFile() ClashCacheFile HistoryStorage() *urltest.HistoryStorage RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker) } -type ClashCacheFile interface { +type CacheFile interface { + Service + PreStarter + + StoreFakeIP() bool + LoadMode() string StoreMode(mode string) error LoadSelected(group string) string diff --git a/box.go b/box.go index c10222ddf7..d3838ca337 100644 --- a/box.go +++ b/box.go @@ -10,6 +10,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/inbound" "github.com/sagernet/sing-box/log" @@ -32,7 +33,8 @@ type Box struct { outbounds []adapter.Outbound logFactory log.Factory logger log.ContextLogger - preServices map[string]adapter.Service + preServices1 map[string]adapter.Service + preServices2 map[string]adapter.Service postServices map[string]adapter.Service done chan struct{} } @@ -45,17 +47,21 @@ type Options struct { } func New(options Options) (*Box, error) { + createdAt := time.Now() ctx := options.Context if ctx == nil { ctx = context.Background() } ctx = service.ContextWithDefaultRegistry(ctx) ctx = pause.ContextWithDefaultManager(ctx) - createdAt := time.Now() experimentalOptions := common.PtrValueOrDefault(options.Experimental) applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) + var needCacheFile bool var needClashAPI bool var needV2RayAPI bool + if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil { + needCacheFile = true + } if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil { needClashAPI = true } @@ -145,8 +151,14 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "initialize platform interface") } } - preServices := make(map[string]adapter.Service) + preServices1 := make(map[string]adapter.Service) + preServices2 := make(map[string]adapter.Service) postServices := make(map[string]adapter.Service) + if needCacheFile { + cacheFile := cachefile.NewCacheFile(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) + preServices1["cache file"] = cacheFile + service.MustRegister[adapter.CacheFile](ctx, cacheFile) + } if needClashAPI { clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options) @@ -155,7 +167,7 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "create clash api server") } router.SetClashServer(clashServer) - preServices["clash api"] = clashServer + preServices2["clash api"] = clashServer } if needV2RayAPI { v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI)) @@ -163,7 +175,7 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "create v2ray api server") } router.SetV2RayServer(v2rayServer) - preServices["v2ray api"] = v2rayServer + preServices2["v2ray api"] = v2rayServer } return &Box{ router: router, @@ -172,7 +184,8 @@ func New(options Options) (*Box, error) { createdAt: createdAt, logFactory: logFactory, logger: logFactory.Logger(), - preServices: preServices, + preServices1: preServices1, + preServices2: preServices2, postServices: postServices, done: make(chan struct{}), }, nil @@ -217,7 +230,16 @@ func (s *Box) Start() error { } func (s *Box) preStart() error { - for serviceName, service := range s.preServices { + for serviceName, service := range s.preServices1 { + if preService, isPreService := service.(adapter.PreStarter); isPreService { + s.logger.Trace("pre-start ", serviceName) + err := preService.PreStart() + if err != nil { + return E.Cause(err, "pre-starting ", serviceName) + } + } + } + for serviceName, service := range s.preServices2 { if preService, isPreService := service.(adapter.PreStarter); isPreService { s.logger.Trace("pre-start ", serviceName) err := preService.PreStart() @@ -238,7 +260,14 @@ func (s *Box) start() error { if err != nil { return err } - for serviceName, service := range s.preServices { + for serviceName, service := range s.preServices1 { + s.logger.Trace("starting ", serviceName) + err = service.Start() + if err != nil { + return E.Cause(err, "start ", serviceName) + } + } + for serviceName, service := range s.preServices2 { s.logger.Trace("starting ", serviceName) err = service.Start() if err != nil { @@ -313,7 +342,13 @@ func (s *Box) Close() error { return E.Cause(err, "close router") }) } - for serviceName, service := range s.preServices { + for serviceName, service := range s.preServices1 { + s.logger.Trace("closing ", serviceName) + errors = E.Append(errors, service.Close(), func(err error) error { + return E.Cause(err, "close ", serviceName) + }) + } + for serviceName, service := range s.preServices2 { s.logger.Trace("closing ", serviceName) errors = E.Append(errors, service.Close(), func(err error) error { return E.Cause(err, "close ", serviceName) diff --git a/experimental/clashapi/cachefile/cache.go b/experimental/cachefile/cache.go similarity index 81% rename from experimental/clashapi/cachefile/cache.go rename to experimental/cachefile/cache.go index 0911829749..2616ded5c1 100644 --- a/experimental/clashapi/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/bbolt" bboltErrors "github.com/sagernet/bbolt/errors" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service/filemanager" @@ -31,11 +32,15 @@ var ( cacheIDDefault = []byte("default") ) -var _ adapter.ClashCacheFile = (*CacheFile)(nil) +var _ adapter.CacheFile = (*CacheFile)(nil) type CacheFile struct { + ctx context.Context + path string + cacheID []byte + storeFakeIP bool + DB *bbolt.DB - cacheID []byte saveAccess sync.RWMutex saveDomain map[netip.Addr]string saveAddress4 map[string]netip.Addr @@ -43,7 +48,29 @@ type CacheFile struct { saveMetadataTimer *time.Timer } -func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) { +func NewCacheFile(ctx context.Context, options option.CacheFileOptions) *CacheFile { + var path string + if options.Path != "" { + path = options.Path + } else { + path = "cache.db" + } + var cacheIDBytes []byte + if options.CacheID != "" { + cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...) + } + return &CacheFile{ + ctx: ctx, + path: filemanager.BasePath(ctx, path), + cacheID: cacheIDBytes, + storeFakeIP: options.StoreFakeIP, + saveDomain: make(map[netip.Addr]string), + saveAddress4: make(map[string]netip.Addr), + saveAddress6: make(map[string]netip.Addr), + } +} + +func (c *CacheFile) start() error { const fileMode = 0o666 options := bbolt.Options{Timeout: time.Second} var ( @@ -51,7 +78,7 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) err error ) for i := 0; i < 10; i++ { - db, err = bbolt.Open(path, fileMode, &options) + db, err = bbolt.Open(c.path, fileMode, &options) if err == nil { break } @@ -59,23 +86,20 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) continue } if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) { - rmErr := os.Remove(path) + rmErr := os.Remove(c.path) if rmErr != nil { - return nil, err + return err } } time.Sleep(100 * time.Millisecond) } if err != nil { - return nil, err + return err } - err = filemanager.Chown(ctx, path) + err = filemanager.Chown(c.ctx, c.path) if err != nil { - return nil, E.Cause(err, "platform chown") - } - var cacheIDBytes []byte - if cacheID != "" { - cacheIDBytes = append([]byte{0}, []byte(cacheID)...) + db.Close() + return E.Cause(err, "platform chown") } err = db.Batch(func(tx *bbolt.Tx) error { return tx.ForEach(func(name []byte, b *bbolt.Bucket) error { @@ -97,15 +121,30 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) }) }) if err != nil { - return nil, err + db.Close() + return err } - return &CacheFile{ - DB: db, - cacheID: cacheIDBytes, - saveDomain: make(map[netip.Addr]string), - saveAddress4: make(map[string]netip.Addr), - saveAddress6: make(map[string]netip.Addr), - }, nil + c.DB = db + return nil +} + +func (c *CacheFile) PreStart() error { + return c.start() +} + +func (c *CacheFile) Start() error { + return nil +} + +func (c *CacheFile) Close() error { + if c.DB == nil { + return nil + } + return c.DB.Close() +} + +func (c *CacheFile) StoreFakeIP() bool { + return c.storeFakeIP } func (c *CacheFile) LoadMode() string { @@ -218,7 +257,3 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { } }) } - -func (c *CacheFile) Close() error { - return c.DB.Close() -} diff --git a/experimental/clashapi/cachefile/fakeip.go b/experimental/cachefile/fakeip.go similarity index 100% rename from experimental/clashapi/cachefile/fakeip.go rename to experimental/cachefile/fakeip.go diff --git a/experimental/clashapi/cache.go b/experimental/clashapi/cache.go index 7582fde5ae..9c088a82f7 100644 --- a/experimental/clashapi/cache.go +++ b/experimental/clashapi/cache.go @@ -1,23 +1,26 @@ package clashapi import ( + "context" "net/http" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/service" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) -func cacheRouter(router adapter.Router) http.Handler { +func cacheRouter(ctx context.Context) http.Handler { r := chi.NewRouter() - r.Post("/fakeip/flush", flushFakeip(router)) + r.Post("/fakeip/flush", flushFakeip(ctx)) return r } -func flushFakeip(router adapter.Router) func(w http.ResponseWriter, r *http.Request) { +func flushFakeip(ctx context.Context) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if cacheFile := router.ClashServer().CacheFile(); cacheFile != nil { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile != nil { err := cacheFile.FakeIPReset() if err != nil { render.Status(r, http.StatusInternalServerError) diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 6a3d6f66f7..c40ff93846 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -15,7 +15,6 @@ import ( "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" - "github.com/sagernet/sing-box/experimental/clashapi/cachefile" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -49,12 +48,6 @@ type Server struct { mode string modeList []string modeUpdateHook chan<- struct{} - storeMode bool - storeSelected bool - storeFakeIP bool - cacheFilePath string - cacheID string - cacheFile adapter.ClashCacheFile externalController bool externalUI string @@ -76,9 +69,6 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ trafficManager: trafficManager, modeList: options.ModeList, externalController: options.ExternalController != "", - storeMode: options.StoreMode, - storeSelected: options.StoreSelected, - storeFakeIP: options.StoreFakeIP, externalUIDownloadURL: options.ExternalUIDownloadURL, externalUIDownloadDetour: options.ExternalUIDownloadDetour, } @@ -94,18 +84,10 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ server.modeList = append([]string{defaultMode}, server.modeList...) } server.mode = defaultMode - if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.ExternalController == "" { - cachePath := os.ExpandEnv(options.CacheFile) - if cachePath == "" { - cachePath = "cache.db" - } - if foundPath, loaded := C.FindPath(cachePath); loaded { - cachePath = foundPath - } else { - cachePath = filemanager.BasePath(ctx, cachePath) - } - server.cacheFilePath = cachePath - server.cacheID = options.CacheID + //goland:noinspection GoDeprecation + //nolint:staticcheck + if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" { + return nil, E.New("cache_file and related fields in Clash API is deprecated in sing-box 1.8.0, use experimental.cache_file instead.") } cors := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, @@ -128,7 +110,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/script", scriptRouter()) r.Mount("/profile", profileRouter()) - r.Mount("/cache", cacheRouter(router)) + r.Mount("/cache", cacheRouter(ctx)) r.Mount("/dns", dnsRouter(router)) server.setupMetaAPI(r) @@ -147,19 +129,13 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ } func (s *Server) PreStart() error { - if s.cacheFilePath != "" { - cacheFile, err := cachefile.Open(s.ctx, s.cacheFilePath, s.cacheID) - if err != nil { - return E.Cause(err, "open cache file") - } - s.cacheFile = cacheFile - if s.storeMode { - mode := s.cacheFile.LoadMode() - if common.Any(s.modeList, func(it string) bool { - return strings.EqualFold(it, mode) - }) { - s.mode = mode - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + mode := cacheFile.LoadMode() + if common.Any(s.modeList, func(it string) bool { + return strings.EqualFold(it, mode) + }) { + s.mode = mode } } return nil @@ -187,7 +163,6 @@ func (s *Server) Close() error { return common.Close( common.PtrOrNil(s.httpServer), s.trafficManager, - s.cacheFile, s.urlTestHistory, ) } @@ -224,8 +199,9 @@ func (s *Server) SetMode(newMode string) { } } s.router.ClearDNSCache() - if s.storeMode { - err := s.cacheFile.StoreMode(newMode) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreMode(newMode) if err != nil { s.logger.Error(E.Cause(err, "save mode")) } @@ -233,18 +209,6 @@ func (s *Server) SetMode(newMode string) { s.logger.Info("updated mode: ", newMode) } -func (s *Server) StoreSelected() bool { - return s.storeSelected -} - -func (s *Server) StoreFakeIP() bool { - return s.storeFakeIP -} - -func (s *Server) CacheFile() adapter.ClashCacheFile { - return s.cacheFile -} - func (s *Server) HistoryStorage() *urltest.HistoryStorage { return s.urlTestHistory } diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go index 934820889e..2fc69b98b4 100644 --- a/experimental/libbox/command_group.go +++ b/experimental/libbox/command_group.go @@ -159,11 +159,7 @@ func readGroups(reader io.Reader) (OutboundGroupIterator, error) { func writeGroups(writer io.Writer, boxService *BoxService) error { historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx) - var cacheFile adapter.ClashCacheFile - if clashServer := boxService.instance.Router().ClashServer(); clashServer != nil { - cacheFile = clashServer.CacheFile() - } - + cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx) outbounds := boxService.instance.Router().Outbounds() var iGroups []adapter.OutboundGroup for _, it := range outbounds { @@ -288,16 +284,15 @@ func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return writeError(conn, E.New("service not ready")) } - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - err = cacheFile.StoreGroupExpand(groupTag, isExpand) - if err != nil { - return writeError(conn, err) - } + cacheFile := service.FromContext[adapter.CacheFile](serviceNow.ctx) + if cacheFile != nil { + err = cacheFile.StoreGroupExpand(groupTag, isExpand) + if err != nil { + return writeError(conn, err) } } return writeError(conn, nil) diff --git a/experimental/libbox/command_urltest.go b/experimental/libbox/command_urltest.go index 88e86a8f8c..3563d8c6a3 100644 --- a/experimental/libbox/command_urltest.go +++ b/experimental/libbox/command_urltest.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/service" ) func (c *CommandClient) URLTest(groupTag string) error { @@ -37,11 +38,11 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return nil } - abstractOutboundGroup, isLoaded := service.instance.Router().Outbound(groupTag) + abstractOutboundGroup, isLoaded := serviceNow.instance.Router().Outbound(groupTag) if !isLoaded { return writeError(conn, E.New("outbound group not found: ", groupTag)) } @@ -53,15 +54,9 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if isURLTest { go urlTest.CheckOutbounds() } else { - var historyStorage *urltest.HistoryStorage - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - historyStorage = clashServer.HistoryStorage() - } else { - return writeError(conn, E.New("Clash API is required for URLTest on non-URLTest group")) - } - + historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx) outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { - itOutbound, _ := service.instance.Router().Outbound(it) + itOutbound, _ := serviceNow.instance.Router().Outbound(it) return itOutbound }), func(it adapter.Outbound) bool { if it == nil { @@ -73,12 +68,12 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { } return true }) - b, _ := batch.New(service.ctx, batch.WithConcurrencyNum[any](10)) + b, _ := batch.New(serviceNow.ctx, batch.WithConcurrencyNum[any](10)) for _, detour := range outbounds { outboundToTest := detour outboundTag := outboundToTest.Tag() b.Go(outboundTag, func() (any, error) { - t, err := urltest.URLTest(service.ctx, "", outboundToTest) + t, err := urltest.URLTest(serviceNow.ctx, "", outboundToTest) if err != nil { historyStorage.DeleteURLTestHistory(outboundTag) } else { diff --git a/option/clash.go b/option/clash.go deleted file mode 100644 index 63ee2aebdf..0000000000 --- a/option/clash.go +++ /dev/null @@ -1,31 +0,0 @@ -package option - -type ClashAPIOptions struct { - ExternalController string `json:"external_controller,omitempty"` - ExternalUI string `json:"external_ui,omitempty"` - ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` - ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` - Secret string `json:"secret,omitempty"` - DefaultMode string `json:"default_mode,omitempty"` - StoreMode bool `json:"store_mode,omitempty"` - StoreSelected bool `json:"store_selected,omitempty"` - StoreFakeIP bool `json:"store_fakeip,omitempty"` - CacheFile string `json:"cache_file,omitempty"` - CacheID string `json:"cache_id,omitempty"` - - ModeList []string `json:"-"` -} - -type SelectorOutboundOptions struct { - Outbounds []string `json:"outbounds"` - Default string `json:"default,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} - -type URLTestOutboundOptions struct { - Outbounds []string `json:"outbounds"` - URL string `json:"url,omitempty"` - Interval Duration `json:"interval,omitempty"` - Tolerance uint16 `json:"tolerance,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} diff --git a/option/experimental.go b/option/experimental.go index a5b6acbd32..72751a590e 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -1,7 +1,48 @@ package option type ExperimentalOptions struct { - ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` - V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` - Debug *DebugOptions `json:"debug,omitempty"` + CacheFile *CacheFileOptions `json:"cache_file,omitempty"` + ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` + V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` + Debug *DebugOptions `json:"debug,omitempty"` +} + +type CacheFileOptions struct { + Enabled bool `json:"enabled,omitempty"` + Path string `json:"path,omitempty"` + CacheID string `json:"cache_id,omitempty"` + StoreFakeIP bool `json:"store_fakeip,omitempty"` +} + +type ClashAPIOptions struct { + ExternalController string `json:"external_controller,omitempty"` + ExternalUI string `json:"external_ui,omitempty"` + ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` + ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` + Secret string `json:"secret,omitempty"` + DefaultMode string `json:"default_mode,omitempty"` + ModeList []string `json:"-"` + + // Deprecated: migrated to global cache file + StoreMode bool `json:"store_mode,omitempty"` + // Deprecated: migrated to global cache file + StoreSelected bool `json:"store_selected,omitempty"` + // Deprecated: migrated to global cache file + StoreFakeIP bool `json:"store_fakeip,omitempty"` + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` +} + +type V2RayAPIOptions struct { + Listen string `json:"listen,omitempty"` + Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` +} + +type V2RayStatsServiceOptions struct { + Enabled bool `json:"enabled,omitempty"` + Inbounds []string `json:"inbounds,omitempty"` + Outbounds []string `json:"outbounds,omitempty"` + Users []string `json:"users,omitempty"` } diff --git a/option/group.go b/option/group.go new file mode 100644 index 0000000000..58824e808b --- /dev/null +++ b/option/group.go @@ -0,0 +1,15 @@ +package option + +type SelectorOutboundOptions struct { + Outbounds []string `json:"outbounds"` + Default string `json:"default,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} + +type URLTestOutboundOptions struct { + Outbounds []string `json:"outbounds"` + URL string `json:"url,omitempty"` + Interval Duration `json:"interval,omitempty"` + Tolerance uint16 `json:"tolerance,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} diff --git a/option/v2ray.go b/option/v2ray.go index 37f7b8c4b0..774a651d1d 100644 --- a/option/v2ray.go +++ b/option/v2ray.go @@ -1,13 +1 @@ package option - -type V2RayAPIOptions struct { - Listen string `json:"listen,omitempty"` - Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` -} - -type V2RayStatsServiceOptions struct { - Enabled bool `json:"enabled,omitempty"` - Inbounds []string `json:"inbounds,omitempty"` - Outbounds []string `json:"outbounds,omitempty"` - Users []string `json:"users,omitempty"` -} diff --git a/outbound/builder.go b/outbound/builder.go index 141758d870..e4d6a80e06 100644 --- a/outbound/builder.go +++ b/outbound/builder.go @@ -56,7 +56,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t case C.TypeHysteria2: return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options) case C.TypeSelector: - return NewSelector(router, logger, tag, options.SelectorOptions) + return NewSelector(ctx, router, logger, tag, options.SelectorOptions) case C.TypeURLTest: return NewURLTest(ctx, router, logger, tag, options.URLTestOptions) default: diff --git a/outbound/selector.go b/outbound/selector.go index c66591cdee..e801daeadc 100644 --- a/outbound/selector.go +++ b/outbound/selector.go @@ -12,6 +12,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" ) var ( @@ -21,6 +22,7 @@ var ( type Selector struct { myOutboundAdapter + ctx context.Context tags []string defaultTag string outbounds map[string]adapter.Outbound @@ -29,7 +31,7 @@ type Selector struct { interruptExternalConnections bool } -func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { +func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { outbound := &Selector{ myOutboundAdapter: myOutboundAdapter{ protocol: C.TypeSelector, @@ -38,6 +40,7 @@ func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, op tag: tag, dependencies: options.Outbounds, }, + ctx: ctx, tags: options.Outbounds, defaultTag: options.Default, outbounds: make(map[string]adapter.Outbound), @@ -67,8 +70,9 @@ func (s *Selector) Start() error { } if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - selected := clashServer.CacheFile().LoadSelected(s.tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + selected := cacheFile.LoadSelected(s.tag) if selected != "" { detour, loaded := s.outbounds[selected] if loaded { @@ -110,8 +114,9 @@ func (s *Selector) SelectOutbound(tag string) bool { } s.selected = detour if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - err := clashServer.CacheFile().StoreSelected(s.tag, tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreSelected(s.tag, tag) if err != nil { s.logger.Error("store selected: ", err) } diff --git a/route/router.go b/route/router.go index 972c4ec048..e50a51a955 100644 --- a/route/router.go +++ b/route/router.go @@ -261,7 +261,7 @@ func NewRouter( if fakeIPOptions.Inet6Range != nil { inet6Range = *fakeIPOptions.Inet6Range } - router.fakeIPStore = fakeip.NewStore(router, router.logger, inet4Range, inet6Range) + router.fakeIPStore = fakeip.NewStore(ctx, router.logger, inet4Range, inet6Range) } usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor() diff --git a/transport/fakeip/store.go b/transport/fakeip/store.go index 96f6bf031e..83677b0d05 100644 --- a/transport/fakeip/store.go +++ b/transport/fakeip/store.go @@ -1,17 +1,19 @@ package fakeip import ( + "context" "net/netip" "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" ) var _ adapter.FakeIPStore = (*Store)(nil) type Store struct { - router adapter.Router + ctx context.Context logger logger.Logger inet4Range netip.Prefix inet6Range netip.Prefix @@ -20,9 +22,9 @@ type Store struct { inet6Current netip.Addr } -func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { +func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { return &Store{ - router: router, + ctx: ctx, logger: logger, inet4Range: inet4Range, inet6Range: inet6Range, @@ -31,10 +33,9 @@ func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Pref func (s *Store) Start() error { var storage adapter.FakeIPStorage - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreFakeIP() { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - storage = cacheFile - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil && cacheFile.StoreFakeIP() { + storage = cacheFile } if storage == nil { storage = NewMemoryStorage() From 554f9942b0ba8ba9d46136b4e8e9a09f4e200741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Nov 2023 23:47:32 +0800 Subject: [PATCH 021/103] Allow nested logical rules --- experimental/clashapi.go | 42 +++++++++++++++++++++++------------ option/rule.go | 21 +++++++++++++----- option/rule_dns.go | 25 +++++++++++++++------ route/router.go | 4 ++-- route/router_geo_resources.go | 12 ++++------ route/rule_default.go | 8 +++---- route/rule_dns.go | 8 +++---- 7 files changed, 76 insertions(+), 44 deletions(-) diff --git a/experimental/clashapi.go b/experimental/clashapi.go index 894d40a74a..805fbd5be7 100644 --- a/experimental/clashapi.go +++ b/experimental/clashapi.go @@ -5,6 +5,7 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" @@ -27,24 +28,37 @@ func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.O func CalculateClashModeList(options option.Options) []string { var clashMode []string - for _, dnsRule := range common.PtrValueOrDefault(options.DNS).Rules { - if dnsRule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, dnsRule.DefaultOptions.ClashMode) { - clashMode = append(clashMode, dnsRule.DefaultOptions.ClashMode) - } - for _, defaultRule := range dnsRule.LogicalOptions.Rules { - if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) { - clashMode = append(clashMode, defaultRule.ClashMode) + clashMode = append(clashMode, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...) + clashMode = append(clashMode, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...) + clashMode = common.FilterNotDefault(common.Uniq(clashMode)) + return clashMode +} + +func extraClashModeFromRule(rules []option.Rule) []string { + var clashMode []string + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if rule.DefaultOptions.ClashMode != "" { + clashMode = append(clashMode, rule.DefaultOptions.ClashMode) } + case C.RuleTypeLogical: + clashMode = append(clashMode, extraClashModeFromRule(rule.LogicalOptions.Rules)...) } } - for _, rule := range common.PtrValueOrDefault(options.Route).Rules { - if rule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, rule.DefaultOptions.ClashMode) { - clashMode = append(clashMode, rule.DefaultOptions.ClashMode) - } - for _, defaultRule := range rule.LogicalOptions.Rules { - if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) { - clashMode = append(clashMode, defaultRule.ClashMode) + return clashMode +} + +func extraClashModeFromDNSRule(rules []option.DNSRule) []string { + var clashMode []string + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if rule.DefaultOptions.ClashMode != "" { + clashMode = append(clashMode, rule.DefaultOptions.ClashMode) } + case C.RuleTypeLogical: + clashMode = append(clashMode, extraClashModeFromDNSRule(rule.LogicalOptions.Rules)...) } } return clashMode diff --git a/option/rule.go b/option/rule.go index 8caba96e57..4f4042025f 100644 --- a/option/rule.go +++ b/option/rule.go @@ -53,6 +53,17 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error { return nil } +func (r Rule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + type DefaultRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` @@ -92,12 +103,12 @@ func (r DefaultRule) IsValid() bool { } type LogicalRule struct { - Mode string `json:"mode"` - Rules []DefaultRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Mode string `json:"mode"` + Rules []Rule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` } func (r LogicalRule) IsValid() bool { - return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid) + return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid) } diff --git a/option/rule_dns.go b/option/rule_dns.go index ba572b9aa2..fca3432293 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -53,6 +53,17 @@ func (r *DNSRule) UnmarshalJSON(bytes []byte) error { return nil } +func (r DNSRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown DNS rule type: " + r.Type) + } +} + type DefaultDNSRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` @@ -96,14 +107,14 @@ func (r DefaultDNSRule) IsValid() bool { } type LogicalDNSRule struct { - Mode string `json:"mode"` - Rules []DefaultDNSRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + Mode string `json:"mode"` + Rules []DNSRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Server string `json:"server,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` } func (r LogicalDNSRule) IsValid() bool { - return len(r.Rules) > 0 && common.All(r.Rules, DefaultDNSRule.IsValid) + return len(r.Rules) > 0 && common.All(r.Rules, DNSRule.IsValid) } diff --git a/route/router.go b/route/router.go index e50a51a955..b48d2346d9 100644 --- a/route/router.go +++ b/route/router.go @@ -127,14 +127,14 @@ func NewRouter( Logger: router.dnsLogger, }) for i, ruleOptions := range options.Rules { - routeRule, err := NewRule(router, router.logger, ruleOptions) + routeRule, err := NewRule(router, router.logger, ruleOptions, true) if err != nil { return nil, E.Cause(err, "parse rule[", i, "]") } router.rules = append(router.rules, routeRule) } for i, dnsRuleOptions := range dnsOptions.Rules { - dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions) + dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions, true) if err != nil { return nil, E.Cause(err, "parse dns rule[", i, "]") } diff --git a/route/router_geo_resources.go b/route/router_geo_resources.go index 8715cf922c..638d00df62 100644 --- a/route/router_geo_resources.go +++ b/route/router_geo_resources.go @@ -252,10 +252,8 @@ func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool return true } case C.RuleTypeLogical: - for _, subRule := range rule.LogicalOptions.Rules { - if cond(subRule) { - return true - } + if hasRule(rule.LogicalOptions.Rules, cond) { + return true } } } @@ -270,10 +268,8 @@ func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bo return true } case C.RuleTypeLogical: - for _, subRule := range rule.LogicalOptions.Rules { - if cond(subRule) { - return true - } + if hasDNSRule(rule.LogicalOptions.Rules, cond) { + return true } } } diff --git a/route/rule_default.go b/route/rule_default.go index 2d62f97a46..8c8473abf5 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -8,13 +8,13 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule) (adapter.Rule, error) { +func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule, checkOutbound bool) (adapter.Rule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - if options.DefaultOptions.Outbound == "" { + if options.DefaultOptions.Outbound == "" && checkOutbound { return nil, E.New("missing outbound field") } return NewDefaultRule(router, logger, options.DefaultOptions) @@ -22,7 +22,7 @@ func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rul if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - if options.LogicalOptions.Outbound == "" { + if options.LogicalOptions.Outbound == "" && checkOutbound { return nil, E.New("missing outbound field") } return NewLogicalRule(router, logger, options.LogicalOptions) @@ -220,7 +220,7 @@ func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options opt return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDefaultRule(router, logger, subRule) + rule, err := NewRule(router, logger, subRule, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } diff --git a/route/rule_dns.go b/route/rule_dns.go index 5132f02456..b444932582 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -8,13 +8,13 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule) (adapter.DNSRule, error) { +func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule, checkServer bool) (adapter.DNSRule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - if options.DefaultOptions.Server == "" { + if options.DefaultOptions.Server == "" && checkServer { return nil, E.New("missing server field") } return NewDefaultDNSRule(router, logger, options.DefaultOptions) @@ -22,7 +22,7 @@ func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option. if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - if options.LogicalOptions.Server == "" { + if options.LogicalOptions.Server == "" && checkServer { return nil, E.New("missing server field") } return NewLogicalDNSRule(router, logger, options.LogicalOptions) @@ -228,7 +228,7 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDefaultDNSRule(router, logger, subRule) + rule, err := NewDNSRule(router, logger, subRule, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } From 92b904dd2eead7b3426b0918389f636e9aee5fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 29 Nov 2023 17:35:40 +0800 Subject: [PATCH 022/103] Add rule set --- .gitignore | 1 + adapter/experimental.go | 66 ++- adapter/inbound.go | 1 + adapter/router.go | 15 +- cmd/sing-box/cmd_format.go | 39 -- cmd/sing-box/cmd_geoip.go | 43 ++ cmd/sing-box/cmd_geoip_export.go | 98 ++++ cmd/sing-box/cmd_geoip_list.go | 31 ++ cmd/sing-box/cmd_geoip_lookup.go | 47 ++ cmd/sing-box/cmd_geosite.go | 41 ++ cmd/sing-box/cmd_geosite_export.go | 81 +++ cmd/sing-box/cmd_geosite_list.go | 50 ++ cmd/sing-box/cmd_geosite_lookup.go | 97 ++++ cmd/sing-box/cmd_geosite_matcher.go | 56 ++ cmd/sing-box/cmd_merge.go | 2 +- cmd/sing-box/cmd_rule_set.go | 14 + cmd/sing-box/cmd_rule_set_compile.go | 80 +++ cmd/sing-box/cmd_rule_set_format.go | 87 ++++ cmd/sing-box/cmd_tools.go | 6 +- cmd/sing-box/cmd_tools_connect.go | 2 +- common/dialer/router.go | 12 +- common/srs/binary.go | 485 ++++++++++++++++++ common/srs/ip_set.go | 116 +++++ constant/rule.go | 8 + experimental/cachefile/cache.go | 35 ++ experimental/cachefile/fakeip.go | 2 +- experimental/clashapi/proxies.go | 6 +- experimental/clashapi/server_resources.go | 6 +- .../clashapi/trafficontrol/tracker.go | 8 +- go.mod | 2 +- go.sum | 4 +- option/experimental.go | 8 +- option/route.go | 1 + option/rule.go | 58 ++- option/rule_dns.go | 1 + option/rule_set.go | 230 +++++++++ option/types.go | 8 + route/router.go | 34 +- route/rule_abstract.go | 22 +- route/rule_default.go | 7 +- route/rule_dns.go | 7 +- route/rule_headless.go | 173 +++++++ route/rule_item_cidr.go | 19 +- route/rule_item_domain.go | 7 + route/rule_item_rule_set.go | 55 ++ route/rule_set.go | 22 + route/rule_set_local.go | 69 +++ route/rule_set_remote.go | 218 ++++++++ 48 files changed, 2375 insertions(+), 105 deletions(-) create mode 100644 cmd/sing-box/cmd_geoip.go create mode 100644 cmd/sing-box/cmd_geoip_export.go create mode 100644 cmd/sing-box/cmd_geoip_list.go create mode 100644 cmd/sing-box/cmd_geoip_lookup.go create mode 100644 cmd/sing-box/cmd_geosite.go create mode 100644 cmd/sing-box/cmd_geosite_export.go create mode 100644 cmd/sing-box/cmd_geosite_list.go create mode 100644 cmd/sing-box/cmd_geosite_lookup.go create mode 100644 cmd/sing-box/cmd_geosite_matcher.go create mode 100644 cmd/sing-box/cmd_rule_set.go create mode 100644 cmd/sing-box/cmd_rule_set_compile.go create mode 100644 cmd/sing-box/cmd_rule_set_format.go create mode 100644 common/srs/binary.go create mode 100644 common/srs/ip_set.go create mode 100644 option/rule_set.go create mode 100644 route/rule_headless.go create mode 100644 route/rule_item_rule_set.go create mode 100644 route/rule_set.go create mode 100644 route/rule_set_local.go create mode 100644 route/rule_set_remote.go diff --git a/.gitignore b/.gitignore index 6630f428ce..55bdab3a0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.idea/ /vendor/ /*.json +/*.srs /*.db /site/ /bin/ diff --git a/adapter/experimental.go b/adapter/experimental.go index ac523e4e74..c26ee8c1cf 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -1,11 +1,16 @@ package adapter import ( + "bytes" "context" + "encoding/binary" + "io" "net" + "time" "github.com/sagernet/sing-box/common/urltest" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" ) type ClashServer interface { @@ -23,6 +28,7 @@ type CacheFile interface { PreStarter StoreFakeIP() bool + FakeIPStorage LoadMode() string StoreMode(mode string) error @@ -30,7 +36,65 @@ type CacheFile interface { StoreSelected(group string, selected string) error LoadGroupExpand(group string) (isExpand bool, loaded bool) StoreGroupExpand(group string, expand bool) error - FakeIPStorage + LoadRuleSet(tag string) *SavedRuleSet + SaveRuleSet(tag string, set *SavedRuleSet) error +} + +type SavedRuleSet struct { + Content []byte + LastUpdated time.Time + LastEtag string +} + +func (s *SavedRuleSet) MarshalBinary() ([]byte, error) { + var buffer bytes.Buffer + err := binary.Write(&buffer, binary.BigEndian, uint8(1)) + if err != nil { + return nil, err + } + err = rw.WriteUVariant(&buffer, uint64(len(s.Content))) + if err != nil { + return nil, err + } + buffer.Write(s.Content) + err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix()) + if err != nil { + return nil, err + } + err = rw.WriteVString(&buffer, s.LastEtag) + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func (s *SavedRuleSet) UnmarshalBinary(data []byte) error { + reader := bytes.NewReader(data) + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return err + } + contentLen, err := rw.ReadUVariant(reader) + if err != nil { + return err + } + s.Content = make([]byte, contentLen) + _, err = io.ReadFull(reader, s.Content) + if err != nil { + return err + } + var lastUpdated int64 + err = binary.Read(reader, binary.BigEndian, &lastUpdated) + if err != nil { + return err + } + s.LastUpdated = time.Unix(lastUpdated, 0) + s.LastEtag, err = rw.ReadVString(reader) + if err != nil { + return err + } + return nil } type Tracker interface { diff --git a/adapter/inbound.go b/adapter/inbound.go index 2d24083c4a..30dec9d1f7 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -47,6 +47,7 @@ type InboundContext struct { GeoIPCode string ProcessInfo *process.Info FakeIP bool + IPCIDRMatchSource bool // dns cache diff --git a/adapter/router.go b/adapter/router.go index e4c3904d51..ca4d6547c4 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -18,7 +18,7 @@ type Router interface { Outbounds() []Outbound Outbound(tag string) (Outbound, bool) - DefaultOutbound(network string) Outbound + DefaultOutbound(network string) (Outbound, error) FakeIPStore() FakeIPStore @@ -27,6 +27,8 @@ type Router interface { GeoIPReader() *geoip.Reader LoadGeosite(code string) (Rule, error) + RuleSet(tag string) (RuleSet, bool) + Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) @@ -61,11 +63,15 @@ func RouterFromContext(ctx context.Context) Router { return service.FromContext[Router](ctx) } +type HeadlessRule interface { + Match(metadata *InboundContext) bool +} + type Rule interface { + HeadlessRule Service Type() string UpdateGeosite() error - Match(metadata *InboundContext) bool Outbound() string String() string } @@ -76,6 +82,11 @@ type DNSRule interface { RewriteTTL() *uint32 } +type RuleSet interface { + HeadlessRule + Service +} + type InterfaceUpdateListener interface { InterfaceUpdated() } diff --git a/cmd/sing-box/cmd_format.go b/cmd/sing-box/cmd_format.go index 10a5497cd4..c5e939e4a9 100644 --- a/cmd/sing-box/cmd_format.go +++ b/cmd/sing-box/cmd_format.go @@ -7,7 +7,6 @@ import ( "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/spf13/cobra" @@ -69,41 +68,3 @@ func format() error { } return nil } - -func formatOne(configPath string) error { - configContent, err := os.ReadFile(configPath) - if err != nil { - return E.Cause(err, "read config") - } - var options option.Options - err = options.UnmarshalJSON(configContent) - if err != nil { - return E.Cause(err, "decode config") - } - buffer := new(bytes.Buffer) - encoder := json.NewEncoder(buffer) - encoder.SetIndent("", " ") - err = encoder.Encode(options) - if err != nil { - return E.Cause(err, "encode config") - } - if !commandFormatFlagWrite { - os.Stdout.WriteString(buffer.String() + "\n") - return nil - } - if bytes.Equal(configContent, buffer.Bytes()) { - return nil - } - output, err := os.Create(configPath) - if err != nil { - return E.Cause(err, "open output") - } - _, err = output.Write(buffer.Bytes()) - output.Close() - if err != nil { - return E.Cause(err, "write output") - } - outputPath, _ := filepath.Abs(configPath) - os.Stderr.WriteString(outputPath + "\n") - return nil -} diff --git a/cmd/sing-box/cmd_geoip.go b/cmd/sing-box/cmd_geoip.go new file mode 100644 index 0000000000..dbbbff135e --- /dev/null +++ b/cmd/sing-box/cmd_geoip.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var ( + geoipReader *maxminddb.Reader + commandGeoIPFlagFile string +) + +var commandGeoip = &cobra.Command{ + Use: "geoip", + Short: "GeoIP tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geoipPreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.PersistentFlags().StringVarP(&commandGeoIPFlagFile, "file", "f", "geoip.db", "geoip file") + mainCommand.AddCommand(commandGeoip) +} + +func geoipPreRun() error { + reader, err := maxminddb.Open(commandGeoIPFlagFile) + if err != nil { + return err + } + if reader.Metadata.DatabaseType != "sing-geoip" { + reader.Close() + return E.New("incorrect database type, expected sing-geoip, got ", reader.Metadata.DatabaseType) + } + geoipReader = reader + return nil +} diff --git a/cmd/sing-box/cmd_geoip_export.go b/cmd/sing-box/cmd_geoip_export.go new file mode 100644 index 0000000000..d170d10b72 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_export.go @@ -0,0 +1,98 @@ +package main + +import ( + "io" + "net" + "os" + "strings" + + "github.com/sagernet/sing-box/common/json" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var flagGeoipExportOutput string + +const flagGeoipExportDefaultOutput = "geoip-.srs" + +var commandGeoipExport = &cobra.Command{ + Use: "export ", + Short: "Export geoip country as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoipExport.Flags().StringVarP(&flagGeoipExportOutput, "output", "o", flagGeoipExportDefaultOutput, "Output path") + commandGeoip.AddCommand(commandGeoipExport) +} + +func geoipExport(countryCode string) error { + networks := geoipReader.Networks(maxminddb.SkipAliasedNetworks) + countryMap := make(map[string][]*net.IPNet) + var ( + ipNet *net.IPNet + nextCountryCode string + err error + ) + for networks.Next() { + ipNet, err = networks.Network(&nextCountryCode) + if err != nil { + return err + } + countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet) + } + ipNets := countryMap[strings.ToLower(countryCode)] + if len(ipNets) == 0 { + return E.New("country code not found: ", countryCode) + } + + var ( + outputFile *os.File + outputWriter io.Writer + ) + if flagGeoipExportOutput == "stdout" { + outputWriter = os.Stdout + } else if flagGeoipExportOutput == flagGeoipExportDefaultOutput { + outputFile, err = os.Create("geoip-" + countryCode + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(flagGeoipExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + headlessRule.IPCIDR = make([]string, 0, len(ipNets)) + for _, cidr := range ipNets { + headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String()) + } + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geoip_list.go b/cmd/sing-box/cmd_geoip_list.go new file mode 100644 index 0000000000..54dd426ea9 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_list.go @@ -0,0 +1,31 @@ +package main + +import ( + "os" + + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var commandGeoipList = &cobra.Command{ + Use: "list", + Short: "List geoip country codes", + Run: func(cmd *cobra.Command, args []string) { + err := listGeoip() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipList) +} + +func listGeoip() error { + for _, code := range geoipReader.Metadata.Languages { + os.Stdout.WriteString(code + "\n") + } + return nil +} diff --git a/cmd/sing-box/cmd_geoip_lookup.go b/cmd/sing-box/cmd_geoip_lookup.go new file mode 100644 index 0000000000..d5157bb404 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_lookup.go @@ -0,0 +1,47 @@ +package main + +import ( + "net/netip" + "os" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + + "github.com/spf13/cobra" +) + +var commandGeoipLookup = &cobra.Command{ + Use: "lookup
", + Short: "Lookup if an IP address is contained in the GeoIP database", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipLookup(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipLookup) +} + +func geoipLookup(address string) error { + addr, err := netip.ParseAddr(address) + if err != nil { + return E.Cause(err, "parse address") + } + if !N.IsPublicAddr(addr) { + os.Stdout.WriteString("private\n") + return nil + } + var code string + _ = geoipReader.Lookup(addr.AsSlice(), &code) + if code != "" { + os.Stdout.WriteString(code + "\n") + return nil + } + os.Stdout.WriteString("unknown\n") + return nil +} diff --git a/cmd/sing-box/cmd_geosite.go b/cmd/sing-box/cmd_geosite.go new file mode 100644 index 0000000000..95db935797 --- /dev/null +++ b/cmd/sing-box/cmd_geosite.go @@ -0,0 +1,41 @@ +package main + +import ( + "github.com/sagernet/sing-box/common/geosite" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var ( + commandGeoSiteFlagFile string + geositeReader *geosite.Reader + geositeCodeList []string +) + +var commandGeoSite = &cobra.Command{ + Use: "geosite", + Short: "Geosite tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geositePreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.PersistentFlags().StringVarP(&commandGeoSiteFlagFile, "file", "f", "geosite.db", "geosite file") + mainCommand.AddCommand(commandGeoSite) +} + +func geositePreRun() error { + reader, codeList, err := geosite.Open(commandGeoSiteFlagFile) + if err != nil { + return E.Cause(err, "open geosite file") + } + geositeReader = reader + geositeCodeList = codeList + return nil +} diff --git a/cmd/sing-box/cmd_geosite_export.go b/cmd/sing-box/cmd_geosite_export.go new file mode 100644 index 0000000000..71f1018d8f --- /dev/null +++ b/cmd/sing-box/cmd_geosite_export.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/json" + "io" + "os" + + "github.com/sagernet/sing-box/common/geosite" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "github.com/spf13/cobra" +) + +var commandGeositeExportOutput string + +const commandGeositeExportDefaultOutput = "geosite-.json" + +var commandGeositeExport = &cobra.Command{ + Use: "export ", + Short: "Export geosite category as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geositeExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeositeExport.Flags().StringVarP(&commandGeositeExportOutput, "output", "o", commandGeositeExportDefaultOutput, "Output path") + commandGeoSite.AddCommand(commandGeositeExport) +} + +func geositeExport(category string) error { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + var ( + outputFile *os.File + outputWriter io.Writer + ) + if commandGeositeExportOutput == "stdout" { + outputWriter = os.Stdout + } else if commandGeositeExportOutput == commandGeositeExportDefaultOutput { + outputFile, err = os.Create("geosite-" + category + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(commandGeositeExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + defaultRule := geosite.Compile(sourceSet) + headlessRule.Domain = defaultRule.Domain + headlessRule.DomainSuffix = defaultRule.DomainSuffix + headlessRule.DomainKeyword = defaultRule.DomainKeyword + headlessRule.DomainRegex = defaultRule.DomainRegex + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geosite_list.go b/cmd/sing-box/cmd_geosite_list.go new file mode 100644 index 0000000000..cedb7adfd2 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_list.go @@ -0,0 +1,50 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + F "github.com/sagernet/sing/common/format" + + "github.com/spf13/cobra" +) + +var commandGeositeList = &cobra.Command{ + Use: "list ", + Short: "List geosite categories", + Run: func(cmd *cobra.Command, args []string) { + err := geositeList() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeList) +} + +func geositeList() error { + var geositeEntry []struct { + category string + items int + } + for _, category := range geositeCodeList { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + geositeEntry = append(geositeEntry, struct { + category string + items int + }{category, len(sourceSet)}) + } + sort.SliceStable(geositeEntry, func(i, j int) bool { + return geositeEntry[i].items < geositeEntry[j].items + }) + for _, entry := range geositeEntry { + os.Stdout.WriteString(F.ToString(entry.category, " (", entry.items, ")\n")) + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_lookup.go b/cmd/sing-box/cmd_geosite_lookup.go new file mode 100644 index 0000000000..f648ce62a4 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_lookup.go @@ -0,0 +1,97 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandGeositeLookup = &cobra.Command{ + Use: "lookup [category] ", + Short: "Check if a domain is in the geosite", + Args: cobra.RangeArgs(1, 2), + Run: func(cmd *cobra.Command, args []string) { + var ( + source string + target string + ) + switch len(args) { + case 1: + target = args[0] + case 2: + source = args[0] + target = args[1] + } + err := geositeLookup(source, target) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeLookup) +} + +func geositeLookup(source string, target string) error { + var sourceMatcherList []struct { + code string + matcher *searchGeositeMatcher + } + if source != "" { + sourceSet, err := geositeReader.Read(source) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+source) + } + sourceMatcherList = []struct { + code string + matcher *searchGeositeMatcher + }{ + { + code: source, + matcher: sourceMatcher, + }, + } + + } else { + for _, code := range geositeCodeList { + sourceSet, err := geositeReader.Read(code) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+code) + } + sourceMatcherList = append(sourceMatcherList, struct { + code string + matcher *searchGeositeMatcher + }{ + code: code, + matcher: sourceMatcher, + }) + } + } + sort.SliceStable(sourceMatcherList, func(i, j int) bool { + return sourceMatcherList[i].code < sourceMatcherList[j].code + }) + + for _, matcherItem := range sourceMatcherList { + if matchRule := matcherItem.matcher.Match(target); matchRule != "" { + os.Stdout.WriteString("Match code (") + os.Stdout.WriteString(matcherItem.code) + os.Stdout.WriteString(") ") + os.Stdout.WriteString(matchRule) + os.Stdout.WriteString("\n") + } + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_matcher.go b/cmd/sing-box/cmd_geosite_matcher.go new file mode 100644 index 0000000000..791dba2499 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_matcher.go @@ -0,0 +1,56 @@ +package main + +import ( + "regexp" + "strings" + + "github.com/sagernet/sing-box/common/geosite" +) + +type searchGeositeMatcher struct { + domainMap map[string]bool + suffixList []string + keywordList []string + regexList []string +} + +func newSearchGeositeMatcher(items []geosite.Item) (*searchGeositeMatcher, error) { + options := geosite.Compile(items) + domainMap := make(map[string]bool) + for _, domain := range options.Domain { + domainMap[domain] = true + } + rule := &searchGeositeMatcher{ + domainMap: domainMap, + suffixList: options.DomainSuffix, + keywordList: options.DomainKeyword, + regexList: options.DomainRegex, + } + return rule, nil +} + +func (r *searchGeositeMatcher) Match(domain string) string { + if r.domainMap[domain] { + return "domain=" + domain + } + for _, suffix := range r.suffixList { + if strings.HasSuffix(domain, suffix) { + return "domain_suffix=" + suffix + } + } + for _, keyword := range r.keywordList { + if strings.Contains(domain, keyword) { + return "domain_keyword=" + keyword + } + } + for _, regexStr := range r.regexList { + regex, err := regexp.Compile(regexStr) + if err != nil { + continue + } + if regex.MatchString(domain) { + return "domain_regex=" + regexStr + } + } + return "" +} diff --git a/cmd/sing-box/cmd_merge.go b/cmd/sing-box/cmd_merge.go index 0aff750182..4fb07b8688 100644 --- a/cmd/sing-box/cmd_merge.go +++ b/cmd/sing-box/cmd_merge.go @@ -18,7 +18,7 @@ import ( ) var commandMerge = &cobra.Command{ - Use: "merge [output]", + Use: "merge ", Short: "Merge configurations", Run: func(cmd *cobra.Command, args []string) { err := merge(args[0]) diff --git a/cmd/sing-box/cmd_rule_set.go b/cmd/sing-box/cmd_rule_set.go new file mode 100644 index 0000000000..f4112a087b --- /dev/null +++ b/cmd/sing-box/cmd_rule_set.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +var commandRuleSet = &cobra.Command{ + Use: "rule-set", + Short: "Manage rule sets", +} + +func init() { + mainCommand.AddCommand(commandRuleSet) +} diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go new file mode 100644 index 0000000000..de318095ac --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -0,0 +1,80 @@ +package main + +import ( + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "github.com/spf13/cobra" +) + +var flagRuleSetCompileOutput string + +const flagRuleSetCompileDefaultOutput = ".srs" + +var commandRuleSetCompile = &cobra.Command{ + Use: "compile [source-path]", + Short: "Compile rule-set json to binary", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := compileRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSet.AddCommand(commandRuleSetCompile) + commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file") +} + +func compileRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + decoder := json.NewDecoder(json.NewCommentFilter(reader)) + decoder.DisallowUnknownFields() + var plainRuleSet option.PlainRuleSetCompat + err = decoder.Decode(&plainRuleSet) + if err != nil { + return err + } + ruleSet := plainRuleSet.Upgrade() + var outputPath string + if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { + if strings.HasSuffix(sourcePath, ".json") { + outputPath = sourcePath[:len(sourcePath)-5] + ".srs" + } else { + outputPath = sourcePath + ".srs" + } + } else { + outputPath = flagRuleSetCompileOutput + } + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + err = srs.Write(outputFile, ruleSet) + if err != nil { + outputFile.Close() + os.Remove(outputPath) + return err + } + outputFile.Close() + return nil +} diff --git a/cmd/sing-box/cmd_rule_set_format.go b/cmd/sing-box/cmd_rule_set_format.go new file mode 100644 index 0000000000..dc3ee6aabd --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_format.go @@ -0,0 +1,87 @@ +package main + +import ( + "bytes" + "io" + "os" + "path/filepath" + + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandRuleSetFormatFlagWrite bool + +var commandRuleSetFormat = &cobra.Command{ + Use: "format ", + Short: "Format rule-set json", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := formatRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") + commandRuleSet.AddCommand(commandRuleSetFormat) +} + +func formatRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + content, err := io.ReadAll(reader) + if err != nil { + return err + } + decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content))) + decoder.DisallowUnknownFields() + var plainRuleSet option.PlainRuleSetCompat + err = decoder.Decode(&plainRuleSet) + if err != nil { + return err + } + ruleSet := plainRuleSet.Upgrade() + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(ruleSet) + if err != nil { + return E.Cause(err, "encode config") + } + outputPath, _ := filepath.Abs(sourcePath) + if !commandRuleSetFormatFlagWrite || sourcePath == "stdin" { + os.Stdout.WriteString(buffer.String() + "\n") + return nil + } + if bytes.Equal(content, buffer.Bytes()) { + return nil + } + output, err := os.Create(sourcePath) + if err != nil { + return E.Cause(err, "open output") + } + _, err = output.Write(buffer.Bytes()) + output.Close() + if err != nil { + return E.Cause(err, "write output") + } + os.Stderr.WriteString(outputPath + "\n") + return nil +} diff --git a/cmd/sing-box/cmd_tools.go b/cmd/sing-box/cmd_tools.go index 460a50cd1c..c45f585576 100644 --- a/cmd/sing-box/cmd_tools.go +++ b/cmd/sing-box/cmd_tools.go @@ -38,11 +38,7 @@ func createPreStartedClient() (*box.Box, error) { func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) { if outboundTag == "" { - outbound := instance.Router().DefaultOutbound(N.NetworkName(network)) - if outbound == nil { - return nil, E.New("missing default outbound") - } - return outbound, nil + return instance.Router().DefaultOutbound(N.NetworkName(network)) } else { outbound, loaded := instance.Router().Outbound(outboundTag) if !loaded { diff --git a/cmd/sing-box/cmd_tools_connect.go b/cmd/sing-box/cmd_tools_connect.go index b904ebc9f5..3ea04bcd40 100644 --- a/cmd/sing-box/cmd_tools_connect.go +++ b/cmd/sing-box/cmd_tools_connect.go @@ -18,7 +18,7 @@ import ( var commandConnectFlagNetwork string var commandConnect = &cobra.Command{ - Use: "connect [address]", + Use: "connect
", Short: "Connect to an address", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { diff --git a/common/dialer/router.go b/common/dialer/router.go index 1d5586546e..2531607753 100644 --- a/common/dialer/router.go +++ b/common/dialer/router.go @@ -18,11 +18,19 @@ func NewRouter(router adapter.Router) N.Dialer { } func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - return d.router.DefaultOutbound(network).DialContext(ctx, network, destination) + dialer, err := d.router.DefaultOutbound(network) + if err != nil { + return nil, err + } + return dialer.DialContext(ctx, network, destination) } func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return d.router.DefaultOutbound(N.NetworkUDP).ListenPacket(ctx, destination) + dialer, err := d.router.DefaultOutbound(N.NetworkUDP) + if err != nil { + return nil, err + } + return dialer.ListenPacket(ctx, destination) } func (d *RouterDialer) Upstream() any { diff --git a/common/srs/binary.go b/common/srs/binary.go new file mode 100644 index 0000000000..dd994c2c29 --- /dev/null +++ b/common/srs/binary.go @@ -0,0 +1,485 @@ +package srs + +import ( + "compress/zlib" + "encoding/binary" + "io" + "net/netip" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/rw" + + "go4.org/netipx" +) + +var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS + +const ( + ruleItemQueryType uint8 = iota + ruleItemNetwork + ruleItemDomain + ruleItemDomainKeyword + ruleItemDomainRegex + ruleItemSourceIPCIDR + ruleItemIPCIDR + ruleItemSourcePort + ruleItemSourcePortRange + ruleItemPort + ruleItemPortRange + ruleItemProcessName + ruleItemProcessPath + ruleItemPackageName + ruleItemWIFISSID + ruleItemWIFIBSSID + ruleItemFinal uint8 = 0xFF +) + +func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) { + var magicBytes [3]byte + _, err = io.ReadFull(reader, magicBytes[:]) + if err != nil { + return + } + if magicBytes != MagicBytes { + err = E.New("invalid sing-box rule set file") + return + } + var version uint8 + err = binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return ruleSet, err + } + if version != 1 { + return ruleSet, E.New("unsupported version: ", version) + } + zReader, err := zlib.NewReader(reader) + if err != nil { + return + } + length, err := rw.ReadUVariant(zReader) + if err != nil { + return + } + ruleSet.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + ruleSet.Rules[i], err = readRule(zReader, recovery) + if err != nil { + err = E.Cause(err, "read rule[", i, "]") + return + } + } + return +} + +func Write(writer io.Writer, ruleSet option.PlainRuleSet) error { + _, err := writer.Write(MagicBytes[:]) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) + if err != nil { + return err + } + err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules))) + if err != nil { + return err + } + for _, rule := range ruleSet.Rules { + err = writeRule(zWriter, rule) + if err != nil { + return err + } + } + return zWriter.Close() +} + +func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) { + var ruleType uint8 + err = binary.Read(reader, binary.BigEndian, &ruleType) + if err != nil { + return + } + switch ruleType { + case 0: + rule.DefaultOptions, err = readDefaultRule(reader, recovery) + case 1: + rule.LogicalOptions, err = readLogicalRule(reader, recovery) + default: + err = E.New("unknown rule type: ", ruleType) + } + return +} + +func writeRule(writer io.Writer, rule option.HeadlessRule) error { + switch rule.Type { + case C.RuleTypeDefault: + return writeDefaultRule(writer, rule.DefaultOptions) + case C.RuleTypeLogical: + return writeLogicalRule(writer, rule.LogicalOptions) + default: + panic("unknown rule type: " + rule.Type) + } +} + +func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) { + var lastItemType uint8 + for { + var itemType uint8 + err = binary.Read(reader, binary.BigEndian, &itemType) + if err != nil { + return + } + switch itemType { + case ruleItemQueryType: + var rawQueryType []uint16 + rawQueryType, err = readRuleItemUint16(reader) + if err != nil { + return + } + rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType { + return option.DNSQueryType(it) + }) + case ruleItemNetwork: + rule.Network, err = readRuleItemString(reader) + case ruleItemDomain: + var matcher *domain.Matcher + matcher, err = domain.ReadMatcher(reader) + if err != nil { + return + } + rule.DomainMatcher = matcher + case ruleItemDomainKeyword: + rule.DomainKeyword, err = readRuleItemString(reader) + case ruleItemDomainRegex: + rule.DomainRegex, err = readRuleItemString(reader) + case ruleItemSourceIPCIDR: + rule.SourceIPSet, err = readIPSet(reader) + if err != nil { + return + } + if recovery { + rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemIPCIDR: + rule.IPSet, err = readIPSet(reader) + if err != nil { + return + } + if recovery { + rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemSourcePort: + rule.SourcePort, err = readRuleItemUint16(reader) + case ruleItemSourcePortRange: + rule.SourcePortRange, err = readRuleItemString(reader) + case ruleItemPort: + rule.Port, err = readRuleItemUint16(reader) + case ruleItemPortRange: + rule.PortRange, err = readRuleItemString(reader) + case ruleItemProcessName: + rule.ProcessName, err = readRuleItemString(reader) + case ruleItemProcessPath: + rule.ProcessPath, err = readRuleItemString(reader) + case ruleItemPackageName: + rule.PackageName, err = readRuleItemString(reader) + case ruleItemWIFISSID: + rule.WIFISSID, err = readRuleItemString(reader) + case ruleItemWIFIBSSID: + rule.WIFIBSSID, err = readRuleItemString(reader) + case ruleItemFinal: + err = binary.Read(reader, binary.BigEndian, &rule.Invert) + return + default: + err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType) + } + if err != nil { + return + } + lastItemType = itemType + } +} + +func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error { + err := binary.Write(writer, binary.BigEndian, uint8(0)) + if err != nil { + return err + } + if len(rule.QueryType) > 0 { + err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 { + return uint16(it) + })) + if err != nil { + return err + } + } + if len(rule.Network) > 0 { + err = writeRuleItemString(writer, ruleItemNetwork, rule.Network) + if err != nil { + return err + } + } + if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 { + err = binary.Write(writer, binary.BigEndian, ruleItemDomain) + if err != nil { + return err + } + err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer) + if err != nil { + return err + } + } + if len(rule.DomainKeyword) > 0 { + err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword) + if err != nil { + return err + } + } + if len(rule.DomainRegex) > 0 { + err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex) + if err != nil { + return err + } + } + if len(rule.SourceIPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR) + if err != nil { + return E.Cause(err, "source_ipcidr") + } + } + if len(rule.IPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR) + if err != nil { + return E.Cause(err, "ipcidr") + } + } + if len(rule.SourcePort) > 0 { + err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort) + if err != nil { + return err + } + } + if len(rule.SourcePortRange) > 0 { + err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange) + if err != nil { + return err + } + } + if len(rule.Port) > 0 { + err = writeRuleItemUint16(writer, ruleItemPort, rule.Port) + if err != nil { + return err + } + } + if len(rule.PortRange) > 0 { + err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange) + if err != nil { + return err + } + } + if len(rule.ProcessName) > 0 { + err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName) + if err != nil { + return err + } + } + if len(rule.ProcessPath) > 0 { + err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath) + if err != nil { + return err + } + } + if len(rule.PackageName) > 0 { + err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName) + if err != nil { + return err + } + } + if len(rule.WIFISSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) + if err != nil { + return err + } + } + if len(rule.WIFIBSSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, ruleItemFinal) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, rule.Invert) + if err != nil { + return err + } + return nil +} + +func readRuleItemString(reader io.Reader) ([]string, error) { + length, err := rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + value := make([]string, length) + for i := uint64(0); i < length; i++ { + value[i], err = rw.ReadVString(reader) + if err != nil { + return nil, err + } + } + return value, nil +} + +func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error { + err := binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(value))) + if err != nil { + return err + } + for _, item := range value { + err = rw.WriteVString(writer, item) + if err != nil { + return err + } + } + return nil +} + +func readRuleItemUint16(reader io.Reader) ([]uint16, error) { + length, err := rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + value := make([]uint16, length) + for i := uint64(0); i < length; i++ { + err = binary.Read(reader, binary.BigEndian, &value[i]) + if err != nil { + return nil, err + } + } + return value, nil +} + +func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error { + err := binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(value))) + if err != nil { + return err + } + for _, item := range value { + err = binary.Write(writer, binary.BigEndian, item) + if err != nil { + return err + } + } + return nil +} + +func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error { + var builder netipx.IPSetBuilder + for i, prefixString := range value { + prefix, err := netip.ParsePrefix(prefixString) + if err == nil { + builder.AddPrefix(prefix) + continue + } + addr, addrErr := netip.ParseAddr(prefixString) + if addrErr == nil { + builder.Add(addr) + continue + } + return E.Cause(err, "parse [", i, "]") + } + ipSet, err := builder.IPSet() + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + return writeIPSet(writer, ipSet) +} + +func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { + var mode uint8 + err = binary.Read(reader, binary.BigEndian, &mode) + if err != nil { + return + } + switch mode { + case 0: + logicalRule.Mode = C.LogicalTypeAnd + case 1: + logicalRule.Mode = C.LogicalTypeOr + default: + err = E.New("unknown logical mode: ", mode) + return + } + length, err := rw.ReadUVariant(reader) + if err != nil { + return + } + logicalRule.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + logicalRule.Rules[i], err = readRule(reader, recovery) + if err != nil { + err = E.Cause(err, "read logical rule [", i, "]") + return + } + } + err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert) + if err != nil { + return + } + return +} + +func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + switch logicalRule.Mode { + case C.LogicalTypeAnd: + err = binary.Write(writer, binary.BigEndian, uint8(0)) + case C.LogicalTypeOr: + err = binary.Write(writer, binary.BigEndian, uint8(1)) + default: + panic("unknown logical mode: " + logicalRule.Mode) + } + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules))) + if err != nil { + return err + } + for _, rule := range logicalRule.Rules { + err = writeRule(writer, rule) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, logicalRule.Invert) + if err != nil { + return err + } + return nil +} diff --git a/common/srs/ip_set.go b/common/srs/ip_set.go new file mode 100644 index 0000000000..b346da26f6 --- /dev/null +++ b/common/srs/ip_set.go @@ -0,0 +1,116 @@ +package srs + +import ( + "encoding/binary" + "io" + "net/netip" + "unsafe" + + "github.com/sagernet/sing/common/rw" + + "go4.org/netipx" +) + +type myIPSet struct { + rr []myIPRange +} + +type myIPRange struct { + from netip.Addr + to netip.Addr +} + +func readIPSet(reader io.Reader) (*netipx.IPSet, error) { + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return nil, err + } + var length uint64 + err = binary.Read(reader, binary.BigEndian, &length) + if err != nil { + return nil, err + } + mySet := &myIPSet{ + rr: make([]myIPRange, length), + } + for i := uint64(0); i < length; i++ { + var ( + fromLen uint64 + toLen uint64 + fromAddr netip.Addr + toAddr netip.Addr + ) + fromLen, err = rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + fromBytes := make([]byte, fromLen) + _, err = io.ReadFull(reader, fromBytes) + if err != nil { + return nil, err + } + err = fromAddr.UnmarshalBinary(fromBytes) + if err != nil { + return nil, err + } + toLen, err = rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + toBytes := make([]byte, toLen) + _, err = io.ReadFull(reader, toBytes) + if err != nil { + return nil, err + } + err = toAddr.UnmarshalBinary(toBytes) + if err != nil { + return nil, err + } + mySet.rr[i] = myIPRange{fromAddr, toAddr} + } + return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil +} + +func writeIPSet(writer io.Writer, set *netipx.IPSet) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + mySet := (*myIPSet)(unsafe.Pointer(set)) + err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr))) + if err != nil { + return err + } + for _, rr := range mySet.rr { + var ( + fromBinary []byte + toBinary []byte + ) + fromBinary, err = rr.from.MarshalBinary() + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(fromBinary))) + if err != nil { + return err + } + _, err = writer.Write(fromBinary) + if err != nil { + return err + } + toBinary, err = rr.to.MarshalBinary() + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(toBinary))) + if err != nil { + return err + } + _, err = writer.Write(toBinary) + if err != nil { + return err + } + } + return nil +} diff --git a/constant/rule.go b/constant/rule.go index 3c741995f8..5a8eaf127f 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -9,3 +9,11 @@ const ( LogicalTypeAnd = "and" LogicalTypeOr = "or" ) + +const ( + RuleSetTypeLocal = "local" + RuleSetTypeRemote = "remote" + RuleSetVersion1 = 1 + RuleSetFormatSource = "source" + RuleSetFormatBinary = "binary" +) diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index 2616ded5c1..001390d391 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -22,11 +22,13 @@ var ( bucketSelected = []byte("selected") bucketExpand = []byte("group_expand") bucketMode = []byte("clash_mode") + bucketRuleSet = []byte("rule_set") bucketNameList = []string{ string(bucketSelected), string(bucketExpand), string(bucketMode), + string(bucketRuleSet), } cacheIDDefault = []byte("default") @@ -257,3 +259,36 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { } }) } + +func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedRuleSet { + var savedSet adapter.SavedRuleSet + err := c.DB.View(func(t *bbolt.Tx) error { + bucket := c.bucket(t, bucketRuleSet) + if bucket == nil { + return os.ErrNotExist + } + setBinary := bucket.Get([]byte(tag)) + if len(setBinary) == 0 { + return os.ErrInvalid + } + return savedSet.UnmarshalBinary(setBinary) + }) + if err != nil { + return nil + } + return &savedSet +} + +func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedRuleSet) error { + return c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := c.createBucket(t, bucketRuleSet) + if err != nil { + return err + } + setBinary, err := set.MarshalBinary() + if err != nil { + return err + } + return bucket.Put([]byte(tag), setBinary) + }) +} diff --git a/experimental/cachefile/fakeip.go b/experimental/cachefile/fakeip.go index 2242342a36..e998ebb859 100644 --- a/experimental/cachefile/fakeip.go +++ b/experimental/cachefile/fakeip.go @@ -25,7 +25,7 @@ func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata { err := c.DB.Batch(func(tx *bbolt.Tx) error { bucket := tx.Bucket(bucketFakeIP) if bucket == nil { - return nil + return os.ErrNotExist } metadataBinary := bucket.Get(keyMetadata) if len(metadataBinary) == 0 { diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go index 050efd8d17..cf96931a85 100644 --- a/experimental/clashapi/proxies.go +++ b/experimental/clashapi/proxies.go @@ -100,8 +100,10 @@ func getProxies(server *Server, router adapter.Router) func(w http.ResponseWrite allProxies = append(allProxies, detour.Tag()) } - defaultTag := router.DefaultOutbound(N.NetworkTCP).Tag() - if defaultTag == "" { + var defaultTag string + if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + defaultTag = defaultOutbound.Tag() + } else { defaultTag = allProxies[0] } diff --git a/experimental/clashapi/server_resources.go b/experimental/clashapi/server_resources.go index ad36641e05..d6d22b5390 100644 --- a/experimental/clashapi/server_resources.go +++ b/experimental/clashapi/server_resources.go @@ -51,7 +51,11 @@ func (s *Server) downloadExternalUI() error { } detour = outbound } else { - detour = s.router.DefaultOutbound(N.NetworkTCP) + outbound, err := s.router.DefaultOutbound(N.NetworkTCP) + if err != nil { + return err + } + detour = outbound } httpClient := &http.Client{ Transport: &http.Transport{ diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go index 3dc5a367e7..b7c20eb075 100644 --- a/experimental/clashapi/trafficontrol/tracker.go +++ b/experimental/clashapi/trafficontrol/tracker.go @@ -94,7 +94,9 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad var chain []string var next string if rule == nil { - next = router.DefaultOutbound(N.NetworkTCP).Tag() + if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + next = defaultOutbound.Tag() + } } else { next = rule.Outbound() } @@ -181,7 +183,9 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route var chain []string var next string if rule == nil { - next = router.DefaultOutbound(N.NetworkUDP).Tag() + if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil { + next = defaultOutbound.Tag() + } } else { next = rule.Outbound() } diff --git a/go.mod b/go.mod index 7efe241ef6..095326d7f9 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 github.com/sagernet/quic-go v0.40.0 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc + github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be github.com/sagernet/sing-dns v0.1.11 github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 github.com/sagernet/sing-quic v0.1.5-0.20231123150216-00957d136203 diff --git a/go.sum b/go.sum index 4db37c0bfe..6d8d994a8c 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= -github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc h1:vESVuxHgbd2EzHxd+TYTpNACIEGBOhp5n3KG7bgbcws= -github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= +github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be h1:FigAM9kq7RRXmHvgn8w2a8tqCY5CMV5GIk0id84dI0o= +github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE= github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE= github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM= diff --git a/option/experimental.go b/option/experimental.go index 72751a590e..c685f51f54 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -23,16 +23,16 @@ type ClashAPIOptions struct { DefaultMode string `json:"default_mode,omitempty"` ModeList []string `json:"-"` + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` // Deprecated: migrated to global cache file StoreMode bool `json:"store_mode,omitempty"` // Deprecated: migrated to global cache file StoreSelected bool `json:"store_selected,omitempty"` // Deprecated: migrated to global cache file StoreFakeIP bool `json:"store_fakeip,omitempty"` - // Deprecated: migrated to global cache file - CacheFile string `json:"cache_file,omitempty"` - // Deprecated: migrated to global cache file - CacheID string `json:"cache_id,omitempty"` } type V2RayAPIOptions struct { diff --git a/option/route.go b/option/route.go index 43150576e2..e313fcf242 100644 --- a/option/route.go +++ b/option/route.go @@ -4,6 +4,7 @@ type RouteOptions struct { GeoIP *GeoIPOptions `json:"geoip,omitempty"` Geosite *GeositeOptions `json:"geosite,omitempty"` Rules []Rule `json:"rules,omitempty"` + RuleSet []RuleSet `json:"rule_set,omitempty"` Final string `json:"final,omitempty"` FindProcess bool `json:"find_process,omitempty"` AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` diff --git a/option/rule.go b/option/rule.go index 4f4042025f..bad605a0fc 100644 --- a/option/rule.go +++ b/option/rule.go @@ -65,34 +65,36 @@ func (r Rule) IsValid() bool { } type DefaultRule struct { - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - GeoIP Listable[string] `json:"geoip,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - IPCIDR Listable[string] `json:"ip_cidr,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + Network Listable[string] `json:"network,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + Protocol Listable[string] `json:"protocol,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + Geosite Listable[string] `json:"geosite,omitempty"` + SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` + GeoIP Listable[string] `json:"geoip,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + User Listable[string] `json:"user,omitempty"` + UserID Listable[int32] `json:"user_id,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` } func (r DefaultRule) IsValid() bool { diff --git a/option/rule_dns.go b/option/rule_dns.go index fca3432293..c02d09f761 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -91,6 +91,7 @@ type DefaultDNSRule struct { ClashMode string `json:"clash_mode,omitempty"` WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` Invert bool `json:"invert,omitempty"` Server string `json:"server,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` diff --git a/option/rule_set.go b/option/rule_set.go new file mode 100644 index 0000000000..7a6f7e9282 --- /dev/null +++ b/option/rule_set.go @@ -0,0 +1,230 @@ +package option + +import ( + "reflect" + + "github.com/sagernet/sing-box/common/json" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + + "go4.org/netipx" +) + +type _RuleSet struct { + Type string `json:"type"` + Tag string `json:"tag"` + Format string `json:"format"` + LocalOptions LocalRuleSet `json:"-"` + RemoteOptions RemoteRuleSet `json:"-"` +} + +type RuleSet _RuleSet + +func (r RuleSet) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleSetTypeLocal: + v = r.LocalOptions + case C.RuleSetTypeRemote: + v = r.RemoteOptions + default: + return nil, E.New("unknown rule set type: " + r.Type) + } + return MarshallObjects((_RuleSet)(r), v) +} + +func (r *RuleSet) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_RuleSet)(r)) + if err != nil { + return err + } + if r.Tag == "" { + return E.New("missing rule_set.[].tag") + } + switch r.Format { + case "": + return E.New("missing rule_set.[].format") + case C.RuleSetFormatSource, C.RuleSetFormatBinary: + default: + return E.New("unknown rule set format: " + r.Format) + } + var v any + switch r.Type { + case C.RuleSetTypeLocal: + v = &r.LocalOptions + case C.RuleSetTypeRemote: + v = &r.RemoteOptions + case "": + return E.New("missing rule_set.[].type") + default: + return E.New("unknown rule set type: " + r.Type) + } + err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v) + if err != nil { + return E.Cause(err, "rule set") + } + return nil +} + +type LocalRuleSet struct { + Path string `json:"path,omitempty"` +} + +type RemoteRuleSet struct { + URL string `json:"url"` + DownloadDetour string `json:"download_detour,omitempty"` + UpdateInterval Duration `json:"update_interval,omitempty"` +} + +type _HeadlessRule struct { + Type string `json:"type,omitempty"` + DefaultOptions DefaultHeadlessRule `json:"-"` + LogicalOptions LogicalHeadlessRule `json:"-"` +} + +type HeadlessRule _HeadlessRule + +func (r HeadlessRule) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleTypeDefault: + r.Type = "" + v = r.DefaultOptions + case C.RuleTypeLogical: + v = r.LogicalOptions + default: + return nil, E.New("unknown rule type: " + r.Type) + } + return MarshallObjects((_HeadlessRule)(r), v) +} + +func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_HeadlessRule)(r)) + if err != nil { + return err + } + var v any + switch r.Type { + case "", C.RuleTypeDefault: + r.Type = C.RuleTypeDefault + v = &r.DefaultOptions + case C.RuleTypeLogical: + v = &r.LogicalOptions + default: + return E.New("unknown rule type: " + r.Type) + } + err = UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v) + if err != nil { + return E.Cause(err, "route rule-set rule") + } + return nil +} + +func (r HeadlessRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault, "": + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + +type DefaultHeadlessRule struct { + QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` + Network Listable[string] `json:"network,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + Invert bool `json:"invert,omitempty"` + + DomainMatcher *domain.Matcher `json:"-"` + SourceIPSet *netipx.IPSet `json:"-"` + IPSet *netipx.IPSet `json:"-"` +} + +func (r DefaultHeadlessRule) IsValid() bool { + var defaultValue DefaultHeadlessRule + defaultValue.Invert = r.Invert + return !reflect.DeepEqual(r, defaultValue) +} + +type LogicalHeadlessRule struct { + Mode string `json:"mode"` + Rules []HeadlessRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` +} + +func (r LogicalHeadlessRule) IsValid() bool { + return len(r.Rules) > 0 && common.All(r.Rules, HeadlessRule.IsValid) +} + +type _PlainRuleSetCompat struct { + Version int `json:"version"` + Options PlainRuleSet `json:"-"` +} + +type PlainRuleSetCompat _PlainRuleSetCompat + +func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { + var v any + switch r.Version { + case C.RuleSetVersion1: + v = r.Options + default: + return nil, E.New("unknown rule set version: ", r.Version) + } + return MarshallObjects((_PlainRuleSetCompat)(r), v) +} + +func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_PlainRuleSetCompat)(r)) + if err != nil { + return err + } + var v any + switch r.Version { + case C.RuleSetVersion1: + v = &r.Options + case 0: + return E.New("missing rule set version") + default: + return E.New("unknown rule set version: ", r.Version) + } + err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) + if err != nil { + return E.Cause(err, "rule set") + } + return nil +} + +func (r PlainRuleSetCompat) Upgrade() PlainRuleSet { + var result PlainRuleSet + switch r.Version { + case C.RuleSetVersion1: + result = r.Options + default: + panic("unknown rule set version: " + F.ToString(r.Version)) + } + return result +} + +type PlainRuleSet struct { + Rules []HeadlessRule `json:"rules,omitempty"` +} diff --git a/option/types.go b/option/types.go index f2fed66309..520c3503aa 100644 --- a/option/types.go +++ b/option/types.go @@ -174,6 +174,14 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error { type DNSQueryType uint16 +func (t DNSQueryType) String() string { + typeName, loaded := mDNS.TypeToString[uint16(t)] + if loaded { + return typeName + } + return F.ToString(uint16(t)) +} + func (t DNSQueryType) MarshalJSON() ([]byte, error) { typeName, loaded := mDNS.TypeToString[uint16(t)] if loaded { diff --git a/route/router.go b/route/router.go index b48d2346d9..967061952e 100644 --- a/route/router.go +++ b/route/router.go @@ -67,6 +67,8 @@ type Router struct { dnsClient *dns.Client defaultDomainStrategy dns.DomainStrategy dnsRules []adapter.DNSRule + ruleSets []adapter.RuleSet + ruleSetMap map[string]adapter.RuleSet defaultTransport dns.Transport transports []dns.Transport transportMap map[string]dns.Transport @@ -106,6 +108,7 @@ func NewRouter( outboundByTag: make(map[string]adapter.Outbound), rules: make([]adapter.Rule, 0, len(options.Rules)), dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)), + ruleSetMap: make(map[string]adapter.RuleSet), needGeoIPDatabase: hasRule(options.Rules, isGeoIPRule) || hasDNSRule(dnsOptions.Rules, isGeoIPDNSRule), needGeositeDatabase: hasRule(options.Rules, isGeositeRule) || hasDNSRule(dnsOptions.Rules, isGeositeDNSRule), geoIPOptions: common.PtrValueOrDefault(options.GeoIP), @@ -140,6 +143,14 @@ func NewRouter( } router.dnsRules = append(router.dnsRules, dnsRule) } + for i, ruleSetOptions := range options.RuleSet { + ruleSet, err := NewRuleSet(ctx, router, router.logger, ruleSetOptions) + if err != nil { + return nil, E.Cause(err, "parse rule-set[", i, "]") + } + router.ruleSets = append(router.ruleSets, ruleSet) + router.ruleSetMap[ruleSetOptions.Tag] = ruleSet + } transports := make([]dns.Transport, len(dnsOptions.Servers)) dummyTransportMap := make(map[string]dns.Transport) @@ -479,6 +490,12 @@ func (r *Router) Start() error { if r.needWIFIState { r.updateWIFIState() } + for i, ruleSet := range r.ruleSets { + err := ruleSet.Start() + if err != nil { + return E.Cause(err, "initialize rule-set[", i, "]") + } + } for i, rule := range r.rules { err := rule.Start() if err != nil { @@ -576,11 +593,17 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { return outbound, loaded } -func (r *Router) DefaultOutbound(network string) adapter.Outbound { +func (r *Router) DefaultOutbound(network string) (adapter.Outbound, error) { if network == N.NetworkTCP { - return r.defaultOutboundForConnection + if r.defaultOutboundForConnection == nil { + return nil, E.New("missing default outbound for TCP connections") + } + return r.defaultOutboundForConnection, nil } else { - return r.defaultOutboundForPacketConnection + if r.defaultOutboundForPacketConnection == nil { + return nil, E.New("missing default outbound for UDP connections") + } + return r.defaultOutboundForPacketConnection, nil } } @@ -588,6 +611,11 @@ func (r *Router) FakeIPStore() adapter.FakeIPStore { return r.fakeIPStore } +func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) { + ruleSet, loaded := r.ruleSetMap[tag] + return ruleSet, loaded +} + func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { diff --git a/route/rule_abstract.go b/route/rule_abstract.go index 38d4d57d41..312caaee73 100644 --- a/route/rule_abstract.go +++ b/route/rule_abstract.go @@ -1,6 +1,7 @@ package route import ( + "io" "strings" "github.com/sagernet/sing-box/adapter" @@ -135,7 +136,7 @@ func (r *abstractDefaultRule) String() string { } type abstractLogicalRule struct { - rules []adapter.Rule + rules []adapter.HeadlessRule mode string invert bool outbound string @@ -146,7 +147,10 @@ func (r *abstractLogicalRule) Type() string { } func (r *abstractLogicalRule) UpdateGeosite() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (adapter.Rule, bool) { + rule, loaded := it.(adapter.Rule) + return rule, loaded + }) { err := rule.UpdateGeosite() if err != nil { return err @@ -156,7 +160,10 @@ func (r *abstractLogicalRule) UpdateGeosite() error { } func (r *abstractLogicalRule) Start() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (common.Starter, bool) { + rule, loaded := it.(common.Starter) + return rule, loaded + }) { err := rule.Start() if err != nil { return err @@ -166,7 +173,10 @@ func (r *abstractLogicalRule) Start() error { } func (r *abstractLogicalRule) Close() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (io.Closer, bool) { + rule, loaded := it.(io.Closer) + return rule, loaded + }) { err := rule.Close() if err != nil { return err @@ -177,11 +187,11 @@ func (r *abstractLogicalRule) Close() error { func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { if r.mode == C.LogicalTypeAnd { - return common.All(r.rules, func(it adapter.Rule) bool { + return common.All(r.rules, func(it adapter.HeadlessRule) bool { return it.Match(metadata) }) != r.invert } else { - return common.Any(r.rules, func(it adapter.Rule) bool { + return common.Any(r.rules, func(it adapter.HeadlessRule) bool { return it.Match(metadata) }) != r.invert } diff --git a/route/rule_default.go b/route/rule_default.go index 8c8473abf5..c0ef9eef60 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -194,6 +194,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.RuleSet) > 0 { + item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } @@ -206,7 +211,7 @@ type LogicalRule struct { func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { r := &LogicalRule{ abstractLogicalRule{ - rules: make([]adapter.Rule, len(options.Rules)), + rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, outbound: options.Outbound, }, diff --git a/route/rule_dns.go b/route/rule_dns.go index b444932582..f5f9fd3583 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -190,6 +190,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.RuleSet) > 0 { + item := NewRuleSetItem(router, options.RuleSet, false) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } @@ -212,7 +217,7 @@ type LogicalDNSRule struct { func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { r := &LogicalDNSRule{ abstractLogicalRule: abstractLogicalRule{ - rules: make([]adapter.Rule, len(options.Rules)), + rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, outbound: options.Server, }, diff --git a/route/rule_headless.go b/route/rule_headless.go new file mode 100644 index 0000000000..9df2ee3036 --- /dev/null +++ b/route/rule_headless.go @@ -0,0 +1,173 @@ +package route + +import ( + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func NewHeadlessRule(router adapter.Router, options option.HeadlessRule) (adapter.HeadlessRule, error) { + switch options.Type { + case "", C.RuleTypeDefault: + if !options.DefaultOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewDefaultHeadlessRule(router, options.DefaultOptions) + case C.RuleTypeLogical: + if !options.LogicalOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewLogicalHeadlessRule(router, options.LogicalOptions) + default: + return nil, E.New("unknown rule type: ", options.Type) + } +} + +var _ adapter.HeadlessRule = (*DefaultHeadlessRule)(nil) + +type DefaultHeadlessRule struct { + abstractDefaultRule +} + +func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) { + rule := &DefaultHeadlessRule{ + abstractDefaultRule{ + invert: options.Invert, + }, + } + if len(options.Network) > 0 { + item := NewNetworkItem(options.Network) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 { + item := NewDomainItem(options.Domain, options.DomainSuffix) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.DomainMatcher != nil { + item := NewRawDomainItem(options.DomainMatcher) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainKeyword) > 0 { + item := NewDomainKeywordItem(options.DomainKeyword) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainRegex) > 0 { + item, err := NewDomainRegexItem(options.DomainRegex) + if err != nil { + return nil, E.Cause(err, "domain_regex") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourceIPCIDR) > 0 { + item, err := NewIPCIDRItem(true, options.SourceIPCIDR) + if err != nil { + return nil, E.Cause(err, "source_ipcidr") + } + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.SourceIPSet != nil { + item := NewRawIPCIDRItem(true, options.SourceIPSet) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.IPCIDR) > 0 { + item, err := NewIPCIDRItem(false, options.IPCIDR) + if err != nil { + return nil, E.Cause(err, "ipcidr") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.IPSet != nil { + item := NewRawIPCIDRItem(false, options.IPSet) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePort) > 0 { + item := NewPortItem(true, options.SourcePort) + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePortRange) > 0 { + item, err := NewPortRangeItem(true, options.SourcePortRange) + if err != nil { + return nil, E.Cause(err, "source_port_range") + } + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Port) > 0 { + item := NewPortItem(false, options.Port) + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PortRange) > 0 { + item, err := NewPortRangeItem(false, options.PortRange) + if err != nil { + return nil, E.Cause(err, "port_range") + } + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessName) > 0 { + item := NewProcessItem(options.ProcessName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessPath) > 0 { + item := NewProcessPathItem(options.ProcessPath) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PackageName) > 0 { + item := NewPackageNameItem(options.PackageName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(router, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFIBSSID) > 0 { + item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + return rule, nil +} + +var _ adapter.HeadlessRule = (*LogicalHeadlessRule)(nil) + +type LogicalHeadlessRule struct { + abstractLogicalRule +} + +func NewLogicalHeadlessRule(router adapter.Router, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) { + r := &LogicalHeadlessRule{ + abstractLogicalRule{ + rules: make([]adapter.HeadlessRule, len(options.Rules)), + invert: options.Invert, + }, + } + switch options.Mode { + case C.LogicalTypeAnd: + r.mode = C.LogicalTypeAnd + case C.LogicalTypeOr: + r.mode = C.LogicalTypeOr + default: + return nil, E.New("unknown logical mode: ", options.Mode) + } + for i, subRule := range options.Rules { + rule, err := NewHeadlessRule(router, subRule) + if err != nil { + return nil, E.Cause(err, "sub rule[", i, "]") + } + r.rules[i] = rule + } + return r, nil +} diff --git a/route/rule_item_cidr.go b/route/rule_item_cidr.go index b72d1e10b1..e17d87def8 100644 --- a/route/rule_item_cidr.go +++ b/route/rule_item_cidr.go @@ -31,7 +31,7 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { builder.Add(addr) continue } - return nil, E.Cause(err, "parse ip_cidr [", i, "]") + return nil, E.Cause(err, "parse [", i, "]") } var description string if isSource { @@ -57,8 +57,23 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { }, nil } +func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem { + var description string + if isSource { + description = "source_ipcidr=" + } else { + description = "ipcidr=" + } + description += "" + return &IPCIDRItem{ + ipSet: ipSet, + isSource: isSource, + description: description, + } +} + func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool { - if r.isSource { + if r.isSource || metadata.QueryType != 0 || metadata.IPCIDRMatchSource { return r.ipSet.Contains(metadata.Source.Addr) } else { if metadata.Destination.IsIP() { diff --git a/route/rule_item_domain.go b/route/rule_item_domain.go index 6602441deb..d2a11181b0 100644 --- a/route/rule_item_domain.go +++ b/route/rule_item_domain.go @@ -43,6 +43,13 @@ func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem { } } +func NewRawDomainItem(matcher *domain.Matcher) *DomainItem { + return &DomainItem{ + matcher, + "domain/domain_suffix=", + } +} + func (r *DomainItem) Match(metadata *adapter.InboundContext) bool { var domainHost string if metadata.Domain != "" { diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go new file mode 100644 index 0000000000..959b2f6110 --- /dev/null +++ b/route/rule_item_rule_set.go @@ -0,0 +1,55 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*RuleSetItem)(nil) + +type RuleSetItem struct { + router adapter.Router + tagList []string + setList []adapter.HeadlessRule + ipcidrMatchSource bool +} + +func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool) *RuleSetItem { + return &RuleSetItem{ + router: router, + tagList: tagList, + ipcidrMatchSource: ipCIDRMatchSource, + } +} + +func (r *RuleSetItem) Start() error { + for _, tag := range r.tagList { + ruleSet, loaded := r.router.RuleSet(tag) + if !loaded { + return E.New("rule-set not found: ", tag) + } + r.setList = append(r.setList, ruleSet) + } + return nil +} + +func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { + metadata.IPCIDRMatchSource = r.ipcidrMatchSource + for _, ruleSet := range r.setList { + if ruleSet.Match(metadata) { + return true + } + } + return false +} + +func (r *RuleSetItem) String() string { + if len(r.tagList) == 1 { + return F.ToString("rule_set=", r.tagList[0]) + } else { + return F.ToString("rule_set=[", strings.Join(r.tagList, " "), "]") + } +} diff --git a/route/rule_set.go b/route/rule_set.go new file mode 100644 index 0000000000..76c78c6214 --- /dev/null +++ b/route/rule_set.go @@ -0,0 +1,22 @@ +package route + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + 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" +) + +func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { + switch options.Type { + case C.RuleSetTypeLocal: + return NewLocalRuleSet(router, options) + case C.RuleSetTypeRemote: + return NewRemoteRuleSet(ctx, router, logger, options), nil + default: + return nil, E.New("unknown rule set type: ", options.Type) + } +} diff --git a/route/rule_set_local.go b/route/rule_set_local.go new file mode 100644 index 0000000000..ccdb1704a8 --- /dev/null +++ b/route/rule_set_local.go @@ -0,0 +1,69 @@ +package route + +import ( + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ adapter.RuleSet = (*LocalRuleSet)(nil) + +type LocalRuleSet struct { + rules []adapter.HeadlessRule +} + +func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { + setFile, err := os.Open(options.LocalOptions.Path) + if err != nil { + return nil, err + } + var plainRuleSet option.PlainRuleSet + switch options.Format { + case C.RuleSetFormatSource, "": + var compat option.PlainRuleSetCompat + decoder := json.NewDecoder(json.NewCommentFilter(setFile)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&compat) + if err != nil { + return nil, err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + plainRuleSet, err = srs.Read(setFile, false) + if err != nil { + return nil, err + } + default: + return nil, E.New("unknown rule set format: ", options.Format) + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(router, ruleOptions) + if err != nil { + return nil, E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + return &LocalRuleSet{rules}, nil +} + +func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} + +func (s *LocalRuleSet) Start() error { + return nil +} + +func (s *LocalRuleSet) Close() error { + return nil +} diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go new file mode 100644 index 0000000000..d9102320dd --- /dev/null +++ b/route/rule_set_remote.go @@ -0,0 +1,218 @@ +package route + +import ( + "bytes" + "context" + "io" + "net" + "net/http" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + 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" + "github.com/sagernet/sing/service" +) + +var _ adapter.RuleSet = (*RemoteRuleSet)(nil) + +type RemoteRuleSet struct { + ctx context.Context + cancel context.CancelFunc + router adapter.Router + logger logger.ContextLogger + options option.RuleSet + updateInterval time.Duration + dialer N.Dialer + rules []adapter.HeadlessRule + lastUpdated time.Time + lastEtag string + updateTicker *time.Ticker +} + +func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { + ctx, cancel := context.WithCancel(ctx) + var updateInterval time.Duration + if options.RemoteOptions.UpdateInterval > 0 { + updateInterval = time.Duration(options.RemoteOptions.UpdateInterval) + } else { + updateInterval = 12 * time.Hour + } + return &RemoteRuleSet{ + ctx: ctx, + cancel: cancel, + router: router, + logger: logger, + options: options, + updateInterval: updateInterval, + } +} + +func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} + +func (s *RemoteRuleSet) Start() error { + var dialer N.Dialer + if s.options.RemoteOptions.DownloadDetour != "" { + outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour) + if !loaded { + return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour) + } + dialer = outbound + } else { + outbound, err := s.router.DefaultOutbound(N.NetworkTCP) + if err != nil { + return err + } + dialer = outbound + } + s.dialer = dialer + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + if savedSet := cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil { + err := s.loadBytes(savedSet.Content) + if err != nil { + return E.Cause(err, "restore cached rule-set") + } + s.lastUpdated = savedSet.LastUpdated + s.lastEtag = savedSet.LastEtag + } + } + if s.lastUpdated.IsZero() || time.Since(s.lastUpdated) > s.updateInterval { + err := s.fetchOnce() + if err != nil { + return E.Cause(err, "fetch rule-set ", s.options.Tag) + } + } + s.updateTicker = time.NewTicker(s.updateInterval) + go s.loopUpdate() + return nil +} + +func (s *RemoteRuleSet) loadBytes(content []byte) error { + var ( + plainRuleSet option.PlainRuleSet + err error + ) + switch s.options.Format { + case C.RuleSetFormatSource, "": + var compat option.PlainRuleSetCompat + decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content))) + decoder.DisallowUnknownFields() + err = decoder.Decode(&compat) + if err != nil { + return err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + plainRuleSet, err = srs.Read(bytes.NewReader(content), false) + if err != nil { + return err + } + default: + return E.New("unknown rule set format: ", s.options.Format) + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(s.router, ruleOptions) + if err != nil { + return E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + s.rules = rules + return nil +} + +func (s *RemoteRuleSet) loopUpdate() { + for { + select { + case <-s.ctx.Done(): + return + case <-s.updateTicker.C: + err := s.fetchOnce() + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } + } + } +} + +func (s *RemoteRuleSet) fetchOnce() error { + s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL) + httpClient := &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + }, + } + defer httpClient.CloseIdleConnections() + request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil) + if err != nil { + return err + } + if s.lastEtag != "" { + request.Header.Set("If-None-Match", s.lastEtag) + } + response, err := httpClient.Do(request.WithContext(s.ctx)) + if err != nil { + return err + } + switch response.StatusCode { + case http.StatusOK: + case http.StatusNotModified: + s.logger.Info("update rule-set ", s.options.Tag, ": not modified") + return nil + default: + return E.New("unexpected status: ", response.Status) + } + content, err := io.ReadAll(response.Body) + if err != nil { + response.Body.Close() + return err + } + err = s.loadBytes(content) + if err != nil { + response.Body.Close() + return err + } + response.Body.Close() + eTagHeader := response.Header.Get("Etag") + if eTagHeader != "" { + s.lastEtag = eTagHeader + } + s.lastUpdated = time.Now() + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err = cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedRuleSet{ + LastUpdated: s.lastUpdated, + Content: content, + LastEtag: s.lastEtag, + }) + if err != nil { + s.logger.Error("save rule-set cache: ", err) + } + } + s.logger.Info("updated rule-set ", s.options.Tag) + return nil +} + +func (s *RemoteRuleSet) Close() error { + s.updateTicker.Stop() + s.cancel() + return nil +} From 4bb8497324469bc0fe5860da9cf2a73fa1e293da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 29 Nov 2023 17:35:56 +0800 Subject: [PATCH 023/103] documentation: Add rule set --- docs/configuration/dns/rule.md | 32 ++- docs/configuration/dns/rule.zh.md | 32 ++- docs/configuration/experimental/cache-file.md | 34 +++ docs/configuration/experimental/clash-api.md | 121 ++++++++++ docs/configuration/experimental/index.md | 145 ++---------- docs/configuration/experimental/index.zh.md | 137 ------------ docs/configuration/experimental/v2ray-api.md | 50 +++++ docs/configuration/route/geoip.md | 8 + docs/configuration/route/geoip.zh.md | 33 --- docs/configuration/route/geosite.md | 8 + docs/configuration/route/geosite.zh.md | 33 --- docs/configuration/route/index.md | 30 ++- docs/configuration/route/index.zh.md | 32 ++- docs/configuration/route/rule.md | 46 +++- docs/configuration/route/rule.zh.md | 44 +++- docs/configuration/rule-set/headless-rule.md | 207 ++++++++++++++++++ docs/configuration/rule-set/index.md | 97 ++++++++ docs/configuration/rule-set/source-format.md | 34 +++ docs/migration.md | 187 ++++++++++++++++ mkdocs.yml | 28 ++- 20 files changed, 980 insertions(+), 358 deletions(-) create mode 100644 docs/configuration/experimental/cache-file.md create mode 100644 docs/configuration/experimental/clash-api.md delete mode 100644 docs/configuration/experimental/index.zh.md create mode 100644 docs/configuration/experimental/v2ray-api.md delete mode 100644 docs/configuration/route/geoip.zh.md delete mode 100644 docs/configuration/route/geosite.zh.md create mode 100644 docs/configuration/rule-set/headless-rule.md create mode 100644 docs/configuration/rule-set/index.md create mode 100644 docs/configuration/rule-set/source-format.md create mode 100644 docs/migration.md diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 18e352b407..896a3b4468 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -1,3 +1,13 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -85,6 +95,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": [ "direct" @@ -166,15 +180,23 @@ Match domain using regular expression. #### geosite +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + Match geosite. #### source_geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + Match source geoip. #### source_ip_cidr -Match source ip cidr. +Match source IP CIDR. #### source_port @@ -250,6 +272,12 @@ Match WiFi SSID. Match WiFi BSSID. +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [Rule Set](/configuration/route/#rule_set). + #### invert Invert match result. @@ -286,4 +314,4 @@ Rewrite TTL in DNS responses. #### rules -Included default rules. \ No newline at end of file +Included rules. \ No newline at end of file diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 98bfa8ab9a..f990ed3ec5 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -1,3 +1,13 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -84,6 +94,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": [ "direct" @@ -163,10 +177,18 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### geosite -匹配 GeoSite。 +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-set)。 + +匹配 Geosite。 #### source_geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + 匹配源 GeoIP。 #### source_ip_cidr @@ -245,6 +267,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配 WiFi BSSID。 +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + #### invert 反选匹配结果。 @@ -281,4 +309,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### rules -包括的默认规则。 \ No newline at end of file +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md new file mode 100644 index 0000000000..66e30ef9b0 --- /dev/null +++ b/docs/configuration/experimental/cache-file.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "enabled": true, + "path": "", + "cache_id": "", + "store_fakeip": false +} +``` + +### Fields + +#### enabled + +Enable cache file. + +#### path + +Path to the cache file. + +`cache.db` will be used if empty. + +#### cache_id + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. diff --git a/docs/configuration/experimental/clash-api.md b/docs/configuration/experimental/clash-api.md new file mode 100644 index 0000000000..a06fe15480 --- /dev/null +++ b/docs/configuration/experimental/clash-api.md @@ -0,0 +1,121 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-delete-alert: [store_mode](#store_mode) + :material-delete-alert: [store_selected](#store_selected) + :material-delete-alert: [store_fakeip](#store_fakeip) + :material-delete-alert: [cache_file](#cache_file) + :material-delete-alert: [cache_id](#cache_id) + + +!!! quote "" + + Clash API is not included by default, see [Installation](./#installation). + +### Structure + +```json +{ + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" +} +``` + +### Fields + +#### external_controller + +RESTful web API listening address. Clash API will be disabled if empty. + +#### external_ui + +A relative path to the configuration directory or an absolute path to a +directory in which you put some static web resource. sing-box will then +serve it at `http://{{external-controller}}/ui`. + + + +#### external_ui_download_url + +ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. + +`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. + +#### external_ui_download_detour + +The tag of the outbound to download the external UI. + +Default outbound will be used if empty. + +#### secret + +Secret for the RESTful API (optional) +Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` +ALWAYS set a secret if RESTful API is listening on 0.0.0.0 + +#### default_mode + +Default mode in clash, `Rule` will be used if empty. + +This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. + +#### store_mode + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_mode` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +Store Clash mode in cache file. + +#### store_selected + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +!!! note "" + + The tag must be set for target outbounds. + +Store selected outbound for the `Selector` outbound in cache file. + +#### store_fakeip + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and migrated to `cache_file.store_fakeip`. + +Store fakeip in cache file. + +#### cache_file + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_file` is deprecated in Clash API and migrated to `cache_file.enabled` and `cache_file.path`. + +Cache file path, `cache.db` will be used if empty. + +#### cache_id + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_id` is deprecated in Clash API and migrated to `cache_file.cache_id`. + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. \ No newline at end of file diff --git a/docs/configuration/experimental/index.md b/docs/configuration/experimental/index.md index 308e851c3b..1057e59b36 100644 --- a/docs/configuration/experimental/index.md +++ b/docs/configuration/experimental/index.md @@ -1,139 +1,30 @@ +--- +icon: material/alert-decagram +--- + # Experimental +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [cache_file](#cache_file) + :material-alert-decagram: [clash_api](#clash_api) + ### Structure ```json { "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" - }, - "v2ray_api": { - "listen": "127.0.0.1:8080", - "stats": { - "enabled": true, - "inbounds": [ - "socks-in" - ], - "outbounds": [ - "proxy", - "direct" - ], - "users": [ - "sekai" - ] - } - } + "cache_file": {}, + "clash_api": {}, + "v2ray_api": {} } } ``` -!!! note "" - - Traffic statistics and connection management can degrade performance. - -### Clash API Fields - -!!! quote "" - - Clash API is not included by default, see [Installation](./#installation). - -#### external_controller - -RESTful web API listening address. Clash API will be disabled if empty. - -#### external_ui - -A relative path to the configuration directory or an absolute path to a -directory in which you put some static web resource. sing-box will then -serve it at `http://{{external-controller}}/ui`. - -#### external_ui_download_url - -ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. - -`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. - -#### external_ui_download_detour - -The tag of the outbound to download the external UI. - -Default outbound will be used if empty. - -#### secret - -Secret for the RESTful API (optional) -Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` -ALWAYS set a secret if RESTful API is listening on 0.0.0.0 - -#### default_mode - -Default mode in clash, `Rule` will be used if empty. - -This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. - -#### store_mode - -Store Clash mode in cache file. - -#### store_selected - -!!! note "" - - The tag must be set for target outbounds. - -Store selected outbound for the `Selector` outbound in cache file. - -#### store_fakeip - -Store fakeip in cache file. - -#### cache_file - -Cache file path, `cache.db` will be used if empty. - -#### cache_id - -Cache ID. - -If not empty, `store_selected` will use a separate store keyed by it. - -### V2Ray API Fields - -!!! quote "" - - V2Ray API is not included by default, see [Installation](./#installation). - -#### listen - -gRPC API listening address. V2Ray API will be disabled if empty. - -#### stats - -Traffic statistics service settings. - -#### stats.enabled - -Enable statistics service. - -#### stats.inbounds - -Inbound list to count traffic. - -#### stats.outbounds - -Outbound list to count traffic. - -#### stats.users +### Fields -User list to count traffic. \ No newline at end of file +| Key | Format | +|--------------|----------------------------| +| `cache_file` | [Cache File](./cache-file) | +| `clash_api` | [Clash API](./clash-api) | +| `v2ray_api` | [V2Ray API](./v2ray-api) | \ No newline at end of file diff --git a/docs/configuration/experimental/index.zh.md b/docs/configuration/experimental/index.zh.md deleted file mode 100644 index 88a95852c9..0000000000 --- a/docs/configuration/experimental/index.zh.md +++ /dev/null @@ -1,137 +0,0 @@ -# 实验性 - -### 结构 - -```json -{ - "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" - }, - "v2ray_api": { - "listen": "127.0.0.1:8080", - "stats": { - "enabled": true, - "inbounds": [ - "socks-in" - ], - "outbounds": [ - "proxy", - "direct" - ], - "users": [ - "sekai" - ] - } - } - } -} -``` - -!!! note "" - - 流量统计和连接管理会降低性能。 - -### Clash API 字段 - -!!! quote "" - - 默认安装不包含 Clash API,参阅 [安装](/zh/#_2)。 - -#### external_controller - -RESTful web API 监听地址。如果为空,则禁用 Clash API。 - -#### external_ui - -到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。 - -#### external_ui_download_url - -静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。 - -默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。 - -#### external_ui_download_detour - -用于下载静态网页资源的出站的标签。 - -如果为空,将使用默认出站。 - -#### secret - -RESTful API 的密钥(可选) -通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证 -如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。 - -#### default_mode - -Clash 中的默认模式,默认使用 `Rule`。 - -此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。 - -#### store_mode - -将 Clash 模式存储在缓存文件中。 - -#### store_selected - -!!! note "" - - 必须为目标出站设置标签。 - -将 `Selector` 中出站的选定的目标出站存储在缓存文件中。 - -#### store_fakeip - -将 fakeip 存储在缓存文件中。 - -#### cache_file - -缓存文件路径,默认使用`cache.db`。 - -#### cache_id - -缓存 ID。 - -如果不为空,`store_selected` 将会使用以此为键的独立存储。 - -### V2Ray API 字段 - -!!! quote "" - - 默认安装不包含 V2Ray API,参阅 [安装](/zh/#_2)。 - -#### listen - -gRPC API 监听地址。如果为空,则禁用 V2Ray API。 - -#### stats - -流量统计服务设置。 - -#### stats.enabled - -启用统计服务。 - -#### stats.inbounds - -统计流量的入站列表。 - -#### stats.outbounds - -统计流量的出站列表。 - -#### stats.users - -统计流量的用户列表。 \ No newline at end of file diff --git a/docs/configuration/experimental/v2ray-api.md b/docs/configuration/experimental/v2ray-api.md new file mode 100644 index 0000000000..398884242e --- /dev/null +++ b/docs/configuration/experimental/v2ray-api.md @@ -0,0 +1,50 @@ +### Structure + +!!! quote "" + + V2Ray API is not included by default, see [Installation](./#installation). + +```json +{ + "listen": "127.0.0.1:8080", + "stats": { + "enabled": true, + "inbounds": [ + "socks-in" + ], + "outbounds": [ + "proxy", + "direct" + ], + "users": [ + "sekai" + ] + } +} +``` + +### Fields + +#### listen + +gRPC API listening address. V2Ray API will be disabled if empty. + +#### stats + +Traffic statistics service settings. + +#### stats.enabled + +Enable statistics service. + +#### stats.inbounds + +Inbound list to count traffic. + +#### stats.outbounds + +Outbound list to count traffic. + +#### stats.users + +User list to count traffic. \ No newline at end of file diff --git a/docs/configuration/route/geoip.md b/docs/configuration/route/geoip.md index b966a292a5..d6dfd23289 100644 --- a/docs/configuration/route/geoip.md +++ b/docs/configuration/route/geoip.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + ### Structure ```json diff --git a/docs/configuration/route/geoip.zh.md b/docs/configuration/route/geoip.zh.md deleted file mode 100644 index 3ee7042728..0000000000 --- a/docs/configuration/route/geoip.zh.md +++ /dev/null @@ -1,33 +0,0 @@ -### 结构 - -```json -{ - "route": { - "geoip": { - "path": "", - "download_url": "", - "download_detour": "" - } - } -} -``` - -### 字段 - -#### path - -指定 GeoIP 资源的路径。 - -默认 `geoip.db`。 - -#### download_url - -指定 GeoIP 资源的下载链接。 - -默认为 `https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db`。 - -#### download_detour - -用于下载 GeoIP 资源的出站的标签。 - -如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/geosite.md b/docs/configuration/route/geosite.md index db700c6af1..515e86b1ec 100644 --- a/docs/configuration/route/geosite.md +++ b/docs/configuration/route/geosite.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + ### Structure ```json diff --git a/docs/configuration/route/geosite.zh.md b/docs/configuration/route/geosite.zh.md deleted file mode 100644 index bee81fbf71..0000000000 --- a/docs/configuration/route/geosite.zh.md +++ /dev/null @@ -1,33 +0,0 @@ -### 结构 - -```json -{ - "route": { - "geosite": { - "path": "", - "download_url": "", - "download_detour": "" - } - } -} -``` - -### 字段 - -#### path - -指定 GeoSite 资源的路径。 - -默认 `geosite.db`。 - -#### download_url - -指定 GeoSite 资源的下载链接。 - -默认为 `https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db`。 - -#### download_detour - -用于下载 GeoSite 资源的出站的标签。 - -如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 846d497330..7c1787eaea 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -1,5 +1,15 @@ +--- +icon: material/alert-decagram +--- + # Route +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -8,6 +18,7 @@ "geoip": {}, "geosite": {}, "rules": [], + "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, @@ -19,11 +30,20 @@ ### Fields -| Key | Format | -|------------|------------------------------------| -| `geoip` | [GeoIP](./geoip) | -| `geosite` | [Geosite](./geosite) | -| `rules` | List of [Route Rule](./rule) | +| Key | Format | +|-----------|----------------------| +| `geoip` | [GeoIP](./geoip) | +| `geosite` | [Geosite](./geosite) | + +#### rules + +List of [Route Rule](./rule) + +#### rule_set + +!!! question "Since sing-box 1.8.0" + +List of [Rule Set](/configuration/rule-set) #### final diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index 8bef5bea9a..b5302727a3 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -1,5 +1,15 @@ +--- +icon: material/alert-decagram +--- + # 路由 +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -7,8 +17,8 @@ "route": { "geoip": {}, "geosite": {}, - "ip_rules": [], "rules": [], + "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, @@ -20,11 +30,21 @@ ### 字段 -| 键 | 格式 | -|------------|-------------------------| -| `geoip` | [GeoIP](./geoip) | -| `geosite` | [GeoSite](./geosite) | -| `rules` | 一组 [路由规则](./rule) | +| 键 | 格式 | +|------------|-----------------------------------| +| `geoip` | [GeoIP](./geoip) | +| `geosite` | [Geosite](./geosite) | + + +#### rule + +一组 [路由规则](./rule)。 + +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +一组 [规则集](/configuration/rule-set)。 #### final diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index abbfde6fdc..35c1d8fc24 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -1,3 +1,15 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -89,6 +101,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": "direct" }, @@ -160,23 +176,35 @@ Match domain using regular expression. #### geosite +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + Match geosite. #### source_geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + Match source geoip. #### geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + Match geoip. #### source_ip_cidr -Match source ip cidr. +Match source IP CIDR. #### ip_cidr -Match ip cidr. +Match IP CIDR. #### source_port @@ -250,6 +278,18 @@ Match WiFi SSID. Match WiFi BSSID. +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [Rule Set](/configuration/route/#rule_set). + +#### rule_set_ipcidr_match_source + +!!! question "Since sing-box 1.8.0" + +Make `ipcidr` in rule sets match the source IP. + #### invert Invert match result. @@ -276,4 +316,4 @@ Tag of the target outbound. ==Required== -Included default rules. +Included rules. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index f4ab7890a7..31ee08d13a 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -1,3 +1,15 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -87,6 +99,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": "direct" }, @@ -158,14 +174,26 @@ #### geosite -匹配 GeoSite。 +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-set)。 + +匹配 Geosite。 #### source_geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + 匹配源 GeoIP。 #### geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + 匹配 GeoIP。 #### source_ip_cidr @@ -248,6 +276,18 @@ 匹配 WiFi BSSID。 +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + +#### rule_set_ipcidr_match_source + +!!! question "自 sing-box 1.8.0 起" + +使规则集中的 `ipcidr` 规则匹配源 IP。 + #### invert 反选匹配结果。 @@ -274,4 +314,4 @@ ==必填== -包括的默认规则。 \ No newline at end of file +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md new file mode 100644 index 0000000000..6ab62eb2e3 --- /dev/null +++ b/docs/configuration/rule-set/headless-rule.md @@ -0,0 +1,207 @@ +--- +icon: material/new-box +--- + +### Structure + +!!! question "Since sing-box 1.8.0" + +```json +{ + "rules": [ + { + "query_type": [ + "A", + "HTTPS", + 32768 + ], + "network": [ + "tcp" + ], + "domain": [ + "test.com" + ], + "domain_suffix": [ + ".cn" + ], + "domain_keyword": [ + "test" + ], + "domain_regex": [ + "^stun\\..+" + ], + "source_ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "source_port": [ + 12345 + ], + "source_port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "port": [ + 80, + 443 + ], + "port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "process_name": [ + "curl" + ], + "process_path": [ + "/usr/bin/curl" + ], + "package_name": [ + "com.termux" + ], + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], + "invert": false + }, + { + "type": "logical", + "mode": "and", + "rules": [], + "invert": false + } + ] +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Default Fields + +!!! note "" + + The default rule uses the following matching logic: + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) && + (`port` || `port_range`) && + (`source_port` || `source_port_range`) && + `other fields` + +#### query_type + +DNS query type. Values can be integers or type name strings. + +#### network + +`tcp` or `udp`. + +#### domain + +Match full domain. + +#### domain_suffix + +Match domain suffix. + +#### domain_keyword + +Match domain using keyword. + +#### domain_regex + +Match domain using regular expression. + +#### source_ip_cidr + +Match source IP CIDR. + +#### ip_cidr + +!!! info "" + + `ip_cidr` is an alias for `source_ip_cidr` when the Rule Set is used in DNS rules or `rule_set_ipcidr_match_source` enabled in route rules. + +Match IP CIDR. + +#### source_port + +Match source port. + +#### source_port_range + +Match source port range. + +#### port + +Match port. + +#### port_range + +Match port range. + +#### process_name + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process name. + +#### process_path + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path. + +#### package_name + +Match android package name. + +#### wifi_ssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi SSID. + +#### wifi_bssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi BSSID. + +#### invert + +Invert match result. + +### Logical Fields + +#### type + +`logical` + +#### mode + +==Required== + +`and` or `or` + +#### rules + +==Required== + +Included rules. diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md new file mode 100644 index 0000000000..5aff55b371 --- /dev/null +++ b/docs/configuration/rule-set/index.md @@ -0,0 +1,97 @@ +--- +icon: material/new-box +--- + +# Rule Set + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "type": "", + "tag": "", + "format": "", + + ... // Typed Fields +} +``` + +#### Local Structure + +```json +{ + "type": "local", + + ... + + "path": "" +} +``` + +#### Remote Structure + +!!! info "" + + Remote rule-set will be cached if `experimental.cache_file.enabled`. + +```json +{ + "type": "remote", + + ..., + + "url": "", + "download_detour": "", + "update_interval": "" +} +``` + +### Fields + +#### type + +==Required== + +Type of Rule Set, `local` or `remote`. + +#### tag + +==Required== + +Tag of Rule Set. + +#### format + +==Required== + +Format of Rule Set, `source` or `binary`. + +### Local Fields + +#### path + +==Required== + +File path of Rule Set. + +### Remote Fields + +#### url + +==Required== + +Download URL of Rule Set. + +#### download_detour + +Tag of the outbound to download rule-set. + +Default outbound will be used if empty. + +#### update_interval + +Update interval of Rule Set. + +`1d` will be used if empty. diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md new file mode 100644 index 0000000000..116c1ee66f --- /dev/null +++ b/docs/configuration/rule-set/source-format.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +# Source Format + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "version": 1, + "rules": [] +} +``` + +### Compile + +Use `sing-box rule-set compile [--output .srs] .json` to compile source to binary rule-set. + +### Fields + +#### version + +==Required== + +Version of Rule Set, must be `1`. + +#### rules + +==Required== + +List of [Headless Rule](./headless-rule.md). diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000000..8eb3daee42 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,187 @@ +--- +icon: material/arrange-bring-forward +--- + +# Migration + +## 1.8.0 + +!!! warning "Unstable" + + This version is still under development, and the following migration guide may be changed in the future. + +### :material-close-box: Migrate cache file from Clash API to independent options + +!!! info "Reference" + + [Clash API](/configuration/experimental/clash-api) / + [Cache File](/configuration/experimental/cache-file) + +=== ":material-card-remove: Deprecated" + + ```json + { + "experimental": { + "clash_api": { + "cache_file": "cache.db", // default value + "cahce_id": "my_profile2", + "store_mode": true, + "store_selected": true, + "store_fakeip": true + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "experimental" : { + "cache_file": { + "enabled": true, + "path": "cache.db", // default value + "cache_id": "my_profile2", + "store_fakeip": true + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate GeoIP to rule sets + +!!! info "Reference" + + [GeoIP](/configuration/route/geoip) / + [Route](/configuration/route) / + [Route Rule](/configuration/route/rule) / + [DNS Rule](/configuration/dns/rule) / + [Rule Set](/configuration/rule-set) + +!!! tip + + `sing-box geoip` commands can help you convert custom GeoIP into rule sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geoip": "cn", + "outbound": "direct" + }, + { + "source_geoip": "cn", + "outbound": "block" + } + ], + "geoip": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geoip-cn", + "outbound": "direct" + }, + { + "rule_set": "geoip-us", + "rule_set_ipcidr_match_source": true, + "outbound": "block" + } + ], + "rule_set": [ + { + "tag": "geoip-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", + "download_detour": "proxy" + }, + { + "tag": "geoip-us", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate Geosite to rule sets + +!!! info "Reference" + + [Geosite](/configuration/route/geosite) / + [Route](/configuration/route) / + [Route Rule](/configuration/route/rule) / + [DNS Rule](/configuration/dns/rule) / + [Rule Set](/configuration/rule-set) + +!!! tip + + `sing-box geosite` commands can help you convert custom Geosite into rule sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geosite": "cn", + "outbound": "direct" + } + ], + "geosite": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geosite-cn", + "outbound": "direct" + } + ], + "rule_set": [ + { + "tag": "geosite-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 1d4b1d8b0e..c5dd7df335 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,12 +32,16 @@ theme: - content.code.copy - content.code.select - content.code.annotate + icon: + admonition: + question: material/new-box nav: - Home: - index.md + - Change Log: changelog.md + - Migration: migration.md - Deprecated: deprecated.md - Support: support.md - - Change Log: changelog.md - Installation: - Package Manager: installation/package-manager.md - Docker: installation/docker.md @@ -56,7 +60,7 @@ nav: - Proxy: - Server: manual/proxy/server.md - Client: manual/proxy/client.md -# - TUN: manual/proxy/tun.md + # - TUN: manual/proxy/tun.md - Proxy Protocol: - Shadowsocks: manual/proxy-protocol/shadowsocks.md - Trojan: manual/proxy-protocol/trojan.md @@ -79,8 +83,15 @@ nav: - Geosite: configuration/route/geosite.md - Route Rule: configuration/route/rule.md - Protocol Sniff: configuration/route/sniff.md + - Rule Set: + - configuration/rule-set/index.md + - Source Format: configuration/rule-set/source-format.md + - Headless Rule: configuration/rule-set/headless-rule.md - Experimental: - configuration/experimental/index.md + - Cache File: configuration/experimental/cache-file.md + - Clash API: configuration/experimental/clash-api.md + - V2Ray API: configuration/experimental/v2ray-api.md - Shared: - Listen Fields: configuration/shared/listen.md - Dial Fields: configuration/shared/dial.md @@ -180,9 +191,10 @@ plugins: name: 简体中文 nav_translations: Home: 开始 + Change Log: 更新日志 + Migration: 迁移指南 Deprecated: 废弃功能列表 Support: 支持 - Change Log: 更新日志 Installation: 安装 Package Manager: 包管理器 @@ -203,6 +215,10 @@ plugins: Route Rule: 路由规则 Protocol Sniff: 协议探测 + Rule Set: 规则集 + Source Format: 源文件格式 + Headless Rule: 无头规则 + Experimental: 实验性 Shared: 通用 @@ -215,10 +231,6 @@ plugins: Inbound: 入站 Outbound: 出站 - FAQ: 常见问题 - Known Issues: 已知问题 - Examples: 示例 - Linux Server Installation: Linux 服务器安装 - DNS Hijack: DNS 劫持 + Manual: 手册 reconfigure_material: true reconfigure_search: true \ No newline at end of file From 4d6b7ff0544a1005e04e088e1566a1c84a794d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Nov 2023 12:00:28 +0800 Subject: [PATCH 024/103] Migrate to independent cache file --- adapter/experimental.go | 10 ++- box.go | 53 ++++++++++-- .../{clashapi => }/cachefile/cache.go | 85 +++++++++++++------ .../{clashapi => }/cachefile/fakeip.go | 0 experimental/clashapi/cache.go | 11 ++- experimental/clashapi/server.go | 66 ++++---------- experimental/libbox/command_group.go | 21 ++--- experimental/libbox/command_urltest.go | 21 ++--- option/clash.go | 31 ------- option/experimental.go | 47 +++++++++- option/group.go | 15 ++++ option/v2ray.go | 12 --- outbound/builder.go | 2 +- outbound/selector.go | 15 ++-- route/router.go | 2 +- transport/fakeip/store.go | 15 ++-- 16 files changed, 227 insertions(+), 179 deletions(-) rename experimental/{clashapi => }/cachefile/cache.go (81%) rename experimental/{clashapi => }/cachefile/fakeip.go (100%) delete mode 100644 option/clash.go create mode 100644 option/group.go diff --git a/adapter/experimental.go b/adapter/experimental.go index 697fa9f199..ac523e4e74 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -13,15 +13,17 @@ type ClashServer interface { PreStarter Mode() string ModeList() []string - StoreSelected() bool - StoreFakeIP() bool - CacheFile() ClashCacheFile HistoryStorage() *urltest.HistoryStorage RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker) } -type ClashCacheFile interface { +type CacheFile interface { + Service + PreStarter + + StoreFakeIP() bool + LoadMode() string StoreMode(mode string) error LoadSelected(group string) string diff --git a/box.go b/box.go index c10222ddf7..d3838ca337 100644 --- a/box.go +++ b/box.go @@ -10,6 +10,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/inbound" "github.com/sagernet/sing-box/log" @@ -32,7 +33,8 @@ type Box struct { outbounds []adapter.Outbound logFactory log.Factory logger log.ContextLogger - preServices map[string]adapter.Service + preServices1 map[string]adapter.Service + preServices2 map[string]adapter.Service postServices map[string]adapter.Service done chan struct{} } @@ -45,17 +47,21 @@ type Options struct { } func New(options Options) (*Box, error) { + createdAt := time.Now() ctx := options.Context if ctx == nil { ctx = context.Background() } ctx = service.ContextWithDefaultRegistry(ctx) ctx = pause.ContextWithDefaultManager(ctx) - createdAt := time.Now() experimentalOptions := common.PtrValueOrDefault(options.Experimental) applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) + var needCacheFile bool var needClashAPI bool var needV2RayAPI bool + if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil { + needCacheFile = true + } if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil { needClashAPI = true } @@ -145,8 +151,14 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "initialize platform interface") } } - preServices := make(map[string]adapter.Service) + preServices1 := make(map[string]adapter.Service) + preServices2 := make(map[string]adapter.Service) postServices := make(map[string]adapter.Service) + if needCacheFile { + cacheFile := cachefile.NewCacheFile(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) + preServices1["cache file"] = cacheFile + service.MustRegister[adapter.CacheFile](ctx, cacheFile) + } if needClashAPI { clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options) @@ -155,7 +167,7 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "create clash api server") } router.SetClashServer(clashServer) - preServices["clash api"] = clashServer + preServices2["clash api"] = clashServer } if needV2RayAPI { v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI)) @@ -163,7 +175,7 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "create v2ray api server") } router.SetV2RayServer(v2rayServer) - preServices["v2ray api"] = v2rayServer + preServices2["v2ray api"] = v2rayServer } return &Box{ router: router, @@ -172,7 +184,8 @@ func New(options Options) (*Box, error) { createdAt: createdAt, logFactory: logFactory, logger: logFactory.Logger(), - preServices: preServices, + preServices1: preServices1, + preServices2: preServices2, postServices: postServices, done: make(chan struct{}), }, nil @@ -217,7 +230,16 @@ func (s *Box) Start() error { } func (s *Box) preStart() error { - for serviceName, service := range s.preServices { + for serviceName, service := range s.preServices1 { + if preService, isPreService := service.(adapter.PreStarter); isPreService { + s.logger.Trace("pre-start ", serviceName) + err := preService.PreStart() + if err != nil { + return E.Cause(err, "pre-starting ", serviceName) + } + } + } + for serviceName, service := range s.preServices2 { if preService, isPreService := service.(adapter.PreStarter); isPreService { s.logger.Trace("pre-start ", serviceName) err := preService.PreStart() @@ -238,7 +260,14 @@ func (s *Box) start() error { if err != nil { return err } - for serviceName, service := range s.preServices { + for serviceName, service := range s.preServices1 { + s.logger.Trace("starting ", serviceName) + err = service.Start() + if err != nil { + return E.Cause(err, "start ", serviceName) + } + } + for serviceName, service := range s.preServices2 { s.logger.Trace("starting ", serviceName) err = service.Start() if err != nil { @@ -313,7 +342,13 @@ func (s *Box) Close() error { return E.Cause(err, "close router") }) } - for serviceName, service := range s.preServices { + for serviceName, service := range s.preServices1 { + s.logger.Trace("closing ", serviceName) + errors = E.Append(errors, service.Close(), func(err error) error { + return E.Cause(err, "close ", serviceName) + }) + } + for serviceName, service := range s.preServices2 { s.logger.Trace("closing ", serviceName) errors = E.Append(errors, service.Close(), func(err error) error { return E.Cause(err, "close ", serviceName) diff --git a/experimental/clashapi/cachefile/cache.go b/experimental/cachefile/cache.go similarity index 81% rename from experimental/clashapi/cachefile/cache.go rename to experimental/cachefile/cache.go index 0911829749..2616ded5c1 100644 --- a/experimental/clashapi/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/bbolt" bboltErrors "github.com/sagernet/bbolt/errors" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service/filemanager" @@ -31,11 +32,15 @@ var ( cacheIDDefault = []byte("default") ) -var _ adapter.ClashCacheFile = (*CacheFile)(nil) +var _ adapter.CacheFile = (*CacheFile)(nil) type CacheFile struct { + ctx context.Context + path string + cacheID []byte + storeFakeIP bool + DB *bbolt.DB - cacheID []byte saveAccess sync.RWMutex saveDomain map[netip.Addr]string saveAddress4 map[string]netip.Addr @@ -43,7 +48,29 @@ type CacheFile struct { saveMetadataTimer *time.Timer } -func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) { +func NewCacheFile(ctx context.Context, options option.CacheFileOptions) *CacheFile { + var path string + if options.Path != "" { + path = options.Path + } else { + path = "cache.db" + } + var cacheIDBytes []byte + if options.CacheID != "" { + cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...) + } + return &CacheFile{ + ctx: ctx, + path: filemanager.BasePath(ctx, path), + cacheID: cacheIDBytes, + storeFakeIP: options.StoreFakeIP, + saveDomain: make(map[netip.Addr]string), + saveAddress4: make(map[string]netip.Addr), + saveAddress6: make(map[string]netip.Addr), + } +} + +func (c *CacheFile) start() error { const fileMode = 0o666 options := bbolt.Options{Timeout: time.Second} var ( @@ -51,7 +78,7 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) err error ) for i := 0; i < 10; i++ { - db, err = bbolt.Open(path, fileMode, &options) + db, err = bbolt.Open(c.path, fileMode, &options) if err == nil { break } @@ -59,23 +86,20 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) continue } if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) { - rmErr := os.Remove(path) + rmErr := os.Remove(c.path) if rmErr != nil { - return nil, err + return err } } time.Sleep(100 * time.Millisecond) } if err != nil { - return nil, err + return err } - err = filemanager.Chown(ctx, path) + err = filemanager.Chown(c.ctx, c.path) if err != nil { - return nil, E.Cause(err, "platform chown") - } - var cacheIDBytes []byte - if cacheID != "" { - cacheIDBytes = append([]byte{0}, []byte(cacheID)...) + db.Close() + return E.Cause(err, "platform chown") } err = db.Batch(func(tx *bbolt.Tx) error { return tx.ForEach(func(name []byte, b *bbolt.Bucket) error { @@ -97,15 +121,30 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) }) }) if err != nil { - return nil, err + db.Close() + return err } - return &CacheFile{ - DB: db, - cacheID: cacheIDBytes, - saveDomain: make(map[netip.Addr]string), - saveAddress4: make(map[string]netip.Addr), - saveAddress6: make(map[string]netip.Addr), - }, nil + c.DB = db + return nil +} + +func (c *CacheFile) PreStart() error { + return c.start() +} + +func (c *CacheFile) Start() error { + return nil +} + +func (c *CacheFile) Close() error { + if c.DB == nil { + return nil + } + return c.DB.Close() +} + +func (c *CacheFile) StoreFakeIP() bool { + return c.storeFakeIP } func (c *CacheFile) LoadMode() string { @@ -218,7 +257,3 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { } }) } - -func (c *CacheFile) Close() error { - return c.DB.Close() -} diff --git a/experimental/clashapi/cachefile/fakeip.go b/experimental/cachefile/fakeip.go similarity index 100% rename from experimental/clashapi/cachefile/fakeip.go rename to experimental/cachefile/fakeip.go diff --git a/experimental/clashapi/cache.go b/experimental/clashapi/cache.go index 7582fde5ae..9c088a82f7 100644 --- a/experimental/clashapi/cache.go +++ b/experimental/clashapi/cache.go @@ -1,23 +1,26 @@ package clashapi import ( + "context" "net/http" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/service" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) -func cacheRouter(router adapter.Router) http.Handler { +func cacheRouter(ctx context.Context) http.Handler { r := chi.NewRouter() - r.Post("/fakeip/flush", flushFakeip(router)) + r.Post("/fakeip/flush", flushFakeip(ctx)) return r } -func flushFakeip(router adapter.Router) func(w http.ResponseWriter, r *http.Request) { +func flushFakeip(ctx context.Context) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if cacheFile := router.ClashServer().CacheFile(); cacheFile != nil { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile != nil { err := cacheFile.FakeIPReset() if err != nil { render.Status(r, http.StatusInternalServerError) diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 6a3d6f66f7..c40ff93846 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -15,7 +15,6 @@ import ( "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" - "github.com/sagernet/sing-box/experimental/clashapi/cachefile" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -49,12 +48,6 @@ type Server struct { mode string modeList []string modeUpdateHook chan<- struct{} - storeMode bool - storeSelected bool - storeFakeIP bool - cacheFilePath string - cacheID string - cacheFile adapter.ClashCacheFile externalController bool externalUI string @@ -76,9 +69,6 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ trafficManager: trafficManager, modeList: options.ModeList, externalController: options.ExternalController != "", - storeMode: options.StoreMode, - storeSelected: options.StoreSelected, - storeFakeIP: options.StoreFakeIP, externalUIDownloadURL: options.ExternalUIDownloadURL, externalUIDownloadDetour: options.ExternalUIDownloadDetour, } @@ -94,18 +84,10 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ server.modeList = append([]string{defaultMode}, server.modeList...) } server.mode = defaultMode - if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.ExternalController == "" { - cachePath := os.ExpandEnv(options.CacheFile) - if cachePath == "" { - cachePath = "cache.db" - } - if foundPath, loaded := C.FindPath(cachePath); loaded { - cachePath = foundPath - } else { - cachePath = filemanager.BasePath(ctx, cachePath) - } - server.cacheFilePath = cachePath - server.cacheID = options.CacheID + //goland:noinspection GoDeprecation + //nolint:staticcheck + if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" { + return nil, E.New("cache_file and related fields in Clash API is deprecated in sing-box 1.8.0, use experimental.cache_file instead.") } cors := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, @@ -128,7 +110,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/script", scriptRouter()) r.Mount("/profile", profileRouter()) - r.Mount("/cache", cacheRouter(router)) + r.Mount("/cache", cacheRouter(ctx)) r.Mount("/dns", dnsRouter(router)) server.setupMetaAPI(r) @@ -147,19 +129,13 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ } func (s *Server) PreStart() error { - if s.cacheFilePath != "" { - cacheFile, err := cachefile.Open(s.ctx, s.cacheFilePath, s.cacheID) - if err != nil { - return E.Cause(err, "open cache file") - } - s.cacheFile = cacheFile - if s.storeMode { - mode := s.cacheFile.LoadMode() - if common.Any(s.modeList, func(it string) bool { - return strings.EqualFold(it, mode) - }) { - s.mode = mode - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + mode := cacheFile.LoadMode() + if common.Any(s.modeList, func(it string) bool { + return strings.EqualFold(it, mode) + }) { + s.mode = mode } } return nil @@ -187,7 +163,6 @@ func (s *Server) Close() error { return common.Close( common.PtrOrNil(s.httpServer), s.trafficManager, - s.cacheFile, s.urlTestHistory, ) } @@ -224,8 +199,9 @@ func (s *Server) SetMode(newMode string) { } } s.router.ClearDNSCache() - if s.storeMode { - err := s.cacheFile.StoreMode(newMode) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreMode(newMode) if err != nil { s.logger.Error(E.Cause(err, "save mode")) } @@ -233,18 +209,6 @@ func (s *Server) SetMode(newMode string) { s.logger.Info("updated mode: ", newMode) } -func (s *Server) StoreSelected() bool { - return s.storeSelected -} - -func (s *Server) StoreFakeIP() bool { - return s.storeFakeIP -} - -func (s *Server) CacheFile() adapter.ClashCacheFile { - return s.cacheFile -} - func (s *Server) HistoryStorage() *urltest.HistoryStorage { return s.urlTestHistory } diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go index 934820889e..2fc69b98b4 100644 --- a/experimental/libbox/command_group.go +++ b/experimental/libbox/command_group.go @@ -159,11 +159,7 @@ func readGroups(reader io.Reader) (OutboundGroupIterator, error) { func writeGroups(writer io.Writer, boxService *BoxService) error { historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx) - var cacheFile adapter.ClashCacheFile - if clashServer := boxService.instance.Router().ClashServer(); clashServer != nil { - cacheFile = clashServer.CacheFile() - } - + cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx) outbounds := boxService.instance.Router().Outbounds() var iGroups []adapter.OutboundGroup for _, it := range outbounds { @@ -288,16 +284,15 @@ func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return writeError(conn, E.New("service not ready")) } - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - err = cacheFile.StoreGroupExpand(groupTag, isExpand) - if err != nil { - return writeError(conn, err) - } + cacheFile := service.FromContext[adapter.CacheFile](serviceNow.ctx) + if cacheFile != nil { + err = cacheFile.StoreGroupExpand(groupTag, isExpand) + if err != nil { + return writeError(conn, err) } } return writeError(conn, nil) diff --git a/experimental/libbox/command_urltest.go b/experimental/libbox/command_urltest.go index 88e86a8f8c..3563d8c6a3 100644 --- a/experimental/libbox/command_urltest.go +++ b/experimental/libbox/command_urltest.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/service" ) func (c *CommandClient) URLTest(groupTag string) error { @@ -37,11 +38,11 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return nil } - abstractOutboundGroup, isLoaded := service.instance.Router().Outbound(groupTag) + abstractOutboundGroup, isLoaded := serviceNow.instance.Router().Outbound(groupTag) if !isLoaded { return writeError(conn, E.New("outbound group not found: ", groupTag)) } @@ -53,15 +54,9 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if isURLTest { go urlTest.CheckOutbounds() } else { - var historyStorage *urltest.HistoryStorage - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - historyStorage = clashServer.HistoryStorage() - } else { - return writeError(conn, E.New("Clash API is required for URLTest on non-URLTest group")) - } - + historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx) outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { - itOutbound, _ := service.instance.Router().Outbound(it) + itOutbound, _ := serviceNow.instance.Router().Outbound(it) return itOutbound }), func(it adapter.Outbound) bool { if it == nil { @@ -73,12 +68,12 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { } return true }) - b, _ := batch.New(service.ctx, batch.WithConcurrencyNum[any](10)) + b, _ := batch.New(serviceNow.ctx, batch.WithConcurrencyNum[any](10)) for _, detour := range outbounds { outboundToTest := detour outboundTag := outboundToTest.Tag() b.Go(outboundTag, func() (any, error) { - t, err := urltest.URLTest(service.ctx, "", outboundToTest) + t, err := urltest.URLTest(serviceNow.ctx, "", outboundToTest) if err != nil { historyStorage.DeleteURLTestHistory(outboundTag) } else { diff --git a/option/clash.go b/option/clash.go deleted file mode 100644 index 63ee2aebdf..0000000000 --- a/option/clash.go +++ /dev/null @@ -1,31 +0,0 @@ -package option - -type ClashAPIOptions struct { - ExternalController string `json:"external_controller,omitempty"` - ExternalUI string `json:"external_ui,omitempty"` - ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` - ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` - Secret string `json:"secret,omitempty"` - DefaultMode string `json:"default_mode,omitempty"` - StoreMode bool `json:"store_mode,omitempty"` - StoreSelected bool `json:"store_selected,omitempty"` - StoreFakeIP bool `json:"store_fakeip,omitempty"` - CacheFile string `json:"cache_file,omitempty"` - CacheID string `json:"cache_id,omitempty"` - - ModeList []string `json:"-"` -} - -type SelectorOutboundOptions struct { - Outbounds []string `json:"outbounds"` - Default string `json:"default,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} - -type URLTestOutboundOptions struct { - Outbounds []string `json:"outbounds"` - URL string `json:"url,omitempty"` - Interval Duration `json:"interval,omitempty"` - Tolerance uint16 `json:"tolerance,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} diff --git a/option/experimental.go b/option/experimental.go index a5b6acbd32..72751a590e 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -1,7 +1,48 @@ package option type ExperimentalOptions struct { - ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` - V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` - Debug *DebugOptions `json:"debug,omitempty"` + CacheFile *CacheFileOptions `json:"cache_file,omitempty"` + ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` + V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` + Debug *DebugOptions `json:"debug,omitempty"` +} + +type CacheFileOptions struct { + Enabled bool `json:"enabled,omitempty"` + Path string `json:"path,omitempty"` + CacheID string `json:"cache_id,omitempty"` + StoreFakeIP bool `json:"store_fakeip,omitempty"` +} + +type ClashAPIOptions struct { + ExternalController string `json:"external_controller,omitempty"` + ExternalUI string `json:"external_ui,omitempty"` + ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` + ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` + Secret string `json:"secret,omitempty"` + DefaultMode string `json:"default_mode,omitempty"` + ModeList []string `json:"-"` + + // Deprecated: migrated to global cache file + StoreMode bool `json:"store_mode,omitempty"` + // Deprecated: migrated to global cache file + StoreSelected bool `json:"store_selected,omitempty"` + // Deprecated: migrated to global cache file + StoreFakeIP bool `json:"store_fakeip,omitempty"` + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` +} + +type V2RayAPIOptions struct { + Listen string `json:"listen,omitempty"` + Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` +} + +type V2RayStatsServiceOptions struct { + Enabled bool `json:"enabled,omitempty"` + Inbounds []string `json:"inbounds,omitempty"` + Outbounds []string `json:"outbounds,omitempty"` + Users []string `json:"users,omitempty"` } diff --git a/option/group.go b/option/group.go new file mode 100644 index 0000000000..58824e808b --- /dev/null +++ b/option/group.go @@ -0,0 +1,15 @@ +package option + +type SelectorOutboundOptions struct { + Outbounds []string `json:"outbounds"` + Default string `json:"default,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} + +type URLTestOutboundOptions struct { + Outbounds []string `json:"outbounds"` + URL string `json:"url,omitempty"` + Interval Duration `json:"interval,omitempty"` + Tolerance uint16 `json:"tolerance,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} diff --git a/option/v2ray.go b/option/v2ray.go index 37f7b8c4b0..774a651d1d 100644 --- a/option/v2ray.go +++ b/option/v2ray.go @@ -1,13 +1 @@ package option - -type V2RayAPIOptions struct { - Listen string `json:"listen,omitempty"` - Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` -} - -type V2RayStatsServiceOptions struct { - Enabled bool `json:"enabled,omitempty"` - Inbounds []string `json:"inbounds,omitempty"` - Outbounds []string `json:"outbounds,omitempty"` - Users []string `json:"users,omitempty"` -} diff --git a/outbound/builder.go b/outbound/builder.go index 141758d870..e4d6a80e06 100644 --- a/outbound/builder.go +++ b/outbound/builder.go @@ -56,7 +56,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t case C.TypeHysteria2: return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options) case C.TypeSelector: - return NewSelector(router, logger, tag, options.SelectorOptions) + return NewSelector(ctx, router, logger, tag, options.SelectorOptions) case C.TypeURLTest: return NewURLTest(ctx, router, logger, tag, options.URLTestOptions) default: diff --git a/outbound/selector.go b/outbound/selector.go index c66591cdee..e801daeadc 100644 --- a/outbound/selector.go +++ b/outbound/selector.go @@ -12,6 +12,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" ) var ( @@ -21,6 +22,7 @@ var ( type Selector struct { myOutboundAdapter + ctx context.Context tags []string defaultTag string outbounds map[string]adapter.Outbound @@ -29,7 +31,7 @@ type Selector struct { interruptExternalConnections bool } -func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { +func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { outbound := &Selector{ myOutboundAdapter: myOutboundAdapter{ protocol: C.TypeSelector, @@ -38,6 +40,7 @@ func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, op tag: tag, dependencies: options.Outbounds, }, + ctx: ctx, tags: options.Outbounds, defaultTag: options.Default, outbounds: make(map[string]adapter.Outbound), @@ -67,8 +70,9 @@ func (s *Selector) Start() error { } if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - selected := clashServer.CacheFile().LoadSelected(s.tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + selected := cacheFile.LoadSelected(s.tag) if selected != "" { detour, loaded := s.outbounds[selected] if loaded { @@ -110,8 +114,9 @@ func (s *Selector) SelectOutbound(tag string) bool { } s.selected = detour if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - err := clashServer.CacheFile().StoreSelected(s.tag, tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreSelected(s.tag, tag) if err != nil { s.logger.Error("store selected: ", err) } diff --git a/route/router.go b/route/router.go index 972c4ec048..e50a51a955 100644 --- a/route/router.go +++ b/route/router.go @@ -261,7 +261,7 @@ func NewRouter( if fakeIPOptions.Inet6Range != nil { inet6Range = *fakeIPOptions.Inet6Range } - router.fakeIPStore = fakeip.NewStore(router, router.logger, inet4Range, inet6Range) + router.fakeIPStore = fakeip.NewStore(ctx, router.logger, inet4Range, inet6Range) } usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor() diff --git a/transport/fakeip/store.go b/transport/fakeip/store.go index 96f6bf031e..83677b0d05 100644 --- a/transport/fakeip/store.go +++ b/transport/fakeip/store.go @@ -1,17 +1,19 @@ package fakeip import ( + "context" "net/netip" "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" ) var _ adapter.FakeIPStore = (*Store)(nil) type Store struct { - router adapter.Router + ctx context.Context logger logger.Logger inet4Range netip.Prefix inet6Range netip.Prefix @@ -20,9 +22,9 @@ type Store struct { inet6Current netip.Addr } -func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { +func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { return &Store{ - router: router, + ctx: ctx, logger: logger, inet4Range: inet4Range, inet6Range: inet6Range, @@ -31,10 +33,9 @@ func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Pref func (s *Store) Start() error { var storage adapter.FakeIPStorage - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreFakeIP() { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - storage = cacheFile - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil && cacheFile.StoreFakeIP() { + storage = cacheFile } if storage == nil { storage = NewMemoryStorage() From 33881ebd8c8b1550e7369c6ce80c0df293d6c14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Nov 2023 23:47:32 +0800 Subject: [PATCH 025/103] Allow nested logical rules --- experimental/clashapi.go | 42 +++++++++++++++++++++++------------ option/rule.go | 21 +++++++++++++----- option/rule_dns.go | 25 +++++++++++++++------ route/router.go | 4 ++-- route/router_geo_resources.go | 12 ++++------ route/rule_default.go | 8 +++---- route/rule_dns.go | 8 +++---- 7 files changed, 76 insertions(+), 44 deletions(-) diff --git a/experimental/clashapi.go b/experimental/clashapi.go index 894d40a74a..805fbd5be7 100644 --- a/experimental/clashapi.go +++ b/experimental/clashapi.go @@ -5,6 +5,7 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" @@ -27,24 +28,37 @@ func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.O func CalculateClashModeList(options option.Options) []string { var clashMode []string - for _, dnsRule := range common.PtrValueOrDefault(options.DNS).Rules { - if dnsRule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, dnsRule.DefaultOptions.ClashMode) { - clashMode = append(clashMode, dnsRule.DefaultOptions.ClashMode) - } - for _, defaultRule := range dnsRule.LogicalOptions.Rules { - if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) { - clashMode = append(clashMode, defaultRule.ClashMode) + clashMode = append(clashMode, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...) + clashMode = append(clashMode, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...) + clashMode = common.FilterNotDefault(common.Uniq(clashMode)) + return clashMode +} + +func extraClashModeFromRule(rules []option.Rule) []string { + var clashMode []string + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if rule.DefaultOptions.ClashMode != "" { + clashMode = append(clashMode, rule.DefaultOptions.ClashMode) } + case C.RuleTypeLogical: + clashMode = append(clashMode, extraClashModeFromRule(rule.LogicalOptions.Rules)...) } } - for _, rule := range common.PtrValueOrDefault(options.Route).Rules { - if rule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, rule.DefaultOptions.ClashMode) { - clashMode = append(clashMode, rule.DefaultOptions.ClashMode) - } - for _, defaultRule := range rule.LogicalOptions.Rules { - if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) { - clashMode = append(clashMode, defaultRule.ClashMode) + return clashMode +} + +func extraClashModeFromDNSRule(rules []option.DNSRule) []string { + var clashMode []string + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if rule.DefaultOptions.ClashMode != "" { + clashMode = append(clashMode, rule.DefaultOptions.ClashMode) } + case C.RuleTypeLogical: + clashMode = append(clashMode, extraClashModeFromDNSRule(rule.LogicalOptions.Rules)...) } } return clashMode diff --git a/option/rule.go b/option/rule.go index 8caba96e57..4f4042025f 100644 --- a/option/rule.go +++ b/option/rule.go @@ -53,6 +53,17 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error { return nil } +func (r Rule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + type DefaultRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` @@ -92,12 +103,12 @@ func (r DefaultRule) IsValid() bool { } type LogicalRule struct { - Mode string `json:"mode"` - Rules []DefaultRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Mode string `json:"mode"` + Rules []Rule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` } func (r LogicalRule) IsValid() bool { - return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid) + return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid) } diff --git a/option/rule_dns.go b/option/rule_dns.go index ba572b9aa2..fca3432293 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -53,6 +53,17 @@ func (r *DNSRule) UnmarshalJSON(bytes []byte) error { return nil } +func (r DNSRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown DNS rule type: " + r.Type) + } +} + type DefaultDNSRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` @@ -96,14 +107,14 @@ func (r DefaultDNSRule) IsValid() bool { } type LogicalDNSRule struct { - Mode string `json:"mode"` - Rules []DefaultDNSRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + Mode string `json:"mode"` + Rules []DNSRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Server string `json:"server,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` } func (r LogicalDNSRule) IsValid() bool { - return len(r.Rules) > 0 && common.All(r.Rules, DefaultDNSRule.IsValid) + return len(r.Rules) > 0 && common.All(r.Rules, DNSRule.IsValid) } diff --git a/route/router.go b/route/router.go index e50a51a955..b48d2346d9 100644 --- a/route/router.go +++ b/route/router.go @@ -127,14 +127,14 @@ func NewRouter( Logger: router.dnsLogger, }) for i, ruleOptions := range options.Rules { - routeRule, err := NewRule(router, router.logger, ruleOptions) + routeRule, err := NewRule(router, router.logger, ruleOptions, true) if err != nil { return nil, E.Cause(err, "parse rule[", i, "]") } router.rules = append(router.rules, routeRule) } for i, dnsRuleOptions := range dnsOptions.Rules { - dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions) + dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions, true) if err != nil { return nil, E.Cause(err, "parse dns rule[", i, "]") } diff --git a/route/router_geo_resources.go b/route/router_geo_resources.go index 8715cf922c..638d00df62 100644 --- a/route/router_geo_resources.go +++ b/route/router_geo_resources.go @@ -252,10 +252,8 @@ func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool return true } case C.RuleTypeLogical: - for _, subRule := range rule.LogicalOptions.Rules { - if cond(subRule) { - return true - } + if hasRule(rule.LogicalOptions.Rules, cond) { + return true } } } @@ -270,10 +268,8 @@ func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bo return true } case C.RuleTypeLogical: - for _, subRule := range rule.LogicalOptions.Rules { - if cond(subRule) { - return true - } + if hasDNSRule(rule.LogicalOptions.Rules, cond) { + return true } } } diff --git a/route/rule_default.go b/route/rule_default.go index 2d62f97a46..8c8473abf5 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -8,13 +8,13 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule) (adapter.Rule, error) { +func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule, checkOutbound bool) (adapter.Rule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - if options.DefaultOptions.Outbound == "" { + if options.DefaultOptions.Outbound == "" && checkOutbound { return nil, E.New("missing outbound field") } return NewDefaultRule(router, logger, options.DefaultOptions) @@ -22,7 +22,7 @@ func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rul if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - if options.LogicalOptions.Outbound == "" { + if options.LogicalOptions.Outbound == "" && checkOutbound { return nil, E.New("missing outbound field") } return NewLogicalRule(router, logger, options.LogicalOptions) @@ -220,7 +220,7 @@ func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options opt return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDefaultRule(router, logger, subRule) + rule, err := NewRule(router, logger, subRule, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } diff --git a/route/rule_dns.go b/route/rule_dns.go index 5132f02456..b444932582 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -8,13 +8,13 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule) (adapter.DNSRule, error) { +func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule, checkServer bool) (adapter.DNSRule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - if options.DefaultOptions.Server == "" { + if options.DefaultOptions.Server == "" && checkServer { return nil, E.New("missing server field") } return NewDefaultDNSRule(router, logger, options.DefaultOptions) @@ -22,7 +22,7 @@ func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option. if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - if options.LogicalOptions.Server == "" { + if options.LogicalOptions.Server == "" && checkServer { return nil, E.New("missing server field") } return NewLogicalDNSRule(router, logger, options.LogicalOptions) @@ -228,7 +228,7 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDefaultDNSRule(router, logger, subRule) + rule, err := NewDNSRule(router, logger, subRule, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } From 4e4c0820d582e8baefa25b99b63c5b6bf6d2f346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 29 Nov 2023 17:35:40 +0800 Subject: [PATCH 026/103] Add rule set --- .gitignore | 1 + adapter/experimental.go | 66 ++- adapter/inbound.go | 1 + adapter/router.go | 15 +- cmd/sing-box/cmd_format.go | 39 -- cmd/sing-box/cmd_geoip.go | 43 ++ cmd/sing-box/cmd_geoip_export.go | 98 ++++ cmd/sing-box/cmd_geoip_list.go | 31 ++ cmd/sing-box/cmd_geoip_lookup.go | 47 ++ cmd/sing-box/cmd_geosite.go | 41 ++ cmd/sing-box/cmd_geosite_export.go | 81 +++ cmd/sing-box/cmd_geosite_list.go | 50 ++ cmd/sing-box/cmd_geosite_lookup.go | 97 ++++ cmd/sing-box/cmd_geosite_matcher.go | 56 ++ cmd/sing-box/cmd_merge.go | 2 +- cmd/sing-box/cmd_rule_set.go | 14 + cmd/sing-box/cmd_rule_set_compile.go | 80 +++ cmd/sing-box/cmd_rule_set_format.go | 87 ++++ cmd/sing-box/cmd_tools.go | 6 +- cmd/sing-box/cmd_tools_connect.go | 2 +- common/dialer/router.go | 12 +- common/srs/binary.go | 485 ++++++++++++++++++ common/srs/ip_set.go | 116 +++++ constant/rule.go | 8 + experimental/cachefile/cache.go | 35 ++ experimental/cachefile/fakeip.go | 2 +- experimental/clashapi/proxies.go | 6 +- experimental/clashapi/server_resources.go | 6 +- .../clashapi/trafficontrol/tracker.go | 8 +- go.mod | 2 +- go.sum | 4 +- option/experimental.go | 8 +- option/route.go | 1 + option/rule.go | 58 ++- option/rule_dns.go | 1 + option/rule_set.go | 230 +++++++++ option/types.go | 8 + route/router.go | 34 +- route/rule_abstract.go | 22 +- route/rule_default.go | 7 +- route/rule_dns.go | 7 +- route/rule_headless.go | 173 +++++++ route/rule_item_cidr.go | 19 +- route/rule_item_domain.go | 7 + route/rule_item_rule_set.go | 55 ++ route/rule_set.go | 22 + route/rule_set_local.go | 69 +++ route/rule_set_remote.go | 218 ++++++++ 48 files changed, 2375 insertions(+), 105 deletions(-) create mode 100644 cmd/sing-box/cmd_geoip.go create mode 100644 cmd/sing-box/cmd_geoip_export.go create mode 100644 cmd/sing-box/cmd_geoip_list.go create mode 100644 cmd/sing-box/cmd_geoip_lookup.go create mode 100644 cmd/sing-box/cmd_geosite.go create mode 100644 cmd/sing-box/cmd_geosite_export.go create mode 100644 cmd/sing-box/cmd_geosite_list.go create mode 100644 cmd/sing-box/cmd_geosite_lookup.go create mode 100644 cmd/sing-box/cmd_geosite_matcher.go create mode 100644 cmd/sing-box/cmd_rule_set.go create mode 100644 cmd/sing-box/cmd_rule_set_compile.go create mode 100644 cmd/sing-box/cmd_rule_set_format.go create mode 100644 common/srs/binary.go create mode 100644 common/srs/ip_set.go create mode 100644 option/rule_set.go create mode 100644 route/rule_headless.go create mode 100644 route/rule_item_rule_set.go create mode 100644 route/rule_set.go create mode 100644 route/rule_set_local.go create mode 100644 route/rule_set_remote.go diff --git a/.gitignore b/.gitignore index 6630f428ce..55bdab3a0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.idea/ /vendor/ /*.json +/*.srs /*.db /site/ /bin/ diff --git a/adapter/experimental.go b/adapter/experimental.go index ac523e4e74..c26ee8c1cf 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -1,11 +1,16 @@ package adapter import ( + "bytes" "context" + "encoding/binary" + "io" "net" + "time" "github.com/sagernet/sing-box/common/urltest" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" ) type ClashServer interface { @@ -23,6 +28,7 @@ type CacheFile interface { PreStarter StoreFakeIP() bool + FakeIPStorage LoadMode() string StoreMode(mode string) error @@ -30,7 +36,65 @@ type CacheFile interface { StoreSelected(group string, selected string) error LoadGroupExpand(group string) (isExpand bool, loaded bool) StoreGroupExpand(group string, expand bool) error - FakeIPStorage + LoadRuleSet(tag string) *SavedRuleSet + SaveRuleSet(tag string, set *SavedRuleSet) error +} + +type SavedRuleSet struct { + Content []byte + LastUpdated time.Time + LastEtag string +} + +func (s *SavedRuleSet) MarshalBinary() ([]byte, error) { + var buffer bytes.Buffer + err := binary.Write(&buffer, binary.BigEndian, uint8(1)) + if err != nil { + return nil, err + } + err = rw.WriteUVariant(&buffer, uint64(len(s.Content))) + if err != nil { + return nil, err + } + buffer.Write(s.Content) + err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix()) + if err != nil { + return nil, err + } + err = rw.WriteVString(&buffer, s.LastEtag) + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func (s *SavedRuleSet) UnmarshalBinary(data []byte) error { + reader := bytes.NewReader(data) + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return err + } + contentLen, err := rw.ReadUVariant(reader) + if err != nil { + return err + } + s.Content = make([]byte, contentLen) + _, err = io.ReadFull(reader, s.Content) + if err != nil { + return err + } + var lastUpdated int64 + err = binary.Read(reader, binary.BigEndian, &lastUpdated) + if err != nil { + return err + } + s.LastUpdated = time.Unix(lastUpdated, 0) + s.LastEtag, err = rw.ReadVString(reader) + if err != nil { + return err + } + return nil } type Tracker interface { diff --git a/adapter/inbound.go b/adapter/inbound.go index 2d24083c4a..30dec9d1f7 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -47,6 +47,7 @@ type InboundContext struct { GeoIPCode string ProcessInfo *process.Info FakeIP bool + IPCIDRMatchSource bool // dns cache diff --git a/adapter/router.go b/adapter/router.go index e4c3904d51..ca4d6547c4 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -18,7 +18,7 @@ type Router interface { Outbounds() []Outbound Outbound(tag string) (Outbound, bool) - DefaultOutbound(network string) Outbound + DefaultOutbound(network string) (Outbound, error) FakeIPStore() FakeIPStore @@ -27,6 +27,8 @@ type Router interface { GeoIPReader() *geoip.Reader LoadGeosite(code string) (Rule, error) + RuleSet(tag string) (RuleSet, bool) + Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) @@ -61,11 +63,15 @@ func RouterFromContext(ctx context.Context) Router { return service.FromContext[Router](ctx) } +type HeadlessRule interface { + Match(metadata *InboundContext) bool +} + type Rule interface { + HeadlessRule Service Type() string UpdateGeosite() error - Match(metadata *InboundContext) bool Outbound() string String() string } @@ -76,6 +82,11 @@ type DNSRule interface { RewriteTTL() *uint32 } +type RuleSet interface { + HeadlessRule + Service +} + type InterfaceUpdateListener interface { InterfaceUpdated() } diff --git a/cmd/sing-box/cmd_format.go b/cmd/sing-box/cmd_format.go index 10a5497cd4..c5e939e4a9 100644 --- a/cmd/sing-box/cmd_format.go +++ b/cmd/sing-box/cmd_format.go @@ -7,7 +7,6 @@ import ( "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/spf13/cobra" @@ -69,41 +68,3 @@ func format() error { } return nil } - -func formatOne(configPath string) error { - configContent, err := os.ReadFile(configPath) - if err != nil { - return E.Cause(err, "read config") - } - var options option.Options - err = options.UnmarshalJSON(configContent) - if err != nil { - return E.Cause(err, "decode config") - } - buffer := new(bytes.Buffer) - encoder := json.NewEncoder(buffer) - encoder.SetIndent("", " ") - err = encoder.Encode(options) - if err != nil { - return E.Cause(err, "encode config") - } - if !commandFormatFlagWrite { - os.Stdout.WriteString(buffer.String() + "\n") - return nil - } - if bytes.Equal(configContent, buffer.Bytes()) { - return nil - } - output, err := os.Create(configPath) - if err != nil { - return E.Cause(err, "open output") - } - _, err = output.Write(buffer.Bytes()) - output.Close() - if err != nil { - return E.Cause(err, "write output") - } - outputPath, _ := filepath.Abs(configPath) - os.Stderr.WriteString(outputPath + "\n") - return nil -} diff --git a/cmd/sing-box/cmd_geoip.go b/cmd/sing-box/cmd_geoip.go new file mode 100644 index 0000000000..dbbbff135e --- /dev/null +++ b/cmd/sing-box/cmd_geoip.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var ( + geoipReader *maxminddb.Reader + commandGeoIPFlagFile string +) + +var commandGeoip = &cobra.Command{ + Use: "geoip", + Short: "GeoIP tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geoipPreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.PersistentFlags().StringVarP(&commandGeoIPFlagFile, "file", "f", "geoip.db", "geoip file") + mainCommand.AddCommand(commandGeoip) +} + +func geoipPreRun() error { + reader, err := maxminddb.Open(commandGeoIPFlagFile) + if err != nil { + return err + } + if reader.Metadata.DatabaseType != "sing-geoip" { + reader.Close() + return E.New("incorrect database type, expected sing-geoip, got ", reader.Metadata.DatabaseType) + } + geoipReader = reader + return nil +} diff --git a/cmd/sing-box/cmd_geoip_export.go b/cmd/sing-box/cmd_geoip_export.go new file mode 100644 index 0000000000..d170d10b72 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_export.go @@ -0,0 +1,98 @@ +package main + +import ( + "io" + "net" + "os" + "strings" + + "github.com/sagernet/sing-box/common/json" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var flagGeoipExportOutput string + +const flagGeoipExportDefaultOutput = "geoip-.srs" + +var commandGeoipExport = &cobra.Command{ + Use: "export ", + Short: "Export geoip country as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoipExport.Flags().StringVarP(&flagGeoipExportOutput, "output", "o", flagGeoipExportDefaultOutput, "Output path") + commandGeoip.AddCommand(commandGeoipExport) +} + +func geoipExport(countryCode string) error { + networks := geoipReader.Networks(maxminddb.SkipAliasedNetworks) + countryMap := make(map[string][]*net.IPNet) + var ( + ipNet *net.IPNet + nextCountryCode string + err error + ) + for networks.Next() { + ipNet, err = networks.Network(&nextCountryCode) + if err != nil { + return err + } + countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet) + } + ipNets := countryMap[strings.ToLower(countryCode)] + if len(ipNets) == 0 { + return E.New("country code not found: ", countryCode) + } + + var ( + outputFile *os.File + outputWriter io.Writer + ) + if flagGeoipExportOutput == "stdout" { + outputWriter = os.Stdout + } else if flagGeoipExportOutput == flagGeoipExportDefaultOutput { + outputFile, err = os.Create("geoip-" + countryCode + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(flagGeoipExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + headlessRule.IPCIDR = make([]string, 0, len(ipNets)) + for _, cidr := range ipNets { + headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String()) + } + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geoip_list.go b/cmd/sing-box/cmd_geoip_list.go new file mode 100644 index 0000000000..54dd426ea9 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_list.go @@ -0,0 +1,31 @@ +package main + +import ( + "os" + + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var commandGeoipList = &cobra.Command{ + Use: "list", + Short: "List geoip country codes", + Run: func(cmd *cobra.Command, args []string) { + err := listGeoip() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipList) +} + +func listGeoip() error { + for _, code := range geoipReader.Metadata.Languages { + os.Stdout.WriteString(code + "\n") + } + return nil +} diff --git a/cmd/sing-box/cmd_geoip_lookup.go b/cmd/sing-box/cmd_geoip_lookup.go new file mode 100644 index 0000000000..d5157bb404 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_lookup.go @@ -0,0 +1,47 @@ +package main + +import ( + "net/netip" + "os" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + + "github.com/spf13/cobra" +) + +var commandGeoipLookup = &cobra.Command{ + Use: "lookup
", + Short: "Lookup if an IP address is contained in the GeoIP database", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipLookup(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipLookup) +} + +func geoipLookup(address string) error { + addr, err := netip.ParseAddr(address) + if err != nil { + return E.Cause(err, "parse address") + } + if !N.IsPublicAddr(addr) { + os.Stdout.WriteString("private\n") + return nil + } + var code string + _ = geoipReader.Lookup(addr.AsSlice(), &code) + if code != "" { + os.Stdout.WriteString(code + "\n") + return nil + } + os.Stdout.WriteString("unknown\n") + return nil +} diff --git a/cmd/sing-box/cmd_geosite.go b/cmd/sing-box/cmd_geosite.go new file mode 100644 index 0000000000..95db935797 --- /dev/null +++ b/cmd/sing-box/cmd_geosite.go @@ -0,0 +1,41 @@ +package main + +import ( + "github.com/sagernet/sing-box/common/geosite" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var ( + commandGeoSiteFlagFile string + geositeReader *geosite.Reader + geositeCodeList []string +) + +var commandGeoSite = &cobra.Command{ + Use: "geosite", + Short: "Geosite tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geositePreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.PersistentFlags().StringVarP(&commandGeoSiteFlagFile, "file", "f", "geosite.db", "geosite file") + mainCommand.AddCommand(commandGeoSite) +} + +func geositePreRun() error { + reader, codeList, err := geosite.Open(commandGeoSiteFlagFile) + if err != nil { + return E.Cause(err, "open geosite file") + } + geositeReader = reader + geositeCodeList = codeList + return nil +} diff --git a/cmd/sing-box/cmd_geosite_export.go b/cmd/sing-box/cmd_geosite_export.go new file mode 100644 index 0000000000..71f1018d8f --- /dev/null +++ b/cmd/sing-box/cmd_geosite_export.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/json" + "io" + "os" + + "github.com/sagernet/sing-box/common/geosite" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "github.com/spf13/cobra" +) + +var commandGeositeExportOutput string + +const commandGeositeExportDefaultOutput = "geosite-.json" + +var commandGeositeExport = &cobra.Command{ + Use: "export ", + Short: "Export geosite category as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geositeExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeositeExport.Flags().StringVarP(&commandGeositeExportOutput, "output", "o", commandGeositeExportDefaultOutput, "Output path") + commandGeoSite.AddCommand(commandGeositeExport) +} + +func geositeExport(category string) error { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + var ( + outputFile *os.File + outputWriter io.Writer + ) + if commandGeositeExportOutput == "stdout" { + outputWriter = os.Stdout + } else if commandGeositeExportOutput == commandGeositeExportDefaultOutput { + outputFile, err = os.Create("geosite-" + category + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(commandGeositeExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + defaultRule := geosite.Compile(sourceSet) + headlessRule.Domain = defaultRule.Domain + headlessRule.DomainSuffix = defaultRule.DomainSuffix + headlessRule.DomainKeyword = defaultRule.DomainKeyword + headlessRule.DomainRegex = defaultRule.DomainRegex + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geosite_list.go b/cmd/sing-box/cmd_geosite_list.go new file mode 100644 index 0000000000..cedb7adfd2 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_list.go @@ -0,0 +1,50 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + F "github.com/sagernet/sing/common/format" + + "github.com/spf13/cobra" +) + +var commandGeositeList = &cobra.Command{ + Use: "list ", + Short: "List geosite categories", + Run: func(cmd *cobra.Command, args []string) { + err := geositeList() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeList) +} + +func geositeList() error { + var geositeEntry []struct { + category string + items int + } + for _, category := range geositeCodeList { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + geositeEntry = append(geositeEntry, struct { + category string + items int + }{category, len(sourceSet)}) + } + sort.SliceStable(geositeEntry, func(i, j int) bool { + return geositeEntry[i].items < geositeEntry[j].items + }) + for _, entry := range geositeEntry { + os.Stdout.WriteString(F.ToString(entry.category, " (", entry.items, ")\n")) + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_lookup.go b/cmd/sing-box/cmd_geosite_lookup.go new file mode 100644 index 0000000000..f648ce62a4 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_lookup.go @@ -0,0 +1,97 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandGeositeLookup = &cobra.Command{ + Use: "lookup [category] ", + Short: "Check if a domain is in the geosite", + Args: cobra.RangeArgs(1, 2), + Run: func(cmd *cobra.Command, args []string) { + var ( + source string + target string + ) + switch len(args) { + case 1: + target = args[0] + case 2: + source = args[0] + target = args[1] + } + err := geositeLookup(source, target) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeLookup) +} + +func geositeLookup(source string, target string) error { + var sourceMatcherList []struct { + code string + matcher *searchGeositeMatcher + } + if source != "" { + sourceSet, err := geositeReader.Read(source) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+source) + } + sourceMatcherList = []struct { + code string + matcher *searchGeositeMatcher + }{ + { + code: source, + matcher: sourceMatcher, + }, + } + + } else { + for _, code := range geositeCodeList { + sourceSet, err := geositeReader.Read(code) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+code) + } + sourceMatcherList = append(sourceMatcherList, struct { + code string + matcher *searchGeositeMatcher + }{ + code: code, + matcher: sourceMatcher, + }) + } + } + sort.SliceStable(sourceMatcherList, func(i, j int) bool { + return sourceMatcherList[i].code < sourceMatcherList[j].code + }) + + for _, matcherItem := range sourceMatcherList { + if matchRule := matcherItem.matcher.Match(target); matchRule != "" { + os.Stdout.WriteString("Match code (") + os.Stdout.WriteString(matcherItem.code) + os.Stdout.WriteString(") ") + os.Stdout.WriteString(matchRule) + os.Stdout.WriteString("\n") + } + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_matcher.go b/cmd/sing-box/cmd_geosite_matcher.go new file mode 100644 index 0000000000..791dba2499 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_matcher.go @@ -0,0 +1,56 @@ +package main + +import ( + "regexp" + "strings" + + "github.com/sagernet/sing-box/common/geosite" +) + +type searchGeositeMatcher struct { + domainMap map[string]bool + suffixList []string + keywordList []string + regexList []string +} + +func newSearchGeositeMatcher(items []geosite.Item) (*searchGeositeMatcher, error) { + options := geosite.Compile(items) + domainMap := make(map[string]bool) + for _, domain := range options.Domain { + domainMap[domain] = true + } + rule := &searchGeositeMatcher{ + domainMap: domainMap, + suffixList: options.DomainSuffix, + keywordList: options.DomainKeyword, + regexList: options.DomainRegex, + } + return rule, nil +} + +func (r *searchGeositeMatcher) Match(domain string) string { + if r.domainMap[domain] { + return "domain=" + domain + } + for _, suffix := range r.suffixList { + if strings.HasSuffix(domain, suffix) { + return "domain_suffix=" + suffix + } + } + for _, keyword := range r.keywordList { + if strings.Contains(domain, keyword) { + return "domain_keyword=" + keyword + } + } + for _, regexStr := range r.regexList { + regex, err := regexp.Compile(regexStr) + if err != nil { + continue + } + if regex.MatchString(domain) { + return "domain_regex=" + regexStr + } + } + return "" +} diff --git a/cmd/sing-box/cmd_merge.go b/cmd/sing-box/cmd_merge.go index 0aff750182..4fb07b8688 100644 --- a/cmd/sing-box/cmd_merge.go +++ b/cmd/sing-box/cmd_merge.go @@ -18,7 +18,7 @@ import ( ) var commandMerge = &cobra.Command{ - Use: "merge [output]", + Use: "merge ", Short: "Merge configurations", Run: func(cmd *cobra.Command, args []string) { err := merge(args[0]) diff --git a/cmd/sing-box/cmd_rule_set.go b/cmd/sing-box/cmd_rule_set.go new file mode 100644 index 0000000000..f4112a087b --- /dev/null +++ b/cmd/sing-box/cmd_rule_set.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +var commandRuleSet = &cobra.Command{ + Use: "rule-set", + Short: "Manage rule sets", +} + +func init() { + mainCommand.AddCommand(commandRuleSet) +} diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go new file mode 100644 index 0000000000..de318095ac --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -0,0 +1,80 @@ +package main + +import ( + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "github.com/spf13/cobra" +) + +var flagRuleSetCompileOutput string + +const flagRuleSetCompileDefaultOutput = ".srs" + +var commandRuleSetCompile = &cobra.Command{ + Use: "compile [source-path]", + Short: "Compile rule-set json to binary", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := compileRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSet.AddCommand(commandRuleSetCompile) + commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file") +} + +func compileRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + decoder := json.NewDecoder(json.NewCommentFilter(reader)) + decoder.DisallowUnknownFields() + var plainRuleSet option.PlainRuleSetCompat + err = decoder.Decode(&plainRuleSet) + if err != nil { + return err + } + ruleSet := plainRuleSet.Upgrade() + var outputPath string + if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { + if strings.HasSuffix(sourcePath, ".json") { + outputPath = sourcePath[:len(sourcePath)-5] + ".srs" + } else { + outputPath = sourcePath + ".srs" + } + } else { + outputPath = flagRuleSetCompileOutput + } + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + err = srs.Write(outputFile, ruleSet) + if err != nil { + outputFile.Close() + os.Remove(outputPath) + return err + } + outputFile.Close() + return nil +} diff --git a/cmd/sing-box/cmd_rule_set_format.go b/cmd/sing-box/cmd_rule_set_format.go new file mode 100644 index 0000000000..dc3ee6aabd --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_format.go @@ -0,0 +1,87 @@ +package main + +import ( + "bytes" + "io" + "os" + "path/filepath" + + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandRuleSetFormatFlagWrite bool + +var commandRuleSetFormat = &cobra.Command{ + Use: "format ", + Short: "Format rule-set json", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := formatRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") + commandRuleSet.AddCommand(commandRuleSetFormat) +} + +func formatRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + content, err := io.ReadAll(reader) + if err != nil { + return err + } + decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content))) + decoder.DisallowUnknownFields() + var plainRuleSet option.PlainRuleSetCompat + err = decoder.Decode(&plainRuleSet) + if err != nil { + return err + } + ruleSet := plainRuleSet.Upgrade() + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(ruleSet) + if err != nil { + return E.Cause(err, "encode config") + } + outputPath, _ := filepath.Abs(sourcePath) + if !commandRuleSetFormatFlagWrite || sourcePath == "stdin" { + os.Stdout.WriteString(buffer.String() + "\n") + return nil + } + if bytes.Equal(content, buffer.Bytes()) { + return nil + } + output, err := os.Create(sourcePath) + if err != nil { + return E.Cause(err, "open output") + } + _, err = output.Write(buffer.Bytes()) + output.Close() + if err != nil { + return E.Cause(err, "write output") + } + os.Stderr.WriteString(outputPath + "\n") + return nil +} diff --git a/cmd/sing-box/cmd_tools.go b/cmd/sing-box/cmd_tools.go index 460a50cd1c..c45f585576 100644 --- a/cmd/sing-box/cmd_tools.go +++ b/cmd/sing-box/cmd_tools.go @@ -38,11 +38,7 @@ func createPreStartedClient() (*box.Box, error) { func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) { if outboundTag == "" { - outbound := instance.Router().DefaultOutbound(N.NetworkName(network)) - if outbound == nil { - return nil, E.New("missing default outbound") - } - return outbound, nil + return instance.Router().DefaultOutbound(N.NetworkName(network)) } else { outbound, loaded := instance.Router().Outbound(outboundTag) if !loaded { diff --git a/cmd/sing-box/cmd_tools_connect.go b/cmd/sing-box/cmd_tools_connect.go index b904ebc9f5..3ea04bcd40 100644 --- a/cmd/sing-box/cmd_tools_connect.go +++ b/cmd/sing-box/cmd_tools_connect.go @@ -18,7 +18,7 @@ import ( var commandConnectFlagNetwork string var commandConnect = &cobra.Command{ - Use: "connect [address]", + Use: "connect
", Short: "Connect to an address", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { diff --git a/common/dialer/router.go b/common/dialer/router.go index 1d5586546e..2531607753 100644 --- a/common/dialer/router.go +++ b/common/dialer/router.go @@ -18,11 +18,19 @@ func NewRouter(router adapter.Router) N.Dialer { } func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - return d.router.DefaultOutbound(network).DialContext(ctx, network, destination) + dialer, err := d.router.DefaultOutbound(network) + if err != nil { + return nil, err + } + return dialer.DialContext(ctx, network, destination) } func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return d.router.DefaultOutbound(N.NetworkUDP).ListenPacket(ctx, destination) + dialer, err := d.router.DefaultOutbound(N.NetworkUDP) + if err != nil { + return nil, err + } + return dialer.ListenPacket(ctx, destination) } func (d *RouterDialer) Upstream() any { diff --git a/common/srs/binary.go b/common/srs/binary.go new file mode 100644 index 0000000000..dd994c2c29 --- /dev/null +++ b/common/srs/binary.go @@ -0,0 +1,485 @@ +package srs + +import ( + "compress/zlib" + "encoding/binary" + "io" + "net/netip" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/rw" + + "go4.org/netipx" +) + +var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS + +const ( + ruleItemQueryType uint8 = iota + ruleItemNetwork + ruleItemDomain + ruleItemDomainKeyword + ruleItemDomainRegex + ruleItemSourceIPCIDR + ruleItemIPCIDR + ruleItemSourcePort + ruleItemSourcePortRange + ruleItemPort + ruleItemPortRange + ruleItemProcessName + ruleItemProcessPath + ruleItemPackageName + ruleItemWIFISSID + ruleItemWIFIBSSID + ruleItemFinal uint8 = 0xFF +) + +func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) { + var magicBytes [3]byte + _, err = io.ReadFull(reader, magicBytes[:]) + if err != nil { + return + } + if magicBytes != MagicBytes { + err = E.New("invalid sing-box rule set file") + return + } + var version uint8 + err = binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return ruleSet, err + } + if version != 1 { + return ruleSet, E.New("unsupported version: ", version) + } + zReader, err := zlib.NewReader(reader) + if err != nil { + return + } + length, err := rw.ReadUVariant(zReader) + if err != nil { + return + } + ruleSet.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + ruleSet.Rules[i], err = readRule(zReader, recovery) + if err != nil { + err = E.Cause(err, "read rule[", i, "]") + return + } + } + return +} + +func Write(writer io.Writer, ruleSet option.PlainRuleSet) error { + _, err := writer.Write(MagicBytes[:]) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) + if err != nil { + return err + } + err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules))) + if err != nil { + return err + } + for _, rule := range ruleSet.Rules { + err = writeRule(zWriter, rule) + if err != nil { + return err + } + } + return zWriter.Close() +} + +func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) { + var ruleType uint8 + err = binary.Read(reader, binary.BigEndian, &ruleType) + if err != nil { + return + } + switch ruleType { + case 0: + rule.DefaultOptions, err = readDefaultRule(reader, recovery) + case 1: + rule.LogicalOptions, err = readLogicalRule(reader, recovery) + default: + err = E.New("unknown rule type: ", ruleType) + } + return +} + +func writeRule(writer io.Writer, rule option.HeadlessRule) error { + switch rule.Type { + case C.RuleTypeDefault: + return writeDefaultRule(writer, rule.DefaultOptions) + case C.RuleTypeLogical: + return writeLogicalRule(writer, rule.LogicalOptions) + default: + panic("unknown rule type: " + rule.Type) + } +} + +func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) { + var lastItemType uint8 + for { + var itemType uint8 + err = binary.Read(reader, binary.BigEndian, &itemType) + if err != nil { + return + } + switch itemType { + case ruleItemQueryType: + var rawQueryType []uint16 + rawQueryType, err = readRuleItemUint16(reader) + if err != nil { + return + } + rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType { + return option.DNSQueryType(it) + }) + case ruleItemNetwork: + rule.Network, err = readRuleItemString(reader) + case ruleItemDomain: + var matcher *domain.Matcher + matcher, err = domain.ReadMatcher(reader) + if err != nil { + return + } + rule.DomainMatcher = matcher + case ruleItemDomainKeyword: + rule.DomainKeyword, err = readRuleItemString(reader) + case ruleItemDomainRegex: + rule.DomainRegex, err = readRuleItemString(reader) + case ruleItemSourceIPCIDR: + rule.SourceIPSet, err = readIPSet(reader) + if err != nil { + return + } + if recovery { + rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemIPCIDR: + rule.IPSet, err = readIPSet(reader) + if err != nil { + return + } + if recovery { + rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemSourcePort: + rule.SourcePort, err = readRuleItemUint16(reader) + case ruleItemSourcePortRange: + rule.SourcePortRange, err = readRuleItemString(reader) + case ruleItemPort: + rule.Port, err = readRuleItemUint16(reader) + case ruleItemPortRange: + rule.PortRange, err = readRuleItemString(reader) + case ruleItemProcessName: + rule.ProcessName, err = readRuleItemString(reader) + case ruleItemProcessPath: + rule.ProcessPath, err = readRuleItemString(reader) + case ruleItemPackageName: + rule.PackageName, err = readRuleItemString(reader) + case ruleItemWIFISSID: + rule.WIFISSID, err = readRuleItemString(reader) + case ruleItemWIFIBSSID: + rule.WIFIBSSID, err = readRuleItemString(reader) + case ruleItemFinal: + err = binary.Read(reader, binary.BigEndian, &rule.Invert) + return + default: + err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType) + } + if err != nil { + return + } + lastItemType = itemType + } +} + +func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error { + err := binary.Write(writer, binary.BigEndian, uint8(0)) + if err != nil { + return err + } + if len(rule.QueryType) > 0 { + err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 { + return uint16(it) + })) + if err != nil { + return err + } + } + if len(rule.Network) > 0 { + err = writeRuleItemString(writer, ruleItemNetwork, rule.Network) + if err != nil { + return err + } + } + if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 { + err = binary.Write(writer, binary.BigEndian, ruleItemDomain) + if err != nil { + return err + } + err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer) + if err != nil { + return err + } + } + if len(rule.DomainKeyword) > 0 { + err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword) + if err != nil { + return err + } + } + if len(rule.DomainRegex) > 0 { + err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex) + if err != nil { + return err + } + } + if len(rule.SourceIPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR) + if err != nil { + return E.Cause(err, "source_ipcidr") + } + } + if len(rule.IPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR) + if err != nil { + return E.Cause(err, "ipcidr") + } + } + if len(rule.SourcePort) > 0 { + err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort) + if err != nil { + return err + } + } + if len(rule.SourcePortRange) > 0 { + err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange) + if err != nil { + return err + } + } + if len(rule.Port) > 0 { + err = writeRuleItemUint16(writer, ruleItemPort, rule.Port) + if err != nil { + return err + } + } + if len(rule.PortRange) > 0 { + err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange) + if err != nil { + return err + } + } + if len(rule.ProcessName) > 0 { + err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName) + if err != nil { + return err + } + } + if len(rule.ProcessPath) > 0 { + err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath) + if err != nil { + return err + } + } + if len(rule.PackageName) > 0 { + err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName) + if err != nil { + return err + } + } + if len(rule.WIFISSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) + if err != nil { + return err + } + } + if len(rule.WIFIBSSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, ruleItemFinal) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, rule.Invert) + if err != nil { + return err + } + return nil +} + +func readRuleItemString(reader io.Reader) ([]string, error) { + length, err := rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + value := make([]string, length) + for i := uint64(0); i < length; i++ { + value[i], err = rw.ReadVString(reader) + if err != nil { + return nil, err + } + } + return value, nil +} + +func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error { + err := binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(value))) + if err != nil { + return err + } + for _, item := range value { + err = rw.WriteVString(writer, item) + if err != nil { + return err + } + } + return nil +} + +func readRuleItemUint16(reader io.Reader) ([]uint16, error) { + length, err := rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + value := make([]uint16, length) + for i := uint64(0); i < length; i++ { + err = binary.Read(reader, binary.BigEndian, &value[i]) + if err != nil { + return nil, err + } + } + return value, nil +} + +func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error { + err := binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(value))) + if err != nil { + return err + } + for _, item := range value { + err = binary.Write(writer, binary.BigEndian, item) + if err != nil { + return err + } + } + return nil +} + +func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error { + var builder netipx.IPSetBuilder + for i, prefixString := range value { + prefix, err := netip.ParsePrefix(prefixString) + if err == nil { + builder.AddPrefix(prefix) + continue + } + addr, addrErr := netip.ParseAddr(prefixString) + if addrErr == nil { + builder.Add(addr) + continue + } + return E.Cause(err, "parse [", i, "]") + } + ipSet, err := builder.IPSet() + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + return writeIPSet(writer, ipSet) +} + +func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { + var mode uint8 + err = binary.Read(reader, binary.BigEndian, &mode) + if err != nil { + return + } + switch mode { + case 0: + logicalRule.Mode = C.LogicalTypeAnd + case 1: + logicalRule.Mode = C.LogicalTypeOr + default: + err = E.New("unknown logical mode: ", mode) + return + } + length, err := rw.ReadUVariant(reader) + if err != nil { + return + } + logicalRule.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + logicalRule.Rules[i], err = readRule(reader, recovery) + if err != nil { + err = E.Cause(err, "read logical rule [", i, "]") + return + } + } + err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert) + if err != nil { + return + } + return +} + +func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + switch logicalRule.Mode { + case C.LogicalTypeAnd: + err = binary.Write(writer, binary.BigEndian, uint8(0)) + case C.LogicalTypeOr: + err = binary.Write(writer, binary.BigEndian, uint8(1)) + default: + panic("unknown logical mode: " + logicalRule.Mode) + } + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules))) + if err != nil { + return err + } + for _, rule := range logicalRule.Rules { + err = writeRule(writer, rule) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, logicalRule.Invert) + if err != nil { + return err + } + return nil +} diff --git a/common/srs/ip_set.go b/common/srs/ip_set.go new file mode 100644 index 0000000000..b346da26f6 --- /dev/null +++ b/common/srs/ip_set.go @@ -0,0 +1,116 @@ +package srs + +import ( + "encoding/binary" + "io" + "net/netip" + "unsafe" + + "github.com/sagernet/sing/common/rw" + + "go4.org/netipx" +) + +type myIPSet struct { + rr []myIPRange +} + +type myIPRange struct { + from netip.Addr + to netip.Addr +} + +func readIPSet(reader io.Reader) (*netipx.IPSet, error) { + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return nil, err + } + var length uint64 + err = binary.Read(reader, binary.BigEndian, &length) + if err != nil { + return nil, err + } + mySet := &myIPSet{ + rr: make([]myIPRange, length), + } + for i := uint64(0); i < length; i++ { + var ( + fromLen uint64 + toLen uint64 + fromAddr netip.Addr + toAddr netip.Addr + ) + fromLen, err = rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + fromBytes := make([]byte, fromLen) + _, err = io.ReadFull(reader, fromBytes) + if err != nil { + return nil, err + } + err = fromAddr.UnmarshalBinary(fromBytes) + if err != nil { + return nil, err + } + toLen, err = rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + toBytes := make([]byte, toLen) + _, err = io.ReadFull(reader, toBytes) + if err != nil { + return nil, err + } + err = toAddr.UnmarshalBinary(toBytes) + if err != nil { + return nil, err + } + mySet.rr[i] = myIPRange{fromAddr, toAddr} + } + return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil +} + +func writeIPSet(writer io.Writer, set *netipx.IPSet) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + mySet := (*myIPSet)(unsafe.Pointer(set)) + err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr))) + if err != nil { + return err + } + for _, rr := range mySet.rr { + var ( + fromBinary []byte + toBinary []byte + ) + fromBinary, err = rr.from.MarshalBinary() + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(fromBinary))) + if err != nil { + return err + } + _, err = writer.Write(fromBinary) + if err != nil { + return err + } + toBinary, err = rr.to.MarshalBinary() + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(toBinary))) + if err != nil { + return err + } + _, err = writer.Write(toBinary) + if err != nil { + return err + } + } + return nil +} diff --git a/constant/rule.go b/constant/rule.go index 3c741995f8..5a8eaf127f 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -9,3 +9,11 @@ const ( LogicalTypeAnd = "and" LogicalTypeOr = "or" ) + +const ( + RuleSetTypeLocal = "local" + RuleSetTypeRemote = "remote" + RuleSetVersion1 = 1 + RuleSetFormatSource = "source" + RuleSetFormatBinary = "binary" +) diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index 2616ded5c1..001390d391 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -22,11 +22,13 @@ var ( bucketSelected = []byte("selected") bucketExpand = []byte("group_expand") bucketMode = []byte("clash_mode") + bucketRuleSet = []byte("rule_set") bucketNameList = []string{ string(bucketSelected), string(bucketExpand), string(bucketMode), + string(bucketRuleSet), } cacheIDDefault = []byte("default") @@ -257,3 +259,36 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { } }) } + +func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedRuleSet { + var savedSet adapter.SavedRuleSet + err := c.DB.View(func(t *bbolt.Tx) error { + bucket := c.bucket(t, bucketRuleSet) + if bucket == nil { + return os.ErrNotExist + } + setBinary := bucket.Get([]byte(tag)) + if len(setBinary) == 0 { + return os.ErrInvalid + } + return savedSet.UnmarshalBinary(setBinary) + }) + if err != nil { + return nil + } + return &savedSet +} + +func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedRuleSet) error { + return c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := c.createBucket(t, bucketRuleSet) + if err != nil { + return err + } + setBinary, err := set.MarshalBinary() + if err != nil { + return err + } + return bucket.Put([]byte(tag), setBinary) + }) +} diff --git a/experimental/cachefile/fakeip.go b/experimental/cachefile/fakeip.go index 2242342a36..e998ebb859 100644 --- a/experimental/cachefile/fakeip.go +++ b/experimental/cachefile/fakeip.go @@ -25,7 +25,7 @@ func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata { err := c.DB.Batch(func(tx *bbolt.Tx) error { bucket := tx.Bucket(bucketFakeIP) if bucket == nil { - return nil + return os.ErrNotExist } metadataBinary := bucket.Get(keyMetadata) if len(metadataBinary) == 0 { diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go index 050efd8d17..cf96931a85 100644 --- a/experimental/clashapi/proxies.go +++ b/experimental/clashapi/proxies.go @@ -100,8 +100,10 @@ func getProxies(server *Server, router adapter.Router) func(w http.ResponseWrite allProxies = append(allProxies, detour.Tag()) } - defaultTag := router.DefaultOutbound(N.NetworkTCP).Tag() - if defaultTag == "" { + var defaultTag string + if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + defaultTag = defaultOutbound.Tag() + } else { defaultTag = allProxies[0] } diff --git a/experimental/clashapi/server_resources.go b/experimental/clashapi/server_resources.go index ad36641e05..d6d22b5390 100644 --- a/experimental/clashapi/server_resources.go +++ b/experimental/clashapi/server_resources.go @@ -51,7 +51,11 @@ func (s *Server) downloadExternalUI() error { } detour = outbound } else { - detour = s.router.DefaultOutbound(N.NetworkTCP) + outbound, err := s.router.DefaultOutbound(N.NetworkTCP) + if err != nil { + return err + } + detour = outbound } httpClient := &http.Client{ Transport: &http.Transport{ diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go index 3dc5a367e7..b7c20eb075 100644 --- a/experimental/clashapi/trafficontrol/tracker.go +++ b/experimental/clashapi/trafficontrol/tracker.go @@ -94,7 +94,9 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad var chain []string var next string if rule == nil { - next = router.DefaultOutbound(N.NetworkTCP).Tag() + if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + next = defaultOutbound.Tag() + } } else { next = rule.Outbound() } @@ -181,7 +183,9 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route var chain []string var next string if rule == nil { - next = router.DefaultOutbound(N.NetworkUDP).Tag() + if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil { + next = defaultOutbound.Tag() + } } else { next = rule.Outbound() } diff --git a/go.mod b/go.mod index 7efe241ef6..095326d7f9 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 github.com/sagernet/quic-go v0.40.0 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc + github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be github.com/sagernet/sing-dns v0.1.11 github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 github.com/sagernet/sing-quic v0.1.5-0.20231123150216-00957d136203 diff --git a/go.sum b/go.sum index 4db37c0bfe..6d8d994a8c 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= -github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc h1:vESVuxHgbd2EzHxd+TYTpNACIEGBOhp5n3KG7bgbcws= -github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= +github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be h1:FigAM9kq7RRXmHvgn8w2a8tqCY5CMV5GIk0id84dI0o= +github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE= github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE= github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM= diff --git a/option/experimental.go b/option/experimental.go index 72751a590e..c685f51f54 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -23,16 +23,16 @@ type ClashAPIOptions struct { DefaultMode string `json:"default_mode,omitempty"` ModeList []string `json:"-"` + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` // Deprecated: migrated to global cache file StoreMode bool `json:"store_mode,omitempty"` // Deprecated: migrated to global cache file StoreSelected bool `json:"store_selected,omitempty"` // Deprecated: migrated to global cache file StoreFakeIP bool `json:"store_fakeip,omitempty"` - // Deprecated: migrated to global cache file - CacheFile string `json:"cache_file,omitempty"` - // Deprecated: migrated to global cache file - CacheID string `json:"cache_id,omitempty"` } type V2RayAPIOptions struct { diff --git a/option/route.go b/option/route.go index 43150576e2..e313fcf242 100644 --- a/option/route.go +++ b/option/route.go @@ -4,6 +4,7 @@ type RouteOptions struct { GeoIP *GeoIPOptions `json:"geoip,omitempty"` Geosite *GeositeOptions `json:"geosite,omitempty"` Rules []Rule `json:"rules,omitempty"` + RuleSet []RuleSet `json:"rule_set,omitempty"` Final string `json:"final,omitempty"` FindProcess bool `json:"find_process,omitempty"` AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` diff --git a/option/rule.go b/option/rule.go index 4f4042025f..bad605a0fc 100644 --- a/option/rule.go +++ b/option/rule.go @@ -65,34 +65,36 @@ func (r Rule) IsValid() bool { } type DefaultRule struct { - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - GeoIP Listable[string] `json:"geoip,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - IPCIDR Listable[string] `json:"ip_cidr,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + Network Listable[string] `json:"network,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + Protocol Listable[string] `json:"protocol,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + Geosite Listable[string] `json:"geosite,omitempty"` + SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` + GeoIP Listable[string] `json:"geoip,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + User Listable[string] `json:"user,omitempty"` + UserID Listable[int32] `json:"user_id,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` } func (r DefaultRule) IsValid() bool { diff --git a/option/rule_dns.go b/option/rule_dns.go index fca3432293..c02d09f761 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -91,6 +91,7 @@ type DefaultDNSRule struct { ClashMode string `json:"clash_mode,omitempty"` WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` Invert bool `json:"invert,omitempty"` Server string `json:"server,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` diff --git a/option/rule_set.go b/option/rule_set.go new file mode 100644 index 0000000000..7a6f7e9282 --- /dev/null +++ b/option/rule_set.go @@ -0,0 +1,230 @@ +package option + +import ( + "reflect" + + "github.com/sagernet/sing-box/common/json" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + + "go4.org/netipx" +) + +type _RuleSet struct { + Type string `json:"type"` + Tag string `json:"tag"` + Format string `json:"format"` + LocalOptions LocalRuleSet `json:"-"` + RemoteOptions RemoteRuleSet `json:"-"` +} + +type RuleSet _RuleSet + +func (r RuleSet) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleSetTypeLocal: + v = r.LocalOptions + case C.RuleSetTypeRemote: + v = r.RemoteOptions + default: + return nil, E.New("unknown rule set type: " + r.Type) + } + return MarshallObjects((_RuleSet)(r), v) +} + +func (r *RuleSet) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_RuleSet)(r)) + if err != nil { + return err + } + if r.Tag == "" { + return E.New("missing rule_set.[].tag") + } + switch r.Format { + case "": + return E.New("missing rule_set.[].format") + case C.RuleSetFormatSource, C.RuleSetFormatBinary: + default: + return E.New("unknown rule set format: " + r.Format) + } + var v any + switch r.Type { + case C.RuleSetTypeLocal: + v = &r.LocalOptions + case C.RuleSetTypeRemote: + v = &r.RemoteOptions + case "": + return E.New("missing rule_set.[].type") + default: + return E.New("unknown rule set type: " + r.Type) + } + err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v) + if err != nil { + return E.Cause(err, "rule set") + } + return nil +} + +type LocalRuleSet struct { + Path string `json:"path,omitempty"` +} + +type RemoteRuleSet struct { + URL string `json:"url"` + DownloadDetour string `json:"download_detour,omitempty"` + UpdateInterval Duration `json:"update_interval,omitempty"` +} + +type _HeadlessRule struct { + Type string `json:"type,omitempty"` + DefaultOptions DefaultHeadlessRule `json:"-"` + LogicalOptions LogicalHeadlessRule `json:"-"` +} + +type HeadlessRule _HeadlessRule + +func (r HeadlessRule) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleTypeDefault: + r.Type = "" + v = r.DefaultOptions + case C.RuleTypeLogical: + v = r.LogicalOptions + default: + return nil, E.New("unknown rule type: " + r.Type) + } + return MarshallObjects((_HeadlessRule)(r), v) +} + +func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_HeadlessRule)(r)) + if err != nil { + return err + } + var v any + switch r.Type { + case "", C.RuleTypeDefault: + r.Type = C.RuleTypeDefault + v = &r.DefaultOptions + case C.RuleTypeLogical: + v = &r.LogicalOptions + default: + return E.New("unknown rule type: " + r.Type) + } + err = UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v) + if err != nil { + return E.Cause(err, "route rule-set rule") + } + return nil +} + +func (r HeadlessRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault, "": + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + +type DefaultHeadlessRule struct { + QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` + Network Listable[string] `json:"network,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + Invert bool `json:"invert,omitempty"` + + DomainMatcher *domain.Matcher `json:"-"` + SourceIPSet *netipx.IPSet `json:"-"` + IPSet *netipx.IPSet `json:"-"` +} + +func (r DefaultHeadlessRule) IsValid() bool { + var defaultValue DefaultHeadlessRule + defaultValue.Invert = r.Invert + return !reflect.DeepEqual(r, defaultValue) +} + +type LogicalHeadlessRule struct { + Mode string `json:"mode"` + Rules []HeadlessRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` +} + +func (r LogicalHeadlessRule) IsValid() bool { + return len(r.Rules) > 0 && common.All(r.Rules, HeadlessRule.IsValid) +} + +type _PlainRuleSetCompat struct { + Version int `json:"version"` + Options PlainRuleSet `json:"-"` +} + +type PlainRuleSetCompat _PlainRuleSetCompat + +func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { + var v any + switch r.Version { + case C.RuleSetVersion1: + v = r.Options + default: + return nil, E.New("unknown rule set version: ", r.Version) + } + return MarshallObjects((_PlainRuleSetCompat)(r), v) +} + +func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_PlainRuleSetCompat)(r)) + if err != nil { + return err + } + var v any + switch r.Version { + case C.RuleSetVersion1: + v = &r.Options + case 0: + return E.New("missing rule set version") + default: + return E.New("unknown rule set version: ", r.Version) + } + err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) + if err != nil { + return E.Cause(err, "rule set") + } + return nil +} + +func (r PlainRuleSetCompat) Upgrade() PlainRuleSet { + var result PlainRuleSet + switch r.Version { + case C.RuleSetVersion1: + result = r.Options + default: + panic("unknown rule set version: " + F.ToString(r.Version)) + } + return result +} + +type PlainRuleSet struct { + Rules []HeadlessRule `json:"rules,omitempty"` +} diff --git a/option/types.go b/option/types.go index f2fed66309..520c3503aa 100644 --- a/option/types.go +++ b/option/types.go @@ -174,6 +174,14 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error { type DNSQueryType uint16 +func (t DNSQueryType) String() string { + typeName, loaded := mDNS.TypeToString[uint16(t)] + if loaded { + return typeName + } + return F.ToString(uint16(t)) +} + func (t DNSQueryType) MarshalJSON() ([]byte, error) { typeName, loaded := mDNS.TypeToString[uint16(t)] if loaded { diff --git a/route/router.go b/route/router.go index b48d2346d9..967061952e 100644 --- a/route/router.go +++ b/route/router.go @@ -67,6 +67,8 @@ type Router struct { dnsClient *dns.Client defaultDomainStrategy dns.DomainStrategy dnsRules []adapter.DNSRule + ruleSets []adapter.RuleSet + ruleSetMap map[string]adapter.RuleSet defaultTransport dns.Transport transports []dns.Transport transportMap map[string]dns.Transport @@ -106,6 +108,7 @@ func NewRouter( outboundByTag: make(map[string]adapter.Outbound), rules: make([]adapter.Rule, 0, len(options.Rules)), dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)), + ruleSetMap: make(map[string]adapter.RuleSet), needGeoIPDatabase: hasRule(options.Rules, isGeoIPRule) || hasDNSRule(dnsOptions.Rules, isGeoIPDNSRule), needGeositeDatabase: hasRule(options.Rules, isGeositeRule) || hasDNSRule(dnsOptions.Rules, isGeositeDNSRule), geoIPOptions: common.PtrValueOrDefault(options.GeoIP), @@ -140,6 +143,14 @@ func NewRouter( } router.dnsRules = append(router.dnsRules, dnsRule) } + for i, ruleSetOptions := range options.RuleSet { + ruleSet, err := NewRuleSet(ctx, router, router.logger, ruleSetOptions) + if err != nil { + return nil, E.Cause(err, "parse rule-set[", i, "]") + } + router.ruleSets = append(router.ruleSets, ruleSet) + router.ruleSetMap[ruleSetOptions.Tag] = ruleSet + } transports := make([]dns.Transport, len(dnsOptions.Servers)) dummyTransportMap := make(map[string]dns.Transport) @@ -479,6 +490,12 @@ func (r *Router) Start() error { if r.needWIFIState { r.updateWIFIState() } + for i, ruleSet := range r.ruleSets { + err := ruleSet.Start() + if err != nil { + return E.Cause(err, "initialize rule-set[", i, "]") + } + } for i, rule := range r.rules { err := rule.Start() if err != nil { @@ -576,11 +593,17 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { return outbound, loaded } -func (r *Router) DefaultOutbound(network string) adapter.Outbound { +func (r *Router) DefaultOutbound(network string) (adapter.Outbound, error) { if network == N.NetworkTCP { - return r.defaultOutboundForConnection + if r.defaultOutboundForConnection == nil { + return nil, E.New("missing default outbound for TCP connections") + } + return r.defaultOutboundForConnection, nil } else { - return r.defaultOutboundForPacketConnection + if r.defaultOutboundForPacketConnection == nil { + return nil, E.New("missing default outbound for UDP connections") + } + return r.defaultOutboundForPacketConnection, nil } } @@ -588,6 +611,11 @@ func (r *Router) FakeIPStore() adapter.FakeIPStore { return r.fakeIPStore } +func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) { + ruleSet, loaded := r.ruleSetMap[tag] + return ruleSet, loaded +} + func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { diff --git a/route/rule_abstract.go b/route/rule_abstract.go index 38d4d57d41..312caaee73 100644 --- a/route/rule_abstract.go +++ b/route/rule_abstract.go @@ -1,6 +1,7 @@ package route import ( + "io" "strings" "github.com/sagernet/sing-box/adapter" @@ -135,7 +136,7 @@ func (r *abstractDefaultRule) String() string { } type abstractLogicalRule struct { - rules []adapter.Rule + rules []adapter.HeadlessRule mode string invert bool outbound string @@ -146,7 +147,10 @@ func (r *abstractLogicalRule) Type() string { } func (r *abstractLogicalRule) UpdateGeosite() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (adapter.Rule, bool) { + rule, loaded := it.(adapter.Rule) + return rule, loaded + }) { err := rule.UpdateGeosite() if err != nil { return err @@ -156,7 +160,10 @@ func (r *abstractLogicalRule) UpdateGeosite() error { } func (r *abstractLogicalRule) Start() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (common.Starter, bool) { + rule, loaded := it.(common.Starter) + return rule, loaded + }) { err := rule.Start() if err != nil { return err @@ -166,7 +173,10 @@ func (r *abstractLogicalRule) Start() error { } func (r *abstractLogicalRule) Close() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (io.Closer, bool) { + rule, loaded := it.(io.Closer) + return rule, loaded + }) { err := rule.Close() if err != nil { return err @@ -177,11 +187,11 @@ func (r *abstractLogicalRule) Close() error { func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { if r.mode == C.LogicalTypeAnd { - return common.All(r.rules, func(it adapter.Rule) bool { + return common.All(r.rules, func(it adapter.HeadlessRule) bool { return it.Match(metadata) }) != r.invert } else { - return common.Any(r.rules, func(it adapter.Rule) bool { + return common.Any(r.rules, func(it adapter.HeadlessRule) bool { return it.Match(metadata) }) != r.invert } diff --git a/route/rule_default.go b/route/rule_default.go index 8c8473abf5..c0ef9eef60 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -194,6 +194,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.RuleSet) > 0 { + item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } @@ -206,7 +211,7 @@ type LogicalRule struct { func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { r := &LogicalRule{ abstractLogicalRule{ - rules: make([]adapter.Rule, len(options.Rules)), + rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, outbound: options.Outbound, }, diff --git a/route/rule_dns.go b/route/rule_dns.go index b444932582..f5f9fd3583 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -190,6 +190,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.RuleSet) > 0 { + item := NewRuleSetItem(router, options.RuleSet, false) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } @@ -212,7 +217,7 @@ type LogicalDNSRule struct { func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { r := &LogicalDNSRule{ abstractLogicalRule: abstractLogicalRule{ - rules: make([]adapter.Rule, len(options.Rules)), + rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, outbound: options.Server, }, diff --git a/route/rule_headless.go b/route/rule_headless.go new file mode 100644 index 0000000000..9df2ee3036 --- /dev/null +++ b/route/rule_headless.go @@ -0,0 +1,173 @@ +package route + +import ( + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func NewHeadlessRule(router adapter.Router, options option.HeadlessRule) (adapter.HeadlessRule, error) { + switch options.Type { + case "", C.RuleTypeDefault: + if !options.DefaultOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewDefaultHeadlessRule(router, options.DefaultOptions) + case C.RuleTypeLogical: + if !options.LogicalOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewLogicalHeadlessRule(router, options.LogicalOptions) + default: + return nil, E.New("unknown rule type: ", options.Type) + } +} + +var _ adapter.HeadlessRule = (*DefaultHeadlessRule)(nil) + +type DefaultHeadlessRule struct { + abstractDefaultRule +} + +func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) { + rule := &DefaultHeadlessRule{ + abstractDefaultRule{ + invert: options.Invert, + }, + } + if len(options.Network) > 0 { + item := NewNetworkItem(options.Network) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 { + item := NewDomainItem(options.Domain, options.DomainSuffix) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.DomainMatcher != nil { + item := NewRawDomainItem(options.DomainMatcher) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainKeyword) > 0 { + item := NewDomainKeywordItem(options.DomainKeyword) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainRegex) > 0 { + item, err := NewDomainRegexItem(options.DomainRegex) + if err != nil { + return nil, E.Cause(err, "domain_regex") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourceIPCIDR) > 0 { + item, err := NewIPCIDRItem(true, options.SourceIPCIDR) + if err != nil { + return nil, E.Cause(err, "source_ipcidr") + } + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.SourceIPSet != nil { + item := NewRawIPCIDRItem(true, options.SourceIPSet) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.IPCIDR) > 0 { + item, err := NewIPCIDRItem(false, options.IPCIDR) + if err != nil { + return nil, E.Cause(err, "ipcidr") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.IPSet != nil { + item := NewRawIPCIDRItem(false, options.IPSet) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePort) > 0 { + item := NewPortItem(true, options.SourcePort) + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePortRange) > 0 { + item, err := NewPortRangeItem(true, options.SourcePortRange) + if err != nil { + return nil, E.Cause(err, "source_port_range") + } + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Port) > 0 { + item := NewPortItem(false, options.Port) + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PortRange) > 0 { + item, err := NewPortRangeItem(false, options.PortRange) + if err != nil { + return nil, E.Cause(err, "port_range") + } + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessName) > 0 { + item := NewProcessItem(options.ProcessName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessPath) > 0 { + item := NewProcessPathItem(options.ProcessPath) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PackageName) > 0 { + item := NewPackageNameItem(options.PackageName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(router, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFIBSSID) > 0 { + item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + return rule, nil +} + +var _ adapter.HeadlessRule = (*LogicalHeadlessRule)(nil) + +type LogicalHeadlessRule struct { + abstractLogicalRule +} + +func NewLogicalHeadlessRule(router adapter.Router, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) { + r := &LogicalHeadlessRule{ + abstractLogicalRule{ + rules: make([]adapter.HeadlessRule, len(options.Rules)), + invert: options.Invert, + }, + } + switch options.Mode { + case C.LogicalTypeAnd: + r.mode = C.LogicalTypeAnd + case C.LogicalTypeOr: + r.mode = C.LogicalTypeOr + default: + return nil, E.New("unknown logical mode: ", options.Mode) + } + for i, subRule := range options.Rules { + rule, err := NewHeadlessRule(router, subRule) + if err != nil { + return nil, E.Cause(err, "sub rule[", i, "]") + } + r.rules[i] = rule + } + return r, nil +} diff --git a/route/rule_item_cidr.go b/route/rule_item_cidr.go index b72d1e10b1..e17d87def8 100644 --- a/route/rule_item_cidr.go +++ b/route/rule_item_cidr.go @@ -31,7 +31,7 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { builder.Add(addr) continue } - return nil, E.Cause(err, "parse ip_cidr [", i, "]") + return nil, E.Cause(err, "parse [", i, "]") } var description string if isSource { @@ -57,8 +57,23 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { }, nil } +func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem { + var description string + if isSource { + description = "source_ipcidr=" + } else { + description = "ipcidr=" + } + description += "" + return &IPCIDRItem{ + ipSet: ipSet, + isSource: isSource, + description: description, + } +} + func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool { - if r.isSource { + if r.isSource || metadata.QueryType != 0 || metadata.IPCIDRMatchSource { return r.ipSet.Contains(metadata.Source.Addr) } else { if metadata.Destination.IsIP() { diff --git a/route/rule_item_domain.go b/route/rule_item_domain.go index 6602441deb..d2a11181b0 100644 --- a/route/rule_item_domain.go +++ b/route/rule_item_domain.go @@ -43,6 +43,13 @@ func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem { } } +func NewRawDomainItem(matcher *domain.Matcher) *DomainItem { + return &DomainItem{ + matcher, + "domain/domain_suffix=", + } +} + func (r *DomainItem) Match(metadata *adapter.InboundContext) bool { var domainHost string if metadata.Domain != "" { diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go new file mode 100644 index 0000000000..959b2f6110 --- /dev/null +++ b/route/rule_item_rule_set.go @@ -0,0 +1,55 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*RuleSetItem)(nil) + +type RuleSetItem struct { + router adapter.Router + tagList []string + setList []adapter.HeadlessRule + ipcidrMatchSource bool +} + +func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool) *RuleSetItem { + return &RuleSetItem{ + router: router, + tagList: tagList, + ipcidrMatchSource: ipCIDRMatchSource, + } +} + +func (r *RuleSetItem) Start() error { + for _, tag := range r.tagList { + ruleSet, loaded := r.router.RuleSet(tag) + if !loaded { + return E.New("rule-set not found: ", tag) + } + r.setList = append(r.setList, ruleSet) + } + return nil +} + +func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { + metadata.IPCIDRMatchSource = r.ipcidrMatchSource + for _, ruleSet := range r.setList { + if ruleSet.Match(metadata) { + return true + } + } + return false +} + +func (r *RuleSetItem) String() string { + if len(r.tagList) == 1 { + return F.ToString("rule_set=", r.tagList[0]) + } else { + return F.ToString("rule_set=[", strings.Join(r.tagList, " "), "]") + } +} diff --git a/route/rule_set.go b/route/rule_set.go new file mode 100644 index 0000000000..76c78c6214 --- /dev/null +++ b/route/rule_set.go @@ -0,0 +1,22 @@ +package route + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + 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" +) + +func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { + switch options.Type { + case C.RuleSetTypeLocal: + return NewLocalRuleSet(router, options) + case C.RuleSetTypeRemote: + return NewRemoteRuleSet(ctx, router, logger, options), nil + default: + return nil, E.New("unknown rule set type: ", options.Type) + } +} diff --git a/route/rule_set_local.go b/route/rule_set_local.go new file mode 100644 index 0000000000..ccdb1704a8 --- /dev/null +++ b/route/rule_set_local.go @@ -0,0 +1,69 @@ +package route + +import ( + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ adapter.RuleSet = (*LocalRuleSet)(nil) + +type LocalRuleSet struct { + rules []adapter.HeadlessRule +} + +func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { + setFile, err := os.Open(options.LocalOptions.Path) + if err != nil { + return nil, err + } + var plainRuleSet option.PlainRuleSet + switch options.Format { + case C.RuleSetFormatSource, "": + var compat option.PlainRuleSetCompat + decoder := json.NewDecoder(json.NewCommentFilter(setFile)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&compat) + if err != nil { + return nil, err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + plainRuleSet, err = srs.Read(setFile, false) + if err != nil { + return nil, err + } + default: + return nil, E.New("unknown rule set format: ", options.Format) + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(router, ruleOptions) + if err != nil { + return nil, E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + return &LocalRuleSet{rules}, nil +} + +func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} + +func (s *LocalRuleSet) Start() error { + return nil +} + +func (s *LocalRuleSet) Close() error { + return nil +} diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go new file mode 100644 index 0000000000..d9102320dd --- /dev/null +++ b/route/rule_set_remote.go @@ -0,0 +1,218 @@ +package route + +import ( + "bytes" + "context" + "io" + "net" + "net/http" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + 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" + "github.com/sagernet/sing/service" +) + +var _ adapter.RuleSet = (*RemoteRuleSet)(nil) + +type RemoteRuleSet struct { + ctx context.Context + cancel context.CancelFunc + router adapter.Router + logger logger.ContextLogger + options option.RuleSet + updateInterval time.Duration + dialer N.Dialer + rules []adapter.HeadlessRule + lastUpdated time.Time + lastEtag string + updateTicker *time.Ticker +} + +func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { + ctx, cancel := context.WithCancel(ctx) + var updateInterval time.Duration + if options.RemoteOptions.UpdateInterval > 0 { + updateInterval = time.Duration(options.RemoteOptions.UpdateInterval) + } else { + updateInterval = 12 * time.Hour + } + return &RemoteRuleSet{ + ctx: ctx, + cancel: cancel, + router: router, + logger: logger, + options: options, + updateInterval: updateInterval, + } +} + +func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} + +func (s *RemoteRuleSet) Start() error { + var dialer N.Dialer + if s.options.RemoteOptions.DownloadDetour != "" { + outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour) + if !loaded { + return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour) + } + dialer = outbound + } else { + outbound, err := s.router.DefaultOutbound(N.NetworkTCP) + if err != nil { + return err + } + dialer = outbound + } + s.dialer = dialer + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + if savedSet := cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil { + err := s.loadBytes(savedSet.Content) + if err != nil { + return E.Cause(err, "restore cached rule-set") + } + s.lastUpdated = savedSet.LastUpdated + s.lastEtag = savedSet.LastEtag + } + } + if s.lastUpdated.IsZero() || time.Since(s.lastUpdated) > s.updateInterval { + err := s.fetchOnce() + if err != nil { + return E.Cause(err, "fetch rule-set ", s.options.Tag) + } + } + s.updateTicker = time.NewTicker(s.updateInterval) + go s.loopUpdate() + return nil +} + +func (s *RemoteRuleSet) loadBytes(content []byte) error { + var ( + plainRuleSet option.PlainRuleSet + err error + ) + switch s.options.Format { + case C.RuleSetFormatSource, "": + var compat option.PlainRuleSetCompat + decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content))) + decoder.DisallowUnknownFields() + err = decoder.Decode(&compat) + if err != nil { + return err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + plainRuleSet, err = srs.Read(bytes.NewReader(content), false) + if err != nil { + return err + } + default: + return E.New("unknown rule set format: ", s.options.Format) + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(s.router, ruleOptions) + if err != nil { + return E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + s.rules = rules + return nil +} + +func (s *RemoteRuleSet) loopUpdate() { + for { + select { + case <-s.ctx.Done(): + return + case <-s.updateTicker.C: + err := s.fetchOnce() + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } + } + } +} + +func (s *RemoteRuleSet) fetchOnce() error { + s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL) + httpClient := &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + }, + } + defer httpClient.CloseIdleConnections() + request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil) + if err != nil { + return err + } + if s.lastEtag != "" { + request.Header.Set("If-None-Match", s.lastEtag) + } + response, err := httpClient.Do(request.WithContext(s.ctx)) + if err != nil { + return err + } + switch response.StatusCode { + case http.StatusOK: + case http.StatusNotModified: + s.logger.Info("update rule-set ", s.options.Tag, ": not modified") + return nil + default: + return E.New("unexpected status: ", response.Status) + } + content, err := io.ReadAll(response.Body) + if err != nil { + response.Body.Close() + return err + } + err = s.loadBytes(content) + if err != nil { + response.Body.Close() + return err + } + response.Body.Close() + eTagHeader := response.Header.Get("Etag") + if eTagHeader != "" { + s.lastEtag = eTagHeader + } + s.lastUpdated = time.Now() + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err = cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedRuleSet{ + LastUpdated: s.lastUpdated, + Content: content, + LastEtag: s.lastEtag, + }) + if err != nil { + s.logger.Error("save rule-set cache: ", err) + } + } + s.logger.Info("updated rule-set ", s.options.Tag) + return nil +} + +func (s *RemoteRuleSet) Close() error { + s.updateTicker.Stop() + s.cancel() + return nil +} From 249e501754d9cebaad0fbc825d7ba22b49eebcb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 29 Nov 2023 17:35:56 +0800 Subject: [PATCH 027/103] documentation: Add rule set --- docs/configuration/dns/rule.md | 32 ++- docs/configuration/dns/rule.zh.md | 32 ++- docs/configuration/experimental/cache-file.md | 34 +++ docs/configuration/experimental/clash-api.md | 121 ++++++++++ docs/configuration/experimental/index.md | 145 ++---------- docs/configuration/experimental/index.zh.md | 137 ------------ docs/configuration/experimental/v2ray-api.md | 50 +++++ docs/configuration/route/geoip.md | 8 + docs/configuration/route/geoip.zh.md | 33 --- docs/configuration/route/geosite.md | 8 + docs/configuration/route/geosite.zh.md | 33 --- docs/configuration/route/index.md | 30 ++- docs/configuration/route/index.zh.md | 32 ++- docs/configuration/route/rule.md | 46 +++- docs/configuration/route/rule.zh.md | 44 +++- docs/configuration/rule-set/headless-rule.md | 207 ++++++++++++++++++ docs/configuration/rule-set/index.md | 97 ++++++++ docs/configuration/rule-set/source-format.md | 34 +++ docs/migration.md | 187 ++++++++++++++++ mkdocs.yml | 28 ++- 20 files changed, 980 insertions(+), 358 deletions(-) create mode 100644 docs/configuration/experimental/cache-file.md create mode 100644 docs/configuration/experimental/clash-api.md delete mode 100644 docs/configuration/experimental/index.zh.md create mode 100644 docs/configuration/experimental/v2ray-api.md delete mode 100644 docs/configuration/route/geoip.zh.md delete mode 100644 docs/configuration/route/geosite.zh.md create mode 100644 docs/configuration/rule-set/headless-rule.md create mode 100644 docs/configuration/rule-set/index.md create mode 100644 docs/configuration/rule-set/source-format.md create mode 100644 docs/migration.md diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 18e352b407..896a3b4468 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -1,3 +1,13 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -85,6 +95,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": [ "direct" @@ -166,15 +180,23 @@ Match domain using regular expression. #### geosite +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + Match geosite. #### source_geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + Match source geoip. #### source_ip_cidr -Match source ip cidr. +Match source IP CIDR. #### source_port @@ -250,6 +272,12 @@ Match WiFi SSID. Match WiFi BSSID. +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [Rule Set](/configuration/route/#rule_set). + #### invert Invert match result. @@ -286,4 +314,4 @@ Rewrite TTL in DNS responses. #### rules -Included default rules. \ No newline at end of file +Included rules. \ No newline at end of file diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 98bfa8ab9a..f990ed3ec5 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -1,3 +1,13 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -84,6 +94,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": [ "direct" @@ -163,10 +177,18 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### geosite -匹配 GeoSite。 +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-set)。 + +匹配 Geosite。 #### source_geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + 匹配源 GeoIP。 #### source_ip_cidr @@ -245,6 +267,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配 WiFi BSSID。 +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + #### invert 反选匹配结果。 @@ -281,4 +309,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### rules -包括的默认规则。 \ No newline at end of file +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md new file mode 100644 index 0000000000..66e30ef9b0 --- /dev/null +++ b/docs/configuration/experimental/cache-file.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "enabled": true, + "path": "", + "cache_id": "", + "store_fakeip": false +} +``` + +### Fields + +#### enabled + +Enable cache file. + +#### path + +Path to the cache file. + +`cache.db` will be used if empty. + +#### cache_id + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. diff --git a/docs/configuration/experimental/clash-api.md b/docs/configuration/experimental/clash-api.md new file mode 100644 index 0000000000..a06fe15480 --- /dev/null +++ b/docs/configuration/experimental/clash-api.md @@ -0,0 +1,121 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-delete-alert: [store_mode](#store_mode) + :material-delete-alert: [store_selected](#store_selected) + :material-delete-alert: [store_fakeip](#store_fakeip) + :material-delete-alert: [cache_file](#cache_file) + :material-delete-alert: [cache_id](#cache_id) + + +!!! quote "" + + Clash API is not included by default, see [Installation](./#installation). + +### Structure + +```json +{ + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" +} +``` + +### Fields + +#### external_controller + +RESTful web API listening address. Clash API will be disabled if empty. + +#### external_ui + +A relative path to the configuration directory or an absolute path to a +directory in which you put some static web resource. sing-box will then +serve it at `http://{{external-controller}}/ui`. + + + +#### external_ui_download_url + +ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. + +`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. + +#### external_ui_download_detour + +The tag of the outbound to download the external UI. + +Default outbound will be used if empty. + +#### secret + +Secret for the RESTful API (optional) +Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` +ALWAYS set a secret if RESTful API is listening on 0.0.0.0 + +#### default_mode + +Default mode in clash, `Rule` will be used if empty. + +This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. + +#### store_mode + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_mode` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +Store Clash mode in cache file. + +#### store_selected + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +!!! note "" + + The tag must be set for target outbounds. + +Store selected outbound for the `Selector` outbound in cache file. + +#### store_fakeip + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and migrated to `cache_file.store_fakeip`. + +Store fakeip in cache file. + +#### cache_file + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_file` is deprecated in Clash API and migrated to `cache_file.enabled` and `cache_file.path`. + +Cache file path, `cache.db` will be used if empty. + +#### cache_id + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_id` is deprecated in Clash API and migrated to `cache_file.cache_id`. + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. \ No newline at end of file diff --git a/docs/configuration/experimental/index.md b/docs/configuration/experimental/index.md index 308e851c3b..1057e59b36 100644 --- a/docs/configuration/experimental/index.md +++ b/docs/configuration/experimental/index.md @@ -1,139 +1,30 @@ +--- +icon: material/alert-decagram +--- + # Experimental +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [cache_file](#cache_file) + :material-alert-decagram: [clash_api](#clash_api) + ### Structure ```json { "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" - }, - "v2ray_api": { - "listen": "127.0.0.1:8080", - "stats": { - "enabled": true, - "inbounds": [ - "socks-in" - ], - "outbounds": [ - "proxy", - "direct" - ], - "users": [ - "sekai" - ] - } - } + "cache_file": {}, + "clash_api": {}, + "v2ray_api": {} } } ``` -!!! note "" - - Traffic statistics and connection management can degrade performance. - -### Clash API Fields - -!!! quote "" - - Clash API is not included by default, see [Installation](./#installation). - -#### external_controller - -RESTful web API listening address. Clash API will be disabled if empty. - -#### external_ui - -A relative path to the configuration directory or an absolute path to a -directory in which you put some static web resource. sing-box will then -serve it at `http://{{external-controller}}/ui`. - -#### external_ui_download_url - -ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. - -`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. - -#### external_ui_download_detour - -The tag of the outbound to download the external UI. - -Default outbound will be used if empty. - -#### secret - -Secret for the RESTful API (optional) -Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` -ALWAYS set a secret if RESTful API is listening on 0.0.0.0 - -#### default_mode - -Default mode in clash, `Rule` will be used if empty. - -This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. - -#### store_mode - -Store Clash mode in cache file. - -#### store_selected - -!!! note "" - - The tag must be set for target outbounds. - -Store selected outbound for the `Selector` outbound in cache file. - -#### store_fakeip - -Store fakeip in cache file. - -#### cache_file - -Cache file path, `cache.db` will be used if empty. - -#### cache_id - -Cache ID. - -If not empty, `store_selected` will use a separate store keyed by it. - -### V2Ray API Fields - -!!! quote "" - - V2Ray API is not included by default, see [Installation](./#installation). - -#### listen - -gRPC API listening address. V2Ray API will be disabled if empty. - -#### stats - -Traffic statistics service settings. - -#### stats.enabled - -Enable statistics service. - -#### stats.inbounds - -Inbound list to count traffic. - -#### stats.outbounds - -Outbound list to count traffic. - -#### stats.users +### Fields -User list to count traffic. \ No newline at end of file +| Key | Format | +|--------------|----------------------------| +| `cache_file` | [Cache File](./cache-file) | +| `clash_api` | [Clash API](./clash-api) | +| `v2ray_api` | [V2Ray API](./v2ray-api) | \ No newline at end of file diff --git a/docs/configuration/experimental/index.zh.md b/docs/configuration/experimental/index.zh.md deleted file mode 100644 index 88a95852c9..0000000000 --- a/docs/configuration/experimental/index.zh.md +++ /dev/null @@ -1,137 +0,0 @@ -# 实验性 - -### 结构 - -```json -{ - "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" - }, - "v2ray_api": { - "listen": "127.0.0.1:8080", - "stats": { - "enabled": true, - "inbounds": [ - "socks-in" - ], - "outbounds": [ - "proxy", - "direct" - ], - "users": [ - "sekai" - ] - } - } - } -} -``` - -!!! note "" - - 流量统计和连接管理会降低性能。 - -### Clash API 字段 - -!!! quote "" - - 默认安装不包含 Clash API,参阅 [安装](/zh/#_2)。 - -#### external_controller - -RESTful web API 监听地址。如果为空,则禁用 Clash API。 - -#### external_ui - -到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。 - -#### external_ui_download_url - -静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。 - -默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。 - -#### external_ui_download_detour - -用于下载静态网页资源的出站的标签。 - -如果为空,将使用默认出站。 - -#### secret - -RESTful API 的密钥(可选) -通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证 -如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。 - -#### default_mode - -Clash 中的默认模式,默认使用 `Rule`。 - -此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。 - -#### store_mode - -将 Clash 模式存储在缓存文件中。 - -#### store_selected - -!!! note "" - - 必须为目标出站设置标签。 - -将 `Selector` 中出站的选定的目标出站存储在缓存文件中。 - -#### store_fakeip - -将 fakeip 存储在缓存文件中。 - -#### cache_file - -缓存文件路径,默认使用`cache.db`。 - -#### cache_id - -缓存 ID。 - -如果不为空,`store_selected` 将会使用以此为键的独立存储。 - -### V2Ray API 字段 - -!!! quote "" - - 默认安装不包含 V2Ray API,参阅 [安装](/zh/#_2)。 - -#### listen - -gRPC API 监听地址。如果为空,则禁用 V2Ray API。 - -#### stats - -流量统计服务设置。 - -#### stats.enabled - -启用统计服务。 - -#### stats.inbounds - -统计流量的入站列表。 - -#### stats.outbounds - -统计流量的出站列表。 - -#### stats.users - -统计流量的用户列表。 \ No newline at end of file diff --git a/docs/configuration/experimental/v2ray-api.md b/docs/configuration/experimental/v2ray-api.md new file mode 100644 index 0000000000..398884242e --- /dev/null +++ b/docs/configuration/experimental/v2ray-api.md @@ -0,0 +1,50 @@ +### Structure + +!!! quote "" + + V2Ray API is not included by default, see [Installation](./#installation). + +```json +{ + "listen": "127.0.0.1:8080", + "stats": { + "enabled": true, + "inbounds": [ + "socks-in" + ], + "outbounds": [ + "proxy", + "direct" + ], + "users": [ + "sekai" + ] + } +} +``` + +### Fields + +#### listen + +gRPC API listening address. V2Ray API will be disabled if empty. + +#### stats + +Traffic statistics service settings. + +#### stats.enabled + +Enable statistics service. + +#### stats.inbounds + +Inbound list to count traffic. + +#### stats.outbounds + +Outbound list to count traffic. + +#### stats.users + +User list to count traffic. \ No newline at end of file diff --git a/docs/configuration/route/geoip.md b/docs/configuration/route/geoip.md index b966a292a5..d6dfd23289 100644 --- a/docs/configuration/route/geoip.md +++ b/docs/configuration/route/geoip.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + ### Structure ```json diff --git a/docs/configuration/route/geoip.zh.md b/docs/configuration/route/geoip.zh.md deleted file mode 100644 index 3ee7042728..0000000000 --- a/docs/configuration/route/geoip.zh.md +++ /dev/null @@ -1,33 +0,0 @@ -### 结构 - -```json -{ - "route": { - "geoip": { - "path": "", - "download_url": "", - "download_detour": "" - } - } -} -``` - -### 字段 - -#### path - -指定 GeoIP 资源的路径。 - -默认 `geoip.db`。 - -#### download_url - -指定 GeoIP 资源的下载链接。 - -默认为 `https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db`。 - -#### download_detour - -用于下载 GeoIP 资源的出站的标签。 - -如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/geosite.md b/docs/configuration/route/geosite.md index db700c6af1..515e86b1ec 100644 --- a/docs/configuration/route/geosite.md +++ b/docs/configuration/route/geosite.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + ### Structure ```json diff --git a/docs/configuration/route/geosite.zh.md b/docs/configuration/route/geosite.zh.md deleted file mode 100644 index bee81fbf71..0000000000 --- a/docs/configuration/route/geosite.zh.md +++ /dev/null @@ -1,33 +0,0 @@ -### 结构 - -```json -{ - "route": { - "geosite": { - "path": "", - "download_url": "", - "download_detour": "" - } - } -} -``` - -### 字段 - -#### path - -指定 GeoSite 资源的路径。 - -默认 `geosite.db`。 - -#### download_url - -指定 GeoSite 资源的下载链接。 - -默认为 `https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db`。 - -#### download_detour - -用于下载 GeoSite 资源的出站的标签。 - -如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 846d497330..7c1787eaea 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -1,5 +1,15 @@ +--- +icon: material/alert-decagram +--- + # Route +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -8,6 +18,7 @@ "geoip": {}, "geosite": {}, "rules": [], + "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, @@ -19,11 +30,20 @@ ### Fields -| Key | Format | -|------------|------------------------------------| -| `geoip` | [GeoIP](./geoip) | -| `geosite` | [Geosite](./geosite) | -| `rules` | List of [Route Rule](./rule) | +| Key | Format | +|-----------|----------------------| +| `geoip` | [GeoIP](./geoip) | +| `geosite` | [Geosite](./geosite) | + +#### rules + +List of [Route Rule](./rule) + +#### rule_set + +!!! question "Since sing-box 1.8.0" + +List of [Rule Set](/configuration/rule-set) #### final diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index 8bef5bea9a..b5302727a3 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -1,5 +1,15 @@ +--- +icon: material/alert-decagram +--- + # 路由 +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -7,8 +17,8 @@ "route": { "geoip": {}, "geosite": {}, - "ip_rules": [], "rules": [], + "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, @@ -20,11 +30,21 @@ ### 字段 -| 键 | 格式 | -|------------|-------------------------| -| `geoip` | [GeoIP](./geoip) | -| `geosite` | [GeoSite](./geosite) | -| `rules` | 一组 [路由规则](./rule) | +| 键 | 格式 | +|------------|-----------------------------------| +| `geoip` | [GeoIP](./geoip) | +| `geosite` | [Geosite](./geosite) | + + +#### rule + +一组 [路由规则](./rule)。 + +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +一组 [规则集](/configuration/rule-set)。 #### final diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index abbfde6fdc..35c1d8fc24 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -1,3 +1,15 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -89,6 +101,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": "direct" }, @@ -160,23 +176,35 @@ Match domain using regular expression. #### geosite +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + Match geosite. #### source_geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + Match source geoip. #### geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + Match geoip. #### source_ip_cidr -Match source ip cidr. +Match source IP CIDR. #### ip_cidr -Match ip cidr. +Match IP CIDR. #### source_port @@ -250,6 +278,18 @@ Match WiFi SSID. Match WiFi BSSID. +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [Rule Set](/configuration/route/#rule_set). + +#### rule_set_ipcidr_match_source + +!!! question "Since sing-box 1.8.0" + +Make `ipcidr` in rule sets match the source IP. + #### invert Invert match result. @@ -276,4 +316,4 @@ Tag of the target outbound. ==Required== -Included default rules. +Included rules. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index f4ab7890a7..31ee08d13a 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -1,3 +1,15 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -87,6 +99,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": "direct" }, @@ -158,14 +174,26 @@ #### geosite -匹配 GeoSite。 +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-set)。 + +匹配 Geosite。 #### source_geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + 匹配源 GeoIP。 #### geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + 匹配 GeoIP。 #### source_ip_cidr @@ -248,6 +276,18 @@ 匹配 WiFi BSSID。 +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + +#### rule_set_ipcidr_match_source + +!!! question "自 sing-box 1.8.0 起" + +使规则集中的 `ipcidr` 规则匹配源 IP。 + #### invert 反选匹配结果。 @@ -274,4 +314,4 @@ ==必填== -包括的默认规则。 \ No newline at end of file +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md new file mode 100644 index 0000000000..6ab62eb2e3 --- /dev/null +++ b/docs/configuration/rule-set/headless-rule.md @@ -0,0 +1,207 @@ +--- +icon: material/new-box +--- + +### Structure + +!!! question "Since sing-box 1.8.0" + +```json +{ + "rules": [ + { + "query_type": [ + "A", + "HTTPS", + 32768 + ], + "network": [ + "tcp" + ], + "domain": [ + "test.com" + ], + "domain_suffix": [ + ".cn" + ], + "domain_keyword": [ + "test" + ], + "domain_regex": [ + "^stun\\..+" + ], + "source_ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "source_port": [ + 12345 + ], + "source_port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "port": [ + 80, + 443 + ], + "port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "process_name": [ + "curl" + ], + "process_path": [ + "/usr/bin/curl" + ], + "package_name": [ + "com.termux" + ], + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], + "invert": false + }, + { + "type": "logical", + "mode": "and", + "rules": [], + "invert": false + } + ] +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Default Fields + +!!! note "" + + The default rule uses the following matching logic: + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) && + (`port` || `port_range`) && + (`source_port` || `source_port_range`) && + `other fields` + +#### query_type + +DNS query type. Values can be integers or type name strings. + +#### network + +`tcp` or `udp`. + +#### domain + +Match full domain. + +#### domain_suffix + +Match domain suffix. + +#### domain_keyword + +Match domain using keyword. + +#### domain_regex + +Match domain using regular expression. + +#### source_ip_cidr + +Match source IP CIDR. + +#### ip_cidr + +!!! info "" + + `ip_cidr` is an alias for `source_ip_cidr` when the Rule Set is used in DNS rules or `rule_set_ipcidr_match_source` enabled in route rules. + +Match IP CIDR. + +#### source_port + +Match source port. + +#### source_port_range + +Match source port range. + +#### port + +Match port. + +#### port_range + +Match port range. + +#### process_name + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process name. + +#### process_path + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path. + +#### package_name + +Match android package name. + +#### wifi_ssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi SSID. + +#### wifi_bssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi BSSID. + +#### invert + +Invert match result. + +### Logical Fields + +#### type + +`logical` + +#### mode + +==Required== + +`and` or `or` + +#### rules + +==Required== + +Included rules. diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md new file mode 100644 index 0000000000..5aff55b371 --- /dev/null +++ b/docs/configuration/rule-set/index.md @@ -0,0 +1,97 @@ +--- +icon: material/new-box +--- + +# Rule Set + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "type": "", + "tag": "", + "format": "", + + ... // Typed Fields +} +``` + +#### Local Structure + +```json +{ + "type": "local", + + ... + + "path": "" +} +``` + +#### Remote Structure + +!!! info "" + + Remote rule-set will be cached if `experimental.cache_file.enabled`. + +```json +{ + "type": "remote", + + ..., + + "url": "", + "download_detour": "", + "update_interval": "" +} +``` + +### Fields + +#### type + +==Required== + +Type of Rule Set, `local` or `remote`. + +#### tag + +==Required== + +Tag of Rule Set. + +#### format + +==Required== + +Format of Rule Set, `source` or `binary`. + +### Local Fields + +#### path + +==Required== + +File path of Rule Set. + +### Remote Fields + +#### url + +==Required== + +Download URL of Rule Set. + +#### download_detour + +Tag of the outbound to download rule-set. + +Default outbound will be used if empty. + +#### update_interval + +Update interval of Rule Set. + +`1d` will be used if empty. diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md new file mode 100644 index 0000000000..116c1ee66f --- /dev/null +++ b/docs/configuration/rule-set/source-format.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +# Source Format + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "version": 1, + "rules": [] +} +``` + +### Compile + +Use `sing-box rule-set compile [--output .srs] .json` to compile source to binary rule-set. + +### Fields + +#### version + +==Required== + +Version of Rule Set, must be `1`. + +#### rules + +==Required== + +List of [Headless Rule](./headless-rule.md). diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000000..8eb3daee42 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,187 @@ +--- +icon: material/arrange-bring-forward +--- + +# Migration + +## 1.8.0 + +!!! warning "Unstable" + + This version is still under development, and the following migration guide may be changed in the future. + +### :material-close-box: Migrate cache file from Clash API to independent options + +!!! info "Reference" + + [Clash API](/configuration/experimental/clash-api) / + [Cache File](/configuration/experimental/cache-file) + +=== ":material-card-remove: Deprecated" + + ```json + { + "experimental": { + "clash_api": { + "cache_file": "cache.db", // default value + "cahce_id": "my_profile2", + "store_mode": true, + "store_selected": true, + "store_fakeip": true + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "experimental" : { + "cache_file": { + "enabled": true, + "path": "cache.db", // default value + "cache_id": "my_profile2", + "store_fakeip": true + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate GeoIP to rule sets + +!!! info "Reference" + + [GeoIP](/configuration/route/geoip) / + [Route](/configuration/route) / + [Route Rule](/configuration/route/rule) / + [DNS Rule](/configuration/dns/rule) / + [Rule Set](/configuration/rule-set) + +!!! tip + + `sing-box geoip` commands can help you convert custom GeoIP into rule sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geoip": "cn", + "outbound": "direct" + }, + { + "source_geoip": "cn", + "outbound": "block" + } + ], + "geoip": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geoip-cn", + "outbound": "direct" + }, + { + "rule_set": "geoip-us", + "rule_set_ipcidr_match_source": true, + "outbound": "block" + } + ], + "rule_set": [ + { + "tag": "geoip-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", + "download_detour": "proxy" + }, + { + "tag": "geoip-us", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate Geosite to rule sets + +!!! info "Reference" + + [Geosite](/configuration/route/geosite) / + [Route](/configuration/route) / + [Route Rule](/configuration/route/rule) / + [DNS Rule](/configuration/dns/rule) / + [Rule Set](/configuration/rule-set) + +!!! tip + + `sing-box geosite` commands can help you convert custom Geosite into rule sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geosite": "cn", + "outbound": "direct" + } + ], + "geosite": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geosite-cn", + "outbound": "direct" + } + ], + "rule_set": [ + { + "tag": "geosite-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 1d4b1d8b0e..c5dd7df335 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,12 +32,16 @@ theme: - content.code.copy - content.code.select - content.code.annotate + icon: + admonition: + question: material/new-box nav: - Home: - index.md + - Change Log: changelog.md + - Migration: migration.md - Deprecated: deprecated.md - Support: support.md - - Change Log: changelog.md - Installation: - Package Manager: installation/package-manager.md - Docker: installation/docker.md @@ -56,7 +60,7 @@ nav: - Proxy: - Server: manual/proxy/server.md - Client: manual/proxy/client.md -# - TUN: manual/proxy/tun.md + # - TUN: manual/proxy/tun.md - Proxy Protocol: - Shadowsocks: manual/proxy-protocol/shadowsocks.md - Trojan: manual/proxy-protocol/trojan.md @@ -79,8 +83,15 @@ nav: - Geosite: configuration/route/geosite.md - Route Rule: configuration/route/rule.md - Protocol Sniff: configuration/route/sniff.md + - Rule Set: + - configuration/rule-set/index.md + - Source Format: configuration/rule-set/source-format.md + - Headless Rule: configuration/rule-set/headless-rule.md - Experimental: - configuration/experimental/index.md + - Cache File: configuration/experimental/cache-file.md + - Clash API: configuration/experimental/clash-api.md + - V2Ray API: configuration/experimental/v2ray-api.md - Shared: - Listen Fields: configuration/shared/listen.md - Dial Fields: configuration/shared/dial.md @@ -180,9 +191,10 @@ plugins: name: 简体中文 nav_translations: Home: 开始 + Change Log: 更新日志 + Migration: 迁移指南 Deprecated: 废弃功能列表 Support: 支持 - Change Log: 更新日志 Installation: 安装 Package Manager: 包管理器 @@ -203,6 +215,10 @@ plugins: Route Rule: 路由规则 Protocol Sniff: 协议探测 + Rule Set: 规则集 + Source Format: 源文件格式 + Headless Rule: 无头规则 + Experimental: 实验性 Shared: 通用 @@ -215,10 +231,6 @@ plugins: Inbound: 入站 Outbound: 出站 - FAQ: 常见问题 - Known Issues: 已知问题 - Examples: 示例 - Linux Server Installation: Linux 服务器安装 - DNS Hijack: DNS 劫持 + Manual: 手册 reconfigure_material: true reconfigure_search: true \ No newline at end of file From bbc1d12015d65ee0ab060cf681a866bf42e868d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 29 Nov 2023 23:24:37 +0800 Subject: [PATCH 028/103] documentation: Bump version --- docs/changelog.md | 50 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 94a7147147..78fe3e81db 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,43 @@ icon: material/alert-decagram # ChangeLog +#### 1.8.0-alpha.1 + +* Migrate cache file from Clash API to independent options **1** +* Introducing [Rule Set](/configuration/rule-set) **2** +* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** +* Allow nested logical rules **4** + +**1**: + +See [Cache File](/configuration/experimental/cache-file) and +[Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options). + +**2**: + +Rule set is independent collections of rules that can be compiled into binaries to improve performance. +Compared to legacy GeoIP and Geosite resources, +it can include more types of rules, load faster, +use less memory, and update automatically. + +See [Route#rule_set](/configuration/route/#rule_set), +[Route Rule](/configuration/route/rule), +[DNS Rule](/configuration/dns/rule), +[Rule Set](/configuration/rule-set), +[Source Format](/configuration/rule-set/source-format) and +[Headless Rule](/configuration/rule-set/headless-rule). + +For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and +[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets). + +**3**: + +New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets. + +**4**: + +Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules. + #### 1.7.0 * Fixes and improvements @@ -142,11 +179,13 @@ Only supported in graphical clients on Android and iOS. **1**: -Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound options. +Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound +options. **2** -Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server, see [TCP Brutal](/configuration/shared/tcp-brutal) for details. +Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server, +see [TCP Brutal](/configuration/shared/tcp-brutal) for details. #### 1.7.0-alpha.3 @@ -213,8 +252,8 @@ When `auto_route` is enabled and `strict_route` is disabled, the device can now **2**: -Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave. - +Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High +Sierra, 10.14 Mojave. #### 1.6.0-rc.4 @@ -227,7 +266,8 @@ Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008 **1**: -Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave. +Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High +Sierra, 10.14 Mojave. #### 1.6.0-beta.4 From cb28aba697487e8b17ead69fce61009d8a16c4c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 30 Nov 2023 11:05:15 +0800 Subject: [PATCH 029/103] documentation: Update rule-set example --- docs/manual/proxy/client.md | 184 ++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/docs/manual/proxy/client.md b/docs/manual/proxy/client.md index 60db02deb6..9a6ede63b2 100644 --- a/docs/manual/proxy/client.md +++ b/docs/manual/proxy/client.md @@ -343,6 +343,83 @@ flowchart TB } ``` +=== ":material-dns: DNS rules (1.8.0+)" + + !!! info + + DNS rules are optional if FakeIP is used. + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "address": "tls://8.8.8.8" + }, + { + "tag": "local", + "address": "223.5.5.5", + "detour": "direct" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + }, + { + "clash_mode": "Direct", + "server": "local" + }, + { + "clash_mode": "Global", + "server": "google" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": [ + "geosite-cn", + "geosite-category-companies@cn" + ] + } + ], + "server": "local" + } + ] + }, + "route": { + "rule_set": [ + { + "type": "remote", + "tag": "geosite-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-geolocation-!cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" + }, + { + "type": "remote", + "tag": "geosite-category-companies@cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-companies@cn.srs" + } + ] + } + } + ``` + === ":material-router-network: Route rules" ```json @@ -422,4 +499,111 @@ flowchart TB ] } } + ``` + +=== ":material-router-network: Route rules (1.8.0+)" + + ```json + { + "outbounds": [ + { + "type": "direct", + "tag": "direct" + }, + { + "type": "block", + "tag": "block" + } + ], + "route": { + "rules": [ + { + "type": "logical", + "mode": "or", + "rules": [ + { + "protocol": "dns" + }, + { + "port": 53 + } + ], + "outbound": "dns" + }, + { + "geoip": "private", + "outbound": "direct" + }, + { + "clash_mode": "Direct", + "outbound": "direct" + }, + { + "clash_mode": "Global", + "outbound": "default" + }, + { + "type": "logical", + "mode": "or", + "rules": [ + { + "port": 853 + }, + { + "network": "udp", + "port": 443 + }, + { + "protocol": "stun" + } + ], + "outbound": "block" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": [ + "geoip-cn", + "geosite-cn", + "geosite-category-companies@cn" + ] + } + ], + "outbound": "direct" + } + ], + "rule_set": [ + { + "type": "remote", + "tag": "geoip-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-geolocation-!cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" + }, + { + "type": "remote", + "tag": "geosite-category-companies@cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-companies@cn.srs" + } + ] + } + } ``` \ No newline at end of file From b256e54a0b8baeea5b71e75c03efa637b482b9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 29 Nov 2023 17:35:40 +0800 Subject: [PATCH 030/103] Add rule set --- .gitignore | 1 + adapter/experimental.go | 66 ++- adapter/inbound.go | 1 + adapter/router.go | 15 +- cmd/sing-box/cmd_format.go | 39 -- cmd/sing-box/cmd_geoip.go | 43 ++ cmd/sing-box/cmd_geoip_export.go | 98 ++++ cmd/sing-box/cmd_geoip_list.go | 31 ++ cmd/sing-box/cmd_geoip_lookup.go | 47 ++ cmd/sing-box/cmd_geosite.go | 41 ++ cmd/sing-box/cmd_geosite_export.go | 81 +++ cmd/sing-box/cmd_geosite_list.go | 50 ++ cmd/sing-box/cmd_geosite_lookup.go | 97 ++++ cmd/sing-box/cmd_geosite_matcher.go | 56 ++ cmd/sing-box/cmd_merge.go | 2 +- cmd/sing-box/cmd_rule_set.go | 14 + cmd/sing-box/cmd_rule_set_compile.go | 80 +++ cmd/sing-box/cmd_rule_set_format.go | 87 ++++ cmd/sing-box/cmd_tools.go | 6 +- cmd/sing-box/cmd_tools_connect.go | 2 +- common/dialer/router.go | 12 +- common/srs/binary.go | 485 ++++++++++++++++++ common/srs/ip_set.go | 116 +++++ constant/rule.go | 8 + experimental/cachefile/cache.go | 35 ++ experimental/cachefile/fakeip.go | 2 +- experimental/clashapi/proxies.go | 6 +- experimental/clashapi/server_resources.go | 6 +- .../clashapi/trafficontrol/tracker.go | 8 +- go.mod | 2 +- go.sum | 4 +- option/experimental.go | 8 +- option/route.go | 1 + option/rule.go | 58 ++- option/rule_dns.go | 1 + option/rule_set.go | 230 +++++++++ option/types.go | 8 + route/router.go | 34 +- route/rule_abstract.go | 22 +- route/rule_default.go | 7 +- route/rule_dns.go | 7 +- route/rule_headless.go | 173 +++++++ route/rule_item_cidr.go | 19 +- route/rule_item_domain.go | 7 + route/rule_item_rule_set.go | 55 ++ route/rule_set.go | 22 + route/rule_set_local.go | 69 +++ route/rule_set_remote.go | 218 ++++++++ 48 files changed, 2375 insertions(+), 105 deletions(-) create mode 100644 cmd/sing-box/cmd_geoip.go create mode 100644 cmd/sing-box/cmd_geoip_export.go create mode 100644 cmd/sing-box/cmd_geoip_list.go create mode 100644 cmd/sing-box/cmd_geoip_lookup.go create mode 100644 cmd/sing-box/cmd_geosite.go create mode 100644 cmd/sing-box/cmd_geosite_export.go create mode 100644 cmd/sing-box/cmd_geosite_list.go create mode 100644 cmd/sing-box/cmd_geosite_lookup.go create mode 100644 cmd/sing-box/cmd_geosite_matcher.go create mode 100644 cmd/sing-box/cmd_rule_set.go create mode 100644 cmd/sing-box/cmd_rule_set_compile.go create mode 100644 cmd/sing-box/cmd_rule_set_format.go create mode 100644 common/srs/binary.go create mode 100644 common/srs/ip_set.go create mode 100644 option/rule_set.go create mode 100644 route/rule_headless.go create mode 100644 route/rule_item_rule_set.go create mode 100644 route/rule_set.go create mode 100644 route/rule_set_local.go create mode 100644 route/rule_set_remote.go diff --git a/.gitignore b/.gitignore index 6630f428ce..55bdab3a0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.idea/ /vendor/ /*.json +/*.srs /*.db /site/ /bin/ diff --git a/adapter/experimental.go b/adapter/experimental.go index ac523e4e74..c26ee8c1cf 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -1,11 +1,16 @@ package adapter import ( + "bytes" "context" + "encoding/binary" + "io" "net" + "time" "github.com/sagernet/sing-box/common/urltest" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" ) type ClashServer interface { @@ -23,6 +28,7 @@ type CacheFile interface { PreStarter StoreFakeIP() bool + FakeIPStorage LoadMode() string StoreMode(mode string) error @@ -30,7 +36,65 @@ type CacheFile interface { StoreSelected(group string, selected string) error LoadGroupExpand(group string) (isExpand bool, loaded bool) StoreGroupExpand(group string, expand bool) error - FakeIPStorage + LoadRuleSet(tag string) *SavedRuleSet + SaveRuleSet(tag string, set *SavedRuleSet) error +} + +type SavedRuleSet struct { + Content []byte + LastUpdated time.Time + LastEtag string +} + +func (s *SavedRuleSet) MarshalBinary() ([]byte, error) { + var buffer bytes.Buffer + err := binary.Write(&buffer, binary.BigEndian, uint8(1)) + if err != nil { + return nil, err + } + err = rw.WriteUVariant(&buffer, uint64(len(s.Content))) + if err != nil { + return nil, err + } + buffer.Write(s.Content) + err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix()) + if err != nil { + return nil, err + } + err = rw.WriteVString(&buffer, s.LastEtag) + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func (s *SavedRuleSet) UnmarshalBinary(data []byte) error { + reader := bytes.NewReader(data) + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return err + } + contentLen, err := rw.ReadUVariant(reader) + if err != nil { + return err + } + s.Content = make([]byte, contentLen) + _, err = io.ReadFull(reader, s.Content) + if err != nil { + return err + } + var lastUpdated int64 + err = binary.Read(reader, binary.BigEndian, &lastUpdated) + if err != nil { + return err + } + s.LastUpdated = time.Unix(lastUpdated, 0) + s.LastEtag, err = rw.ReadVString(reader) + if err != nil { + return err + } + return nil } type Tracker interface { diff --git a/adapter/inbound.go b/adapter/inbound.go index 2d24083c4a..30dec9d1f7 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -47,6 +47,7 @@ type InboundContext struct { GeoIPCode string ProcessInfo *process.Info FakeIP bool + IPCIDRMatchSource bool // dns cache diff --git a/adapter/router.go b/adapter/router.go index e4c3904d51..ca4d6547c4 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -18,7 +18,7 @@ type Router interface { Outbounds() []Outbound Outbound(tag string) (Outbound, bool) - DefaultOutbound(network string) Outbound + DefaultOutbound(network string) (Outbound, error) FakeIPStore() FakeIPStore @@ -27,6 +27,8 @@ type Router interface { GeoIPReader() *geoip.Reader LoadGeosite(code string) (Rule, error) + RuleSet(tag string) (RuleSet, bool) + Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) @@ -61,11 +63,15 @@ func RouterFromContext(ctx context.Context) Router { return service.FromContext[Router](ctx) } +type HeadlessRule interface { + Match(metadata *InboundContext) bool +} + type Rule interface { + HeadlessRule Service Type() string UpdateGeosite() error - Match(metadata *InboundContext) bool Outbound() string String() string } @@ -76,6 +82,11 @@ type DNSRule interface { RewriteTTL() *uint32 } +type RuleSet interface { + HeadlessRule + Service +} + type InterfaceUpdateListener interface { InterfaceUpdated() } diff --git a/cmd/sing-box/cmd_format.go b/cmd/sing-box/cmd_format.go index 10a5497cd4..c5e939e4a9 100644 --- a/cmd/sing-box/cmd_format.go +++ b/cmd/sing-box/cmd_format.go @@ -7,7 +7,6 @@ import ( "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/spf13/cobra" @@ -69,41 +68,3 @@ func format() error { } return nil } - -func formatOne(configPath string) error { - configContent, err := os.ReadFile(configPath) - if err != nil { - return E.Cause(err, "read config") - } - var options option.Options - err = options.UnmarshalJSON(configContent) - if err != nil { - return E.Cause(err, "decode config") - } - buffer := new(bytes.Buffer) - encoder := json.NewEncoder(buffer) - encoder.SetIndent("", " ") - err = encoder.Encode(options) - if err != nil { - return E.Cause(err, "encode config") - } - if !commandFormatFlagWrite { - os.Stdout.WriteString(buffer.String() + "\n") - return nil - } - if bytes.Equal(configContent, buffer.Bytes()) { - return nil - } - output, err := os.Create(configPath) - if err != nil { - return E.Cause(err, "open output") - } - _, err = output.Write(buffer.Bytes()) - output.Close() - if err != nil { - return E.Cause(err, "write output") - } - outputPath, _ := filepath.Abs(configPath) - os.Stderr.WriteString(outputPath + "\n") - return nil -} diff --git a/cmd/sing-box/cmd_geoip.go b/cmd/sing-box/cmd_geoip.go new file mode 100644 index 0000000000..dbbbff135e --- /dev/null +++ b/cmd/sing-box/cmd_geoip.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var ( + geoipReader *maxminddb.Reader + commandGeoIPFlagFile string +) + +var commandGeoip = &cobra.Command{ + Use: "geoip", + Short: "GeoIP tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geoipPreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.PersistentFlags().StringVarP(&commandGeoIPFlagFile, "file", "f", "geoip.db", "geoip file") + mainCommand.AddCommand(commandGeoip) +} + +func geoipPreRun() error { + reader, err := maxminddb.Open(commandGeoIPFlagFile) + if err != nil { + return err + } + if reader.Metadata.DatabaseType != "sing-geoip" { + reader.Close() + return E.New("incorrect database type, expected sing-geoip, got ", reader.Metadata.DatabaseType) + } + geoipReader = reader + return nil +} diff --git a/cmd/sing-box/cmd_geoip_export.go b/cmd/sing-box/cmd_geoip_export.go new file mode 100644 index 0000000000..d170d10b72 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_export.go @@ -0,0 +1,98 @@ +package main + +import ( + "io" + "net" + "os" + "strings" + + "github.com/sagernet/sing-box/common/json" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var flagGeoipExportOutput string + +const flagGeoipExportDefaultOutput = "geoip-.srs" + +var commandGeoipExport = &cobra.Command{ + Use: "export ", + Short: "Export geoip country as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoipExport.Flags().StringVarP(&flagGeoipExportOutput, "output", "o", flagGeoipExportDefaultOutput, "Output path") + commandGeoip.AddCommand(commandGeoipExport) +} + +func geoipExport(countryCode string) error { + networks := geoipReader.Networks(maxminddb.SkipAliasedNetworks) + countryMap := make(map[string][]*net.IPNet) + var ( + ipNet *net.IPNet + nextCountryCode string + err error + ) + for networks.Next() { + ipNet, err = networks.Network(&nextCountryCode) + if err != nil { + return err + } + countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet) + } + ipNets := countryMap[strings.ToLower(countryCode)] + if len(ipNets) == 0 { + return E.New("country code not found: ", countryCode) + } + + var ( + outputFile *os.File + outputWriter io.Writer + ) + if flagGeoipExportOutput == "stdout" { + outputWriter = os.Stdout + } else if flagGeoipExportOutput == flagGeoipExportDefaultOutput { + outputFile, err = os.Create("geoip-" + countryCode + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(flagGeoipExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + headlessRule.IPCIDR = make([]string, 0, len(ipNets)) + for _, cidr := range ipNets { + headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String()) + } + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geoip_list.go b/cmd/sing-box/cmd_geoip_list.go new file mode 100644 index 0000000000..54dd426ea9 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_list.go @@ -0,0 +1,31 @@ +package main + +import ( + "os" + + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var commandGeoipList = &cobra.Command{ + Use: "list", + Short: "List geoip country codes", + Run: func(cmd *cobra.Command, args []string) { + err := listGeoip() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipList) +} + +func listGeoip() error { + for _, code := range geoipReader.Metadata.Languages { + os.Stdout.WriteString(code + "\n") + } + return nil +} diff --git a/cmd/sing-box/cmd_geoip_lookup.go b/cmd/sing-box/cmd_geoip_lookup.go new file mode 100644 index 0000000000..d5157bb404 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_lookup.go @@ -0,0 +1,47 @@ +package main + +import ( + "net/netip" + "os" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + + "github.com/spf13/cobra" +) + +var commandGeoipLookup = &cobra.Command{ + Use: "lookup
", + Short: "Lookup if an IP address is contained in the GeoIP database", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipLookup(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipLookup) +} + +func geoipLookup(address string) error { + addr, err := netip.ParseAddr(address) + if err != nil { + return E.Cause(err, "parse address") + } + if !N.IsPublicAddr(addr) { + os.Stdout.WriteString("private\n") + return nil + } + var code string + _ = geoipReader.Lookup(addr.AsSlice(), &code) + if code != "" { + os.Stdout.WriteString(code + "\n") + return nil + } + os.Stdout.WriteString("unknown\n") + return nil +} diff --git a/cmd/sing-box/cmd_geosite.go b/cmd/sing-box/cmd_geosite.go new file mode 100644 index 0000000000..95db935797 --- /dev/null +++ b/cmd/sing-box/cmd_geosite.go @@ -0,0 +1,41 @@ +package main + +import ( + "github.com/sagernet/sing-box/common/geosite" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var ( + commandGeoSiteFlagFile string + geositeReader *geosite.Reader + geositeCodeList []string +) + +var commandGeoSite = &cobra.Command{ + Use: "geosite", + Short: "Geosite tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geositePreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.PersistentFlags().StringVarP(&commandGeoSiteFlagFile, "file", "f", "geosite.db", "geosite file") + mainCommand.AddCommand(commandGeoSite) +} + +func geositePreRun() error { + reader, codeList, err := geosite.Open(commandGeoSiteFlagFile) + if err != nil { + return E.Cause(err, "open geosite file") + } + geositeReader = reader + geositeCodeList = codeList + return nil +} diff --git a/cmd/sing-box/cmd_geosite_export.go b/cmd/sing-box/cmd_geosite_export.go new file mode 100644 index 0000000000..71f1018d8f --- /dev/null +++ b/cmd/sing-box/cmd_geosite_export.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/json" + "io" + "os" + + "github.com/sagernet/sing-box/common/geosite" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "github.com/spf13/cobra" +) + +var commandGeositeExportOutput string + +const commandGeositeExportDefaultOutput = "geosite-.json" + +var commandGeositeExport = &cobra.Command{ + Use: "export ", + Short: "Export geosite category as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geositeExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeositeExport.Flags().StringVarP(&commandGeositeExportOutput, "output", "o", commandGeositeExportDefaultOutput, "Output path") + commandGeoSite.AddCommand(commandGeositeExport) +} + +func geositeExport(category string) error { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + var ( + outputFile *os.File + outputWriter io.Writer + ) + if commandGeositeExportOutput == "stdout" { + outputWriter = os.Stdout + } else if commandGeositeExportOutput == commandGeositeExportDefaultOutput { + outputFile, err = os.Create("geosite-" + category + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(commandGeositeExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + defaultRule := geosite.Compile(sourceSet) + headlessRule.Domain = defaultRule.Domain + headlessRule.DomainSuffix = defaultRule.DomainSuffix + headlessRule.DomainKeyword = defaultRule.DomainKeyword + headlessRule.DomainRegex = defaultRule.DomainRegex + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geosite_list.go b/cmd/sing-box/cmd_geosite_list.go new file mode 100644 index 0000000000..cedb7adfd2 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_list.go @@ -0,0 +1,50 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + F "github.com/sagernet/sing/common/format" + + "github.com/spf13/cobra" +) + +var commandGeositeList = &cobra.Command{ + Use: "list ", + Short: "List geosite categories", + Run: func(cmd *cobra.Command, args []string) { + err := geositeList() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeList) +} + +func geositeList() error { + var geositeEntry []struct { + category string + items int + } + for _, category := range geositeCodeList { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + geositeEntry = append(geositeEntry, struct { + category string + items int + }{category, len(sourceSet)}) + } + sort.SliceStable(geositeEntry, func(i, j int) bool { + return geositeEntry[i].items < geositeEntry[j].items + }) + for _, entry := range geositeEntry { + os.Stdout.WriteString(F.ToString(entry.category, " (", entry.items, ")\n")) + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_lookup.go b/cmd/sing-box/cmd_geosite_lookup.go new file mode 100644 index 0000000000..f648ce62a4 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_lookup.go @@ -0,0 +1,97 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandGeositeLookup = &cobra.Command{ + Use: "lookup [category] ", + Short: "Check if a domain is in the geosite", + Args: cobra.RangeArgs(1, 2), + Run: func(cmd *cobra.Command, args []string) { + var ( + source string + target string + ) + switch len(args) { + case 1: + target = args[0] + case 2: + source = args[0] + target = args[1] + } + err := geositeLookup(source, target) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeLookup) +} + +func geositeLookup(source string, target string) error { + var sourceMatcherList []struct { + code string + matcher *searchGeositeMatcher + } + if source != "" { + sourceSet, err := geositeReader.Read(source) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+source) + } + sourceMatcherList = []struct { + code string + matcher *searchGeositeMatcher + }{ + { + code: source, + matcher: sourceMatcher, + }, + } + + } else { + for _, code := range geositeCodeList { + sourceSet, err := geositeReader.Read(code) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+code) + } + sourceMatcherList = append(sourceMatcherList, struct { + code string + matcher *searchGeositeMatcher + }{ + code: code, + matcher: sourceMatcher, + }) + } + } + sort.SliceStable(sourceMatcherList, func(i, j int) bool { + return sourceMatcherList[i].code < sourceMatcherList[j].code + }) + + for _, matcherItem := range sourceMatcherList { + if matchRule := matcherItem.matcher.Match(target); matchRule != "" { + os.Stdout.WriteString("Match code (") + os.Stdout.WriteString(matcherItem.code) + os.Stdout.WriteString(") ") + os.Stdout.WriteString(matchRule) + os.Stdout.WriteString("\n") + } + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_matcher.go b/cmd/sing-box/cmd_geosite_matcher.go new file mode 100644 index 0000000000..791dba2499 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_matcher.go @@ -0,0 +1,56 @@ +package main + +import ( + "regexp" + "strings" + + "github.com/sagernet/sing-box/common/geosite" +) + +type searchGeositeMatcher struct { + domainMap map[string]bool + suffixList []string + keywordList []string + regexList []string +} + +func newSearchGeositeMatcher(items []geosite.Item) (*searchGeositeMatcher, error) { + options := geosite.Compile(items) + domainMap := make(map[string]bool) + for _, domain := range options.Domain { + domainMap[domain] = true + } + rule := &searchGeositeMatcher{ + domainMap: domainMap, + suffixList: options.DomainSuffix, + keywordList: options.DomainKeyword, + regexList: options.DomainRegex, + } + return rule, nil +} + +func (r *searchGeositeMatcher) Match(domain string) string { + if r.domainMap[domain] { + return "domain=" + domain + } + for _, suffix := range r.suffixList { + if strings.HasSuffix(domain, suffix) { + return "domain_suffix=" + suffix + } + } + for _, keyword := range r.keywordList { + if strings.Contains(domain, keyword) { + return "domain_keyword=" + keyword + } + } + for _, regexStr := range r.regexList { + regex, err := regexp.Compile(regexStr) + if err != nil { + continue + } + if regex.MatchString(domain) { + return "domain_regex=" + regexStr + } + } + return "" +} diff --git a/cmd/sing-box/cmd_merge.go b/cmd/sing-box/cmd_merge.go index 0aff750182..4fb07b8688 100644 --- a/cmd/sing-box/cmd_merge.go +++ b/cmd/sing-box/cmd_merge.go @@ -18,7 +18,7 @@ import ( ) var commandMerge = &cobra.Command{ - Use: "merge [output]", + Use: "merge ", Short: "Merge configurations", Run: func(cmd *cobra.Command, args []string) { err := merge(args[0]) diff --git a/cmd/sing-box/cmd_rule_set.go b/cmd/sing-box/cmd_rule_set.go new file mode 100644 index 0000000000..f4112a087b --- /dev/null +++ b/cmd/sing-box/cmd_rule_set.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +var commandRuleSet = &cobra.Command{ + Use: "rule-set", + Short: "Manage rule sets", +} + +func init() { + mainCommand.AddCommand(commandRuleSet) +} diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go new file mode 100644 index 0000000000..de318095ac --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -0,0 +1,80 @@ +package main + +import ( + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "github.com/spf13/cobra" +) + +var flagRuleSetCompileOutput string + +const flagRuleSetCompileDefaultOutput = ".srs" + +var commandRuleSetCompile = &cobra.Command{ + Use: "compile [source-path]", + Short: "Compile rule-set json to binary", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := compileRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSet.AddCommand(commandRuleSetCompile) + commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file") +} + +func compileRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + decoder := json.NewDecoder(json.NewCommentFilter(reader)) + decoder.DisallowUnknownFields() + var plainRuleSet option.PlainRuleSetCompat + err = decoder.Decode(&plainRuleSet) + if err != nil { + return err + } + ruleSet := plainRuleSet.Upgrade() + var outputPath string + if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { + if strings.HasSuffix(sourcePath, ".json") { + outputPath = sourcePath[:len(sourcePath)-5] + ".srs" + } else { + outputPath = sourcePath + ".srs" + } + } else { + outputPath = flagRuleSetCompileOutput + } + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + err = srs.Write(outputFile, ruleSet) + if err != nil { + outputFile.Close() + os.Remove(outputPath) + return err + } + outputFile.Close() + return nil +} diff --git a/cmd/sing-box/cmd_rule_set_format.go b/cmd/sing-box/cmd_rule_set_format.go new file mode 100644 index 0000000000..dc3ee6aabd --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_format.go @@ -0,0 +1,87 @@ +package main + +import ( + "bytes" + "io" + "os" + "path/filepath" + + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandRuleSetFormatFlagWrite bool + +var commandRuleSetFormat = &cobra.Command{ + Use: "format ", + Short: "Format rule-set json", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := formatRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") + commandRuleSet.AddCommand(commandRuleSetFormat) +} + +func formatRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + content, err := io.ReadAll(reader) + if err != nil { + return err + } + decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content))) + decoder.DisallowUnknownFields() + var plainRuleSet option.PlainRuleSetCompat + err = decoder.Decode(&plainRuleSet) + if err != nil { + return err + } + ruleSet := plainRuleSet.Upgrade() + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(ruleSet) + if err != nil { + return E.Cause(err, "encode config") + } + outputPath, _ := filepath.Abs(sourcePath) + if !commandRuleSetFormatFlagWrite || sourcePath == "stdin" { + os.Stdout.WriteString(buffer.String() + "\n") + return nil + } + if bytes.Equal(content, buffer.Bytes()) { + return nil + } + output, err := os.Create(sourcePath) + if err != nil { + return E.Cause(err, "open output") + } + _, err = output.Write(buffer.Bytes()) + output.Close() + if err != nil { + return E.Cause(err, "write output") + } + os.Stderr.WriteString(outputPath + "\n") + return nil +} diff --git a/cmd/sing-box/cmd_tools.go b/cmd/sing-box/cmd_tools.go index 460a50cd1c..c45f585576 100644 --- a/cmd/sing-box/cmd_tools.go +++ b/cmd/sing-box/cmd_tools.go @@ -38,11 +38,7 @@ func createPreStartedClient() (*box.Box, error) { func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) { if outboundTag == "" { - outbound := instance.Router().DefaultOutbound(N.NetworkName(network)) - if outbound == nil { - return nil, E.New("missing default outbound") - } - return outbound, nil + return instance.Router().DefaultOutbound(N.NetworkName(network)) } else { outbound, loaded := instance.Router().Outbound(outboundTag) if !loaded { diff --git a/cmd/sing-box/cmd_tools_connect.go b/cmd/sing-box/cmd_tools_connect.go index b904ebc9f5..3ea04bcd40 100644 --- a/cmd/sing-box/cmd_tools_connect.go +++ b/cmd/sing-box/cmd_tools_connect.go @@ -18,7 +18,7 @@ import ( var commandConnectFlagNetwork string var commandConnect = &cobra.Command{ - Use: "connect [address]", + Use: "connect
", Short: "Connect to an address", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { diff --git a/common/dialer/router.go b/common/dialer/router.go index 1d5586546e..2531607753 100644 --- a/common/dialer/router.go +++ b/common/dialer/router.go @@ -18,11 +18,19 @@ func NewRouter(router adapter.Router) N.Dialer { } func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - return d.router.DefaultOutbound(network).DialContext(ctx, network, destination) + dialer, err := d.router.DefaultOutbound(network) + if err != nil { + return nil, err + } + return dialer.DialContext(ctx, network, destination) } func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return d.router.DefaultOutbound(N.NetworkUDP).ListenPacket(ctx, destination) + dialer, err := d.router.DefaultOutbound(N.NetworkUDP) + if err != nil { + return nil, err + } + return dialer.ListenPacket(ctx, destination) } func (d *RouterDialer) Upstream() any { diff --git a/common/srs/binary.go b/common/srs/binary.go new file mode 100644 index 0000000000..dd994c2c29 --- /dev/null +++ b/common/srs/binary.go @@ -0,0 +1,485 @@ +package srs + +import ( + "compress/zlib" + "encoding/binary" + "io" + "net/netip" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/rw" + + "go4.org/netipx" +) + +var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS + +const ( + ruleItemQueryType uint8 = iota + ruleItemNetwork + ruleItemDomain + ruleItemDomainKeyword + ruleItemDomainRegex + ruleItemSourceIPCIDR + ruleItemIPCIDR + ruleItemSourcePort + ruleItemSourcePortRange + ruleItemPort + ruleItemPortRange + ruleItemProcessName + ruleItemProcessPath + ruleItemPackageName + ruleItemWIFISSID + ruleItemWIFIBSSID + ruleItemFinal uint8 = 0xFF +) + +func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) { + var magicBytes [3]byte + _, err = io.ReadFull(reader, magicBytes[:]) + if err != nil { + return + } + if magicBytes != MagicBytes { + err = E.New("invalid sing-box rule set file") + return + } + var version uint8 + err = binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return ruleSet, err + } + if version != 1 { + return ruleSet, E.New("unsupported version: ", version) + } + zReader, err := zlib.NewReader(reader) + if err != nil { + return + } + length, err := rw.ReadUVariant(zReader) + if err != nil { + return + } + ruleSet.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + ruleSet.Rules[i], err = readRule(zReader, recovery) + if err != nil { + err = E.Cause(err, "read rule[", i, "]") + return + } + } + return +} + +func Write(writer io.Writer, ruleSet option.PlainRuleSet) error { + _, err := writer.Write(MagicBytes[:]) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) + if err != nil { + return err + } + err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules))) + if err != nil { + return err + } + for _, rule := range ruleSet.Rules { + err = writeRule(zWriter, rule) + if err != nil { + return err + } + } + return zWriter.Close() +} + +func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) { + var ruleType uint8 + err = binary.Read(reader, binary.BigEndian, &ruleType) + if err != nil { + return + } + switch ruleType { + case 0: + rule.DefaultOptions, err = readDefaultRule(reader, recovery) + case 1: + rule.LogicalOptions, err = readLogicalRule(reader, recovery) + default: + err = E.New("unknown rule type: ", ruleType) + } + return +} + +func writeRule(writer io.Writer, rule option.HeadlessRule) error { + switch rule.Type { + case C.RuleTypeDefault: + return writeDefaultRule(writer, rule.DefaultOptions) + case C.RuleTypeLogical: + return writeLogicalRule(writer, rule.LogicalOptions) + default: + panic("unknown rule type: " + rule.Type) + } +} + +func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) { + var lastItemType uint8 + for { + var itemType uint8 + err = binary.Read(reader, binary.BigEndian, &itemType) + if err != nil { + return + } + switch itemType { + case ruleItemQueryType: + var rawQueryType []uint16 + rawQueryType, err = readRuleItemUint16(reader) + if err != nil { + return + } + rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType { + return option.DNSQueryType(it) + }) + case ruleItemNetwork: + rule.Network, err = readRuleItemString(reader) + case ruleItemDomain: + var matcher *domain.Matcher + matcher, err = domain.ReadMatcher(reader) + if err != nil { + return + } + rule.DomainMatcher = matcher + case ruleItemDomainKeyword: + rule.DomainKeyword, err = readRuleItemString(reader) + case ruleItemDomainRegex: + rule.DomainRegex, err = readRuleItemString(reader) + case ruleItemSourceIPCIDR: + rule.SourceIPSet, err = readIPSet(reader) + if err != nil { + return + } + if recovery { + rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemIPCIDR: + rule.IPSet, err = readIPSet(reader) + if err != nil { + return + } + if recovery { + rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemSourcePort: + rule.SourcePort, err = readRuleItemUint16(reader) + case ruleItemSourcePortRange: + rule.SourcePortRange, err = readRuleItemString(reader) + case ruleItemPort: + rule.Port, err = readRuleItemUint16(reader) + case ruleItemPortRange: + rule.PortRange, err = readRuleItemString(reader) + case ruleItemProcessName: + rule.ProcessName, err = readRuleItemString(reader) + case ruleItemProcessPath: + rule.ProcessPath, err = readRuleItemString(reader) + case ruleItemPackageName: + rule.PackageName, err = readRuleItemString(reader) + case ruleItemWIFISSID: + rule.WIFISSID, err = readRuleItemString(reader) + case ruleItemWIFIBSSID: + rule.WIFIBSSID, err = readRuleItemString(reader) + case ruleItemFinal: + err = binary.Read(reader, binary.BigEndian, &rule.Invert) + return + default: + err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType) + } + if err != nil { + return + } + lastItemType = itemType + } +} + +func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error { + err := binary.Write(writer, binary.BigEndian, uint8(0)) + if err != nil { + return err + } + if len(rule.QueryType) > 0 { + err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 { + return uint16(it) + })) + if err != nil { + return err + } + } + if len(rule.Network) > 0 { + err = writeRuleItemString(writer, ruleItemNetwork, rule.Network) + if err != nil { + return err + } + } + if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 { + err = binary.Write(writer, binary.BigEndian, ruleItemDomain) + if err != nil { + return err + } + err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer) + if err != nil { + return err + } + } + if len(rule.DomainKeyword) > 0 { + err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword) + if err != nil { + return err + } + } + if len(rule.DomainRegex) > 0 { + err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex) + if err != nil { + return err + } + } + if len(rule.SourceIPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR) + if err != nil { + return E.Cause(err, "source_ipcidr") + } + } + if len(rule.IPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR) + if err != nil { + return E.Cause(err, "ipcidr") + } + } + if len(rule.SourcePort) > 0 { + err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort) + if err != nil { + return err + } + } + if len(rule.SourcePortRange) > 0 { + err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange) + if err != nil { + return err + } + } + if len(rule.Port) > 0 { + err = writeRuleItemUint16(writer, ruleItemPort, rule.Port) + if err != nil { + return err + } + } + if len(rule.PortRange) > 0 { + err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange) + if err != nil { + return err + } + } + if len(rule.ProcessName) > 0 { + err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName) + if err != nil { + return err + } + } + if len(rule.ProcessPath) > 0 { + err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath) + if err != nil { + return err + } + } + if len(rule.PackageName) > 0 { + err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName) + if err != nil { + return err + } + } + if len(rule.WIFISSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) + if err != nil { + return err + } + } + if len(rule.WIFIBSSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, ruleItemFinal) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, rule.Invert) + if err != nil { + return err + } + return nil +} + +func readRuleItemString(reader io.Reader) ([]string, error) { + length, err := rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + value := make([]string, length) + for i := uint64(0); i < length; i++ { + value[i], err = rw.ReadVString(reader) + if err != nil { + return nil, err + } + } + return value, nil +} + +func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error { + err := binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(value))) + if err != nil { + return err + } + for _, item := range value { + err = rw.WriteVString(writer, item) + if err != nil { + return err + } + } + return nil +} + +func readRuleItemUint16(reader io.Reader) ([]uint16, error) { + length, err := rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + value := make([]uint16, length) + for i := uint64(0); i < length; i++ { + err = binary.Read(reader, binary.BigEndian, &value[i]) + if err != nil { + return nil, err + } + } + return value, nil +} + +func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error { + err := binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(value))) + if err != nil { + return err + } + for _, item := range value { + err = binary.Write(writer, binary.BigEndian, item) + if err != nil { + return err + } + } + return nil +} + +func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error { + var builder netipx.IPSetBuilder + for i, prefixString := range value { + prefix, err := netip.ParsePrefix(prefixString) + if err == nil { + builder.AddPrefix(prefix) + continue + } + addr, addrErr := netip.ParseAddr(prefixString) + if addrErr == nil { + builder.Add(addr) + continue + } + return E.Cause(err, "parse [", i, "]") + } + ipSet, err := builder.IPSet() + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + return writeIPSet(writer, ipSet) +} + +func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { + var mode uint8 + err = binary.Read(reader, binary.BigEndian, &mode) + if err != nil { + return + } + switch mode { + case 0: + logicalRule.Mode = C.LogicalTypeAnd + case 1: + logicalRule.Mode = C.LogicalTypeOr + default: + err = E.New("unknown logical mode: ", mode) + return + } + length, err := rw.ReadUVariant(reader) + if err != nil { + return + } + logicalRule.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + logicalRule.Rules[i], err = readRule(reader, recovery) + if err != nil { + err = E.Cause(err, "read logical rule [", i, "]") + return + } + } + err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert) + if err != nil { + return + } + return +} + +func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + switch logicalRule.Mode { + case C.LogicalTypeAnd: + err = binary.Write(writer, binary.BigEndian, uint8(0)) + case C.LogicalTypeOr: + err = binary.Write(writer, binary.BigEndian, uint8(1)) + default: + panic("unknown logical mode: " + logicalRule.Mode) + } + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules))) + if err != nil { + return err + } + for _, rule := range logicalRule.Rules { + err = writeRule(writer, rule) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, logicalRule.Invert) + if err != nil { + return err + } + return nil +} diff --git a/common/srs/ip_set.go b/common/srs/ip_set.go new file mode 100644 index 0000000000..b346da26f6 --- /dev/null +++ b/common/srs/ip_set.go @@ -0,0 +1,116 @@ +package srs + +import ( + "encoding/binary" + "io" + "net/netip" + "unsafe" + + "github.com/sagernet/sing/common/rw" + + "go4.org/netipx" +) + +type myIPSet struct { + rr []myIPRange +} + +type myIPRange struct { + from netip.Addr + to netip.Addr +} + +func readIPSet(reader io.Reader) (*netipx.IPSet, error) { + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return nil, err + } + var length uint64 + err = binary.Read(reader, binary.BigEndian, &length) + if err != nil { + return nil, err + } + mySet := &myIPSet{ + rr: make([]myIPRange, length), + } + for i := uint64(0); i < length; i++ { + var ( + fromLen uint64 + toLen uint64 + fromAddr netip.Addr + toAddr netip.Addr + ) + fromLen, err = rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + fromBytes := make([]byte, fromLen) + _, err = io.ReadFull(reader, fromBytes) + if err != nil { + return nil, err + } + err = fromAddr.UnmarshalBinary(fromBytes) + if err != nil { + return nil, err + } + toLen, err = rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + toBytes := make([]byte, toLen) + _, err = io.ReadFull(reader, toBytes) + if err != nil { + return nil, err + } + err = toAddr.UnmarshalBinary(toBytes) + if err != nil { + return nil, err + } + mySet.rr[i] = myIPRange{fromAddr, toAddr} + } + return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil +} + +func writeIPSet(writer io.Writer, set *netipx.IPSet) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + mySet := (*myIPSet)(unsafe.Pointer(set)) + err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr))) + if err != nil { + return err + } + for _, rr := range mySet.rr { + var ( + fromBinary []byte + toBinary []byte + ) + fromBinary, err = rr.from.MarshalBinary() + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(fromBinary))) + if err != nil { + return err + } + _, err = writer.Write(fromBinary) + if err != nil { + return err + } + toBinary, err = rr.to.MarshalBinary() + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(toBinary))) + if err != nil { + return err + } + _, err = writer.Write(toBinary) + if err != nil { + return err + } + } + return nil +} diff --git a/constant/rule.go b/constant/rule.go index 3c741995f8..5a8eaf127f 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -9,3 +9,11 @@ const ( LogicalTypeAnd = "and" LogicalTypeOr = "or" ) + +const ( + RuleSetTypeLocal = "local" + RuleSetTypeRemote = "remote" + RuleSetVersion1 = 1 + RuleSetFormatSource = "source" + RuleSetFormatBinary = "binary" +) diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index 2616ded5c1..001390d391 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -22,11 +22,13 @@ var ( bucketSelected = []byte("selected") bucketExpand = []byte("group_expand") bucketMode = []byte("clash_mode") + bucketRuleSet = []byte("rule_set") bucketNameList = []string{ string(bucketSelected), string(bucketExpand), string(bucketMode), + string(bucketRuleSet), } cacheIDDefault = []byte("default") @@ -257,3 +259,36 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { } }) } + +func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedRuleSet { + var savedSet adapter.SavedRuleSet + err := c.DB.View(func(t *bbolt.Tx) error { + bucket := c.bucket(t, bucketRuleSet) + if bucket == nil { + return os.ErrNotExist + } + setBinary := bucket.Get([]byte(tag)) + if len(setBinary) == 0 { + return os.ErrInvalid + } + return savedSet.UnmarshalBinary(setBinary) + }) + if err != nil { + return nil + } + return &savedSet +} + +func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedRuleSet) error { + return c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := c.createBucket(t, bucketRuleSet) + if err != nil { + return err + } + setBinary, err := set.MarshalBinary() + if err != nil { + return err + } + return bucket.Put([]byte(tag), setBinary) + }) +} diff --git a/experimental/cachefile/fakeip.go b/experimental/cachefile/fakeip.go index 2242342a36..e998ebb859 100644 --- a/experimental/cachefile/fakeip.go +++ b/experimental/cachefile/fakeip.go @@ -25,7 +25,7 @@ func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata { err := c.DB.Batch(func(tx *bbolt.Tx) error { bucket := tx.Bucket(bucketFakeIP) if bucket == nil { - return nil + return os.ErrNotExist } metadataBinary := bucket.Get(keyMetadata) if len(metadataBinary) == 0 { diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go index 050efd8d17..cf96931a85 100644 --- a/experimental/clashapi/proxies.go +++ b/experimental/clashapi/proxies.go @@ -100,8 +100,10 @@ func getProxies(server *Server, router adapter.Router) func(w http.ResponseWrite allProxies = append(allProxies, detour.Tag()) } - defaultTag := router.DefaultOutbound(N.NetworkTCP).Tag() - if defaultTag == "" { + var defaultTag string + if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + defaultTag = defaultOutbound.Tag() + } else { defaultTag = allProxies[0] } diff --git a/experimental/clashapi/server_resources.go b/experimental/clashapi/server_resources.go index ad36641e05..d6d22b5390 100644 --- a/experimental/clashapi/server_resources.go +++ b/experimental/clashapi/server_resources.go @@ -51,7 +51,11 @@ func (s *Server) downloadExternalUI() error { } detour = outbound } else { - detour = s.router.DefaultOutbound(N.NetworkTCP) + outbound, err := s.router.DefaultOutbound(N.NetworkTCP) + if err != nil { + return err + } + detour = outbound } httpClient := &http.Client{ Transport: &http.Transport{ diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go index 3dc5a367e7..b7c20eb075 100644 --- a/experimental/clashapi/trafficontrol/tracker.go +++ b/experimental/clashapi/trafficontrol/tracker.go @@ -94,7 +94,9 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad var chain []string var next string if rule == nil { - next = router.DefaultOutbound(N.NetworkTCP).Tag() + if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + next = defaultOutbound.Tag() + } } else { next = rule.Outbound() } @@ -181,7 +183,9 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route var chain []string var next string if rule == nil { - next = router.DefaultOutbound(N.NetworkUDP).Tag() + if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil { + next = defaultOutbound.Tag() + } } else { next = rule.Outbound() } diff --git a/go.mod b/go.mod index 7efe241ef6..095326d7f9 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 github.com/sagernet/quic-go v0.40.0 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc + github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be github.com/sagernet/sing-dns v0.1.11 github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 github.com/sagernet/sing-quic v0.1.5-0.20231123150216-00957d136203 diff --git a/go.sum b/go.sum index 4db37c0bfe..6d8d994a8c 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= -github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc h1:vESVuxHgbd2EzHxd+TYTpNACIEGBOhp5n3KG7bgbcws= -github.com/sagernet/sing v0.2.18-0.20231124125253-2dcabf4bfcbc/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= +github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be h1:FigAM9kq7RRXmHvgn8w2a8tqCY5CMV5GIk0id84dI0o= +github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE= github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE= github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM= diff --git a/option/experimental.go b/option/experimental.go index 72751a590e..c685f51f54 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -23,16 +23,16 @@ type ClashAPIOptions struct { DefaultMode string `json:"default_mode,omitempty"` ModeList []string `json:"-"` + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` // Deprecated: migrated to global cache file StoreMode bool `json:"store_mode,omitempty"` // Deprecated: migrated to global cache file StoreSelected bool `json:"store_selected,omitempty"` // Deprecated: migrated to global cache file StoreFakeIP bool `json:"store_fakeip,omitempty"` - // Deprecated: migrated to global cache file - CacheFile string `json:"cache_file,omitempty"` - // Deprecated: migrated to global cache file - CacheID string `json:"cache_id,omitempty"` } type V2RayAPIOptions struct { diff --git a/option/route.go b/option/route.go index 43150576e2..e313fcf242 100644 --- a/option/route.go +++ b/option/route.go @@ -4,6 +4,7 @@ type RouteOptions struct { GeoIP *GeoIPOptions `json:"geoip,omitempty"` Geosite *GeositeOptions `json:"geosite,omitempty"` Rules []Rule `json:"rules,omitempty"` + RuleSet []RuleSet `json:"rule_set,omitempty"` Final string `json:"final,omitempty"` FindProcess bool `json:"find_process,omitempty"` AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` diff --git a/option/rule.go b/option/rule.go index 4f4042025f..bad605a0fc 100644 --- a/option/rule.go +++ b/option/rule.go @@ -65,34 +65,36 @@ func (r Rule) IsValid() bool { } type DefaultRule struct { - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - GeoIP Listable[string] `json:"geoip,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - IPCIDR Listable[string] `json:"ip_cidr,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + Network Listable[string] `json:"network,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + Protocol Listable[string] `json:"protocol,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + Geosite Listable[string] `json:"geosite,omitempty"` + SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` + GeoIP Listable[string] `json:"geoip,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + User Listable[string] `json:"user,omitempty"` + UserID Listable[int32] `json:"user_id,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` } func (r DefaultRule) IsValid() bool { diff --git a/option/rule_dns.go b/option/rule_dns.go index fca3432293..c02d09f761 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -91,6 +91,7 @@ type DefaultDNSRule struct { ClashMode string `json:"clash_mode,omitempty"` WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` Invert bool `json:"invert,omitempty"` Server string `json:"server,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` diff --git a/option/rule_set.go b/option/rule_set.go new file mode 100644 index 0000000000..7a6f7e9282 --- /dev/null +++ b/option/rule_set.go @@ -0,0 +1,230 @@ +package option + +import ( + "reflect" + + "github.com/sagernet/sing-box/common/json" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + + "go4.org/netipx" +) + +type _RuleSet struct { + Type string `json:"type"` + Tag string `json:"tag"` + Format string `json:"format"` + LocalOptions LocalRuleSet `json:"-"` + RemoteOptions RemoteRuleSet `json:"-"` +} + +type RuleSet _RuleSet + +func (r RuleSet) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleSetTypeLocal: + v = r.LocalOptions + case C.RuleSetTypeRemote: + v = r.RemoteOptions + default: + return nil, E.New("unknown rule set type: " + r.Type) + } + return MarshallObjects((_RuleSet)(r), v) +} + +func (r *RuleSet) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_RuleSet)(r)) + if err != nil { + return err + } + if r.Tag == "" { + return E.New("missing rule_set.[].tag") + } + switch r.Format { + case "": + return E.New("missing rule_set.[].format") + case C.RuleSetFormatSource, C.RuleSetFormatBinary: + default: + return E.New("unknown rule set format: " + r.Format) + } + var v any + switch r.Type { + case C.RuleSetTypeLocal: + v = &r.LocalOptions + case C.RuleSetTypeRemote: + v = &r.RemoteOptions + case "": + return E.New("missing rule_set.[].type") + default: + return E.New("unknown rule set type: " + r.Type) + } + err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v) + if err != nil { + return E.Cause(err, "rule set") + } + return nil +} + +type LocalRuleSet struct { + Path string `json:"path,omitempty"` +} + +type RemoteRuleSet struct { + URL string `json:"url"` + DownloadDetour string `json:"download_detour,omitempty"` + UpdateInterval Duration `json:"update_interval,omitempty"` +} + +type _HeadlessRule struct { + Type string `json:"type,omitempty"` + DefaultOptions DefaultHeadlessRule `json:"-"` + LogicalOptions LogicalHeadlessRule `json:"-"` +} + +type HeadlessRule _HeadlessRule + +func (r HeadlessRule) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleTypeDefault: + r.Type = "" + v = r.DefaultOptions + case C.RuleTypeLogical: + v = r.LogicalOptions + default: + return nil, E.New("unknown rule type: " + r.Type) + } + return MarshallObjects((_HeadlessRule)(r), v) +} + +func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_HeadlessRule)(r)) + if err != nil { + return err + } + var v any + switch r.Type { + case "", C.RuleTypeDefault: + r.Type = C.RuleTypeDefault + v = &r.DefaultOptions + case C.RuleTypeLogical: + v = &r.LogicalOptions + default: + return E.New("unknown rule type: " + r.Type) + } + err = UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v) + if err != nil { + return E.Cause(err, "route rule-set rule") + } + return nil +} + +func (r HeadlessRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault, "": + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + +type DefaultHeadlessRule struct { + QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` + Network Listable[string] `json:"network,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + Invert bool `json:"invert,omitempty"` + + DomainMatcher *domain.Matcher `json:"-"` + SourceIPSet *netipx.IPSet `json:"-"` + IPSet *netipx.IPSet `json:"-"` +} + +func (r DefaultHeadlessRule) IsValid() bool { + var defaultValue DefaultHeadlessRule + defaultValue.Invert = r.Invert + return !reflect.DeepEqual(r, defaultValue) +} + +type LogicalHeadlessRule struct { + Mode string `json:"mode"` + Rules []HeadlessRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` +} + +func (r LogicalHeadlessRule) IsValid() bool { + return len(r.Rules) > 0 && common.All(r.Rules, HeadlessRule.IsValid) +} + +type _PlainRuleSetCompat struct { + Version int `json:"version"` + Options PlainRuleSet `json:"-"` +} + +type PlainRuleSetCompat _PlainRuleSetCompat + +func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { + var v any + switch r.Version { + case C.RuleSetVersion1: + v = r.Options + default: + return nil, E.New("unknown rule set version: ", r.Version) + } + return MarshallObjects((_PlainRuleSetCompat)(r), v) +} + +func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_PlainRuleSetCompat)(r)) + if err != nil { + return err + } + var v any + switch r.Version { + case C.RuleSetVersion1: + v = &r.Options + case 0: + return E.New("missing rule set version") + default: + return E.New("unknown rule set version: ", r.Version) + } + err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) + if err != nil { + return E.Cause(err, "rule set") + } + return nil +} + +func (r PlainRuleSetCompat) Upgrade() PlainRuleSet { + var result PlainRuleSet + switch r.Version { + case C.RuleSetVersion1: + result = r.Options + default: + panic("unknown rule set version: " + F.ToString(r.Version)) + } + return result +} + +type PlainRuleSet struct { + Rules []HeadlessRule `json:"rules,omitempty"` +} diff --git a/option/types.go b/option/types.go index f2fed66309..520c3503aa 100644 --- a/option/types.go +++ b/option/types.go @@ -174,6 +174,14 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error { type DNSQueryType uint16 +func (t DNSQueryType) String() string { + typeName, loaded := mDNS.TypeToString[uint16(t)] + if loaded { + return typeName + } + return F.ToString(uint16(t)) +} + func (t DNSQueryType) MarshalJSON() ([]byte, error) { typeName, loaded := mDNS.TypeToString[uint16(t)] if loaded { diff --git a/route/router.go b/route/router.go index b48d2346d9..967061952e 100644 --- a/route/router.go +++ b/route/router.go @@ -67,6 +67,8 @@ type Router struct { dnsClient *dns.Client defaultDomainStrategy dns.DomainStrategy dnsRules []adapter.DNSRule + ruleSets []adapter.RuleSet + ruleSetMap map[string]adapter.RuleSet defaultTransport dns.Transport transports []dns.Transport transportMap map[string]dns.Transport @@ -106,6 +108,7 @@ func NewRouter( outboundByTag: make(map[string]adapter.Outbound), rules: make([]adapter.Rule, 0, len(options.Rules)), dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)), + ruleSetMap: make(map[string]adapter.RuleSet), needGeoIPDatabase: hasRule(options.Rules, isGeoIPRule) || hasDNSRule(dnsOptions.Rules, isGeoIPDNSRule), needGeositeDatabase: hasRule(options.Rules, isGeositeRule) || hasDNSRule(dnsOptions.Rules, isGeositeDNSRule), geoIPOptions: common.PtrValueOrDefault(options.GeoIP), @@ -140,6 +143,14 @@ func NewRouter( } router.dnsRules = append(router.dnsRules, dnsRule) } + for i, ruleSetOptions := range options.RuleSet { + ruleSet, err := NewRuleSet(ctx, router, router.logger, ruleSetOptions) + if err != nil { + return nil, E.Cause(err, "parse rule-set[", i, "]") + } + router.ruleSets = append(router.ruleSets, ruleSet) + router.ruleSetMap[ruleSetOptions.Tag] = ruleSet + } transports := make([]dns.Transport, len(dnsOptions.Servers)) dummyTransportMap := make(map[string]dns.Transport) @@ -479,6 +490,12 @@ func (r *Router) Start() error { if r.needWIFIState { r.updateWIFIState() } + for i, ruleSet := range r.ruleSets { + err := ruleSet.Start() + if err != nil { + return E.Cause(err, "initialize rule-set[", i, "]") + } + } for i, rule := range r.rules { err := rule.Start() if err != nil { @@ -576,11 +593,17 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { return outbound, loaded } -func (r *Router) DefaultOutbound(network string) adapter.Outbound { +func (r *Router) DefaultOutbound(network string) (adapter.Outbound, error) { if network == N.NetworkTCP { - return r.defaultOutboundForConnection + if r.defaultOutboundForConnection == nil { + return nil, E.New("missing default outbound for TCP connections") + } + return r.defaultOutboundForConnection, nil } else { - return r.defaultOutboundForPacketConnection + if r.defaultOutboundForPacketConnection == nil { + return nil, E.New("missing default outbound for UDP connections") + } + return r.defaultOutboundForPacketConnection, nil } } @@ -588,6 +611,11 @@ func (r *Router) FakeIPStore() adapter.FakeIPStore { return r.fakeIPStore } +func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) { + ruleSet, loaded := r.ruleSetMap[tag] + return ruleSet, loaded +} + func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { diff --git a/route/rule_abstract.go b/route/rule_abstract.go index 38d4d57d41..312caaee73 100644 --- a/route/rule_abstract.go +++ b/route/rule_abstract.go @@ -1,6 +1,7 @@ package route import ( + "io" "strings" "github.com/sagernet/sing-box/adapter" @@ -135,7 +136,7 @@ func (r *abstractDefaultRule) String() string { } type abstractLogicalRule struct { - rules []adapter.Rule + rules []adapter.HeadlessRule mode string invert bool outbound string @@ -146,7 +147,10 @@ func (r *abstractLogicalRule) Type() string { } func (r *abstractLogicalRule) UpdateGeosite() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (adapter.Rule, bool) { + rule, loaded := it.(adapter.Rule) + return rule, loaded + }) { err := rule.UpdateGeosite() if err != nil { return err @@ -156,7 +160,10 @@ func (r *abstractLogicalRule) UpdateGeosite() error { } func (r *abstractLogicalRule) Start() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (common.Starter, bool) { + rule, loaded := it.(common.Starter) + return rule, loaded + }) { err := rule.Start() if err != nil { return err @@ -166,7 +173,10 @@ func (r *abstractLogicalRule) Start() error { } func (r *abstractLogicalRule) Close() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (io.Closer, bool) { + rule, loaded := it.(io.Closer) + return rule, loaded + }) { err := rule.Close() if err != nil { return err @@ -177,11 +187,11 @@ func (r *abstractLogicalRule) Close() error { func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { if r.mode == C.LogicalTypeAnd { - return common.All(r.rules, func(it adapter.Rule) bool { + return common.All(r.rules, func(it adapter.HeadlessRule) bool { return it.Match(metadata) }) != r.invert } else { - return common.Any(r.rules, func(it adapter.Rule) bool { + return common.Any(r.rules, func(it adapter.HeadlessRule) bool { return it.Match(metadata) }) != r.invert } diff --git a/route/rule_default.go b/route/rule_default.go index 8c8473abf5..c0ef9eef60 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -194,6 +194,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.RuleSet) > 0 { + item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } @@ -206,7 +211,7 @@ type LogicalRule struct { func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { r := &LogicalRule{ abstractLogicalRule{ - rules: make([]adapter.Rule, len(options.Rules)), + rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, outbound: options.Outbound, }, diff --git a/route/rule_dns.go b/route/rule_dns.go index b444932582..f5f9fd3583 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -190,6 +190,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.RuleSet) > 0 { + item := NewRuleSetItem(router, options.RuleSet, false) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } @@ -212,7 +217,7 @@ type LogicalDNSRule struct { func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { r := &LogicalDNSRule{ abstractLogicalRule: abstractLogicalRule{ - rules: make([]adapter.Rule, len(options.Rules)), + rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, outbound: options.Server, }, diff --git a/route/rule_headless.go b/route/rule_headless.go new file mode 100644 index 0000000000..9df2ee3036 --- /dev/null +++ b/route/rule_headless.go @@ -0,0 +1,173 @@ +package route + +import ( + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func NewHeadlessRule(router adapter.Router, options option.HeadlessRule) (adapter.HeadlessRule, error) { + switch options.Type { + case "", C.RuleTypeDefault: + if !options.DefaultOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewDefaultHeadlessRule(router, options.DefaultOptions) + case C.RuleTypeLogical: + if !options.LogicalOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewLogicalHeadlessRule(router, options.LogicalOptions) + default: + return nil, E.New("unknown rule type: ", options.Type) + } +} + +var _ adapter.HeadlessRule = (*DefaultHeadlessRule)(nil) + +type DefaultHeadlessRule struct { + abstractDefaultRule +} + +func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) { + rule := &DefaultHeadlessRule{ + abstractDefaultRule{ + invert: options.Invert, + }, + } + if len(options.Network) > 0 { + item := NewNetworkItem(options.Network) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 { + item := NewDomainItem(options.Domain, options.DomainSuffix) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.DomainMatcher != nil { + item := NewRawDomainItem(options.DomainMatcher) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainKeyword) > 0 { + item := NewDomainKeywordItem(options.DomainKeyword) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainRegex) > 0 { + item, err := NewDomainRegexItem(options.DomainRegex) + if err != nil { + return nil, E.Cause(err, "domain_regex") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourceIPCIDR) > 0 { + item, err := NewIPCIDRItem(true, options.SourceIPCIDR) + if err != nil { + return nil, E.Cause(err, "source_ipcidr") + } + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.SourceIPSet != nil { + item := NewRawIPCIDRItem(true, options.SourceIPSet) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.IPCIDR) > 0 { + item, err := NewIPCIDRItem(false, options.IPCIDR) + if err != nil { + return nil, E.Cause(err, "ipcidr") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.IPSet != nil { + item := NewRawIPCIDRItem(false, options.IPSet) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePort) > 0 { + item := NewPortItem(true, options.SourcePort) + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePortRange) > 0 { + item, err := NewPortRangeItem(true, options.SourcePortRange) + if err != nil { + return nil, E.Cause(err, "source_port_range") + } + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Port) > 0 { + item := NewPortItem(false, options.Port) + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PortRange) > 0 { + item, err := NewPortRangeItem(false, options.PortRange) + if err != nil { + return nil, E.Cause(err, "port_range") + } + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessName) > 0 { + item := NewProcessItem(options.ProcessName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessPath) > 0 { + item := NewProcessPathItem(options.ProcessPath) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PackageName) > 0 { + item := NewPackageNameItem(options.PackageName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(router, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFIBSSID) > 0 { + item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + return rule, nil +} + +var _ adapter.HeadlessRule = (*LogicalHeadlessRule)(nil) + +type LogicalHeadlessRule struct { + abstractLogicalRule +} + +func NewLogicalHeadlessRule(router adapter.Router, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) { + r := &LogicalHeadlessRule{ + abstractLogicalRule{ + rules: make([]adapter.HeadlessRule, len(options.Rules)), + invert: options.Invert, + }, + } + switch options.Mode { + case C.LogicalTypeAnd: + r.mode = C.LogicalTypeAnd + case C.LogicalTypeOr: + r.mode = C.LogicalTypeOr + default: + return nil, E.New("unknown logical mode: ", options.Mode) + } + for i, subRule := range options.Rules { + rule, err := NewHeadlessRule(router, subRule) + if err != nil { + return nil, E.Cause(err, "sub rule[", i, "]") + } + r.rules[i] = rule + } + return r, nil +} diff --git a/route/rule_item_cidr.go b/route/rule_item_cidr.go index b72d1e10b1..e17d87def8 100644 --- a/route/rule_item_cidr.go +++ b/route/rule_item_cidr.go @@ -31,7 +31,7 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { builder.Add(addr) continue } - return nil, E.Cause(err, "parse ip_cidr [", i, "]") + return nil, E.Cause(err, "parse [", i, "]") } var description string if isSource { @@ -57,8 +57,23 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { }, nil } +func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem { + var description string + if isSource { + description = "source_ipcidr=" + } else { + description = "ipcidr=" + } + description += "" + return &IPCIDRItem{ + ipSet: ipSet, + isSource: isSource, + description: description, + } +} + func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool { - if r.isSource { + if r.isSource || metadata.QueryType != 0 || metadata.IPCIDRMatchSource { return r.ipSet.Contains(metadata.Source.Addr) } else { if metadata.Destination.IsIP() { diff --git a/route/rule_item_domain.go b/route/rule_item_domain.go index 6602441deb..d2a11181b0 100644 --- a/route/rule_item_domain.go +++ b/route/rule_item_domain.go @@ -43,6 +43,13 @@ func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem { } } +func NewRawDomainItem(matcher *domain.Matcher) *DomainItem { + return &DomainItem{ + matcher, + "domain/domain_suffix=", + } +} + func (r *DomainItem) Match(metadata *adapter.InboundContext) bool { var domainHost string if metadata.Domain != "" { diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go new file mode 100644 index 0000000000..959b2f6110 --- /dev/null +++ b/route/rule_item_rule_set.go @@ -0,0 +1,55 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*RuleSetItem)(nil) + +type RuleSetItem struct { + router adapter.Router + tagList []string + setList []adapter.HeadlessRule + ipcidrMatchSource bool +} + +func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool) *RuleSetItem { + return &RuleSetItem{ + router: router, + tagList: tagList, + ipcidrMatchSource: ipCIDRMatchSource, + } +} + +func (r *RuleSetItem) Start() error { + for _, tag := range r.tagList { + ruleSet, loaded := r.router.RuleSet(tag) + if !loaded { + return E.New("rule-set not found: ", tag) + } + r.setList = append(r.setList, ruleSet) + } + return nil +} + +func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { + metadata.IPCIDRMatchSource = r.ipcidrMatchSource + for _, ruleSet := range r.setList { + if ruleSet.Match(metadata) { + return true + } + } + return false +} + +func (r *RuleSetItem) String() string { + if len(r.tagList) == 1 { + return F.ToString("rule_set=", r.tagList[0]) + } else { + return F.ToString("rule_set=[", strings.Join(r.tagList, " "), "]") + } +} diff --git a/route/rule_set.go b/route/rule_set.go new file mode 100644 index 0000000000..76c78c6214 --- /dev/null +++ b/route/rule_set.go @@ -0,0 +1,22 @@ +package route + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + 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" +) + +func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { + switch options.Type { + case C.RuleSetTypeLocal: + return NewLocalRuleSet(router, options) + case C.RuleSetTypeRemote: + return NewRemoteRuleSet(ctx, router, logger, options), nil + default: + return nil, E.New("unknown rule set type: ", options.Type) + } +} diff --git a/route/rule_set_local.go b/route/rule_set_local.go new file mode 100644 index 0000000000..ccdb1704a8 --- /dev/null +++ b/route/rule_set_local.go @@ -0,0 +1,69 @@ +package route + +import ( + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ adapter.RuleSet = (*LocalRuleSet)(nil) + +type LocalRuleSet struct { + rules []adapter.HeadlessRule +} + +func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { + setFile, err := os.Open(options.LocalOptions.Path) + if err != nil { + return nil, err + } + var plainRuleSet option.PlainRuleSet + switch options.Format { + case C.RuleSetFormatSource, "": + var compat option.PlainRuleSetCompat + decoder := json.NewDecoder(json.NewCommentFilter(setFile)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&compat) + if err != nil { + return nil, err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + plainRuleSet, err = srs.Read(setFile, false) + if err != nil { + return nil, err + } + default: + return nil, E.New("unknown rule set format: ", options.Format) + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(router, ruleOptions) + if err != nil { + return nil, E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + return &LocalRuleSet{rules}, nil +} + +func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} + +func (s *LocalRuleSet) Start() error { + return nil +} + +func (s *LocalRuleSet) Close() error { + return nil +} diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go new file mode 100644 index 0000000000..2092ed7466 --- /dev/null +++ b/route/rule_set_remote.go @@ -0,0 +1,218 @@ +package route + +import ( + "bytes" + "context" + "io" + "net" + "net/http" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + 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" + "github.com/sagernet/sing/service" +) + +var _ adapter.RuleSet = (*RemoteRuleSet)(nil) + +type RemoteRuleSet struct { + ctx context.Context + cancel context.CancelFunc + router adapter.Router + logger logger.ContextLogger + options option.RuleSet + updateInterval time.Duration + dialer N.Dialer + rules []adapter.HeadlessRule + lastUpdated time.Time + lastEtag string + updateTicker *time.Ticker +} + +func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { + ctx, cancel := context.WithCancel(ctx) + var updateInterval time.Duration + if options.RemoteOptions.UpdateInterval > 0 { + updateInterval = time.Duration(options.RemoteOptions.UpdateInterval) + } else { + updateInterval = 24 * time.Hour + } + return &RemoteRuleSet{ + ctx: ctx, + cancel: cancel, + router: router, + logger: logger, + options: options, + updateInterval: updateInterval, + } +} + +func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} + +func (s *RemoteRuleSet) Start() error { + var dialer N.Dialer + if s.options.RemoteOptions.DownloadDetour != "" { + outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour) + if !loaded { + return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour) + } + dialer = outbound + } else { + outbound, err := s.router.DefaultOutbound(N.NetworkTCP) + if err != nil { + return err + } + dialer = outbound + } + s.dialer = dialer + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + if savedSet := cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil { + err := s.loadBytes(savedSet.Content) + if err != nil { + return E.Cause(err, "restore cached rule-set") + } + s.lastUpdated = savedSet.LastUpdated + s.lastEtag = savedSet.LastEtag + } + } + if s.lastUpdated.IsZero() || time.Since(s.lastUpdated) > s.updateInterval { + err := s.fetchOnce() + if err != nil { + return E.Cause(err, "fetch rule-set ", s.options.Tag) + } + } + s.updateTicker = time.NewTicker(s.updateInterval) + go s.loopUpdate() + return nil +} + +func (s *RemoteRuleSet) loadBytes(content []byte) error { + var ( + plainRuleSet option.PlainRuleSet + err error + ) + switch s.options.Format { + case C.RuleSetFormatSource, "": + var compat option.PlainRuleSetCompat + decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content))) + decoder.DisallowUnknownFields() + err = decoder.Decode(&compat) + if err != nil { + return err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + plainRuleSet, err = srs.Read(bytes.NewReader(content), false) + if err != nil { + return err + } + default: + return E.New("unknown rule set format: ", s.options.Format) + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(s.router, ruleOptions) + if err != nil { + return E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + s.rules = rules + return nil +} + +func (s *RemoteRuleSet) loopUpdate() { + for { + select { + case <-s.ctx.Done(): + return + case <-s.updateTicker.C: + err := s.fetchOnce() + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } + } + } +} + +func (s *RemoteRuleSet) fetchOnce() error { + s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL) + httpClient := &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + }, + } + defer httpClient.CloseIdleConnections() + request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil) + if err != nil { + return err + } + if s.lastEtag != "" { + request.Header.Set("If-None-Match", s.lastEtag) + } + response, err := httpClient.Do(request.WithContext(s.ctx)) + if err != nil { + return err + } + switch response.StatusCode { + case http.StatusOK: + case http.StatusNotModified: + s.logger.Info("update rule-set ", s.options.Tag, ": not modified") + return nil + default: + return E.New("unexpected status: ", response.Status) + } + content, err := io.ReadAll(response.Body) + if err != nil { + response.Body.Close() + return err + } + err = s.loadBytes(content) + if err != nil { + response.Body.Close() + return err + } + response.Body.Close() + eTagHeader := response.Header.Get("Etag") + if eTagHeader != "" { + s.lastEtag = eTagHeader + } + s.lastUpdated = time.Now() + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err = cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedRuleSet{ + LastUpdated: s.lastUpdated, + Content: content, + LastEtag: s.lastEtag, + }) + if err != nil { + s.logger.Error("save rule-set cache: ", err) + } + } + s.logger.Info("updated rule-set ", s.options.Tag) + return nil +} + +func (s *RemoteRuleSet) Close() error { + s.updateTicker.Stop() + s.cancel() + return nil +} From d8fd51f5f13a5b1acad030c3935882a2014d5477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 29 Nov 2023 17:35:56 +0800 Subject: [PATCH 031/103] documentation: Add rule set --- docs/configuration/dns/rule.md | 32 ++- docs/configuration/dns/rule.zh.md | 32 ++- docs/configuration/experimental/cache-file.md | 34 +++ docs/configuration/experimental/clash-api.md | 121 ++++++++++ docs/configuration/experimental/index.md | 145 ++---------- docs/configuration/experimental/index.zh.md | 137 ------------ docs/configuration/experimental/v2ray-api.md | 50 +++++ docs/configuration/route/geoip.md | 8 + docs/configuration/route/geoip.zh.md | 33 --- docs/configuration/route/geosite.md | 8 + docs/configuration/route/geosite.zh.md | 33 --- docs/configuration/route/index.md | 30 ++- docs/configuration/route/index.zh.md | 32 ++- docs/configuration/route/rule.md | 46 +++- docs/configuration/route/rule.zh.md | 44 +++- docs/configuration/rule-set/headless-rule.md | 207 ++++++++++++++++++ docs/configuration/rule-set/index.md | 97 ++++++++ docs/configuration/rule-set/source-format.md | 34 +++ docs/migration.md | 187 ++++++++++++++++ mkdocs.yml | 28 ++- 20 files changed, 980 insertions(+), 358 deletions(-) create mode 100644 docs/configuration/experimental/cache-file.md create mode 100644 docs/configuration/experimental/clash-api.md delete mode 100644 docs/configuration/experimental/index.zh.md create mode 100644 docs/configuration/experimental/v2ray-api.md delete mode 100644 docs/configuration/route/geoip.zh.md delete mode 100644 docs/configuration/route/geosite.zh.md create mode 100644 docs/configuration/rule-set/headless-rule.md create mode 100644 docs/configuration/rule-set/index.md create mode 100644 docs/configuration/rule-set/source-format.md create mode 100644 docs/migration.md diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 18e352b407..896a3b4468 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -1,3 +1,13 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -85,6 +95,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": [ "direct" @@ -166,15 +180,23 @@ Match domain using regular expression. #### geosite +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + Match geosite. #### source_geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + Match source geoip. #### source_ip_cidr -Match source ip cidr. +Match source IP CIDR. #### source_port @@ -250,6 +272,12 @@ Match WiFi SSID. Match WiFi BSSID. +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [Rule Set](/configuration/route/#rule_set). + #### invert Invert match result. @@ -286,4 +314,4 @@ Rewrite TTL in DNS responses. #### rules -Included default rules. \ No newline at end of file +Included rules. \ No newline at end of file diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 98bfa8ab9a..f990ed3ec5 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -1,3 +1,13 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -84,6 +94,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": [ "direct" @@ -163,10 +177,18 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### geosite -匹配 GeoSite。 +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-set)。 + +匹配 Geosite。 #### source_geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + 匹配源 GeoIP。 #### source_ip_cidr @@ -245,6 +267,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配 WiFi BSSID。 +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + #### invert 反选匹配结果。 @@ -281,4 +309,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### rules -包括的默认规则。 \ No newline at end of file +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md new file mode 100644 index 0000000000..66e30ef9b0 --- /dev/null +++ b/docs/configuration/experimental/cache-file.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "enabled": true, + "path": "", + "cache_id": "", + "store_fakeip": false +} +``` + +### Fields + +#### enabled + +Enable cache file. + +#### path + +Path to the cache file. + +`cache.db` will be used if empty. + +#### cache_id + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. diff --git a/docs/configuration/experimental/clash-api.md b/docs/configuration/experimental/clash-api.md new file mode 100644 index 0000000000..a06fe15480 --- /dev/null +++ b/docs/configuration/experimental/clash-api.md @@ -0,0 +1,121 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-delete-alert: [store_mode](#store_mode) + :material-delete-alert: [store_selected](#store_selected) + :material-delete-alert: [store_fakeip](#store_fakeip) + :material-delete-alert: [cache_file](#cache_file) + :material-delete-alert: [cache_id](#cache_id) + + +!!! quote "" + + Clash API is not included by default, see [Installation](./#installation). + +### Structure + +```json +{ + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" +} +``` + +### Fields + +#### external_controller + +RESTful web API listening address. Clash API will be disabled if empty. + +#### external_ui + +A relative path to the configuration directory or an absolute path to a +directory in which you put some static web resource. sing-box will then +serve it at `http://{{external-controller}}/ui`. + + + +#### external_ui_download_url + +ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. + +`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. + +#### external_ui_download_detour + +The tag of the outbound to download the external UI. + +Default outbound will be used if empty. + +#### secret + +Secret for the RESTful API (optional) +Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` +ALWAYS set a secret if RESTful API is listening on 0.0.0.0 + +#### default_mode + +Default mode in clash, `Rule` will be used if empty. + +This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. + +#### store_mode + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_mode` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +Store Clash mode in cache file. + +#### store_selected + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +!!! note "" + + The tag must be set for target outbounds. + +Store selected outbound for the `Selector` outbound in cache file. + +#### store_fakeip + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and migrated to `cache_file.store_fakeip`. + +Store fakeip in cache file. + +#### cache_file + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_file` is deprecated in Clash API and migrated to `cache_file.enabled` and `cache_file.path`. + +Cache file path, `cache.db` will be used if empty. + +#### cache_id + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_id` is deprecated in Clash API and migrated to `cache_file.cache_id`. + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. \ No newline at end of file diff --git a/docs/configuration/experimental/index.md b/docs/configuration/experimental/index.md index 308e851c3b..1057e59b36 100644 --- a/docs/configuration/experimental/index.md +++ b/docs/configuration/experimental/index.md @@ -1,139 +1,30 @@ +--- +icon: material/alert-decagram +--- + # Experimental +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [cache_file](#cache_file) + :material-alert-decagram: [clash_api](#clash_api) + ### Structure ```json { "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" - }, - "v2ray_api": { - "listen": "127.0.0.1:8080", - "stats": { - "enabled": true, - "inbounds": [ - "socks-in" - ], - "outbounds": [ - "proxy", - "direct" - ], - "users": [ - "sekai" - ] - } - } + "cache_file": {}, + "clash_api": {}, + "v2ray_api": {} } } ``` -!!! note "" - - Traffic statistics and connection management can degrade performance. - -### Clash API Fields - -!!! quote "" - - Clash API is not included by default, see [Installation](./#installation). - -#### external_controller - -RESTful web API listening address. Clash API will be disabled if empty. - -#### external_ui - -A relative path to the configuration directory or an absolute path to a -directory in which you put some static web resource. sing-box will then -serve it at `http://{{external-controller}}/ui`. - -#### external_ui_download_url - -ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. - -`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. - -#### external_ui_download_detour - -The tag of the outbound to download the external UI. - -Default outbound will be used if empty. - -#### secret - -Secret for the RESTful API (optional) -Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` -ALWAYS set a secret if RESTful API is listening on 0.0.0.0 - -#### default_mode - -Default mode in clash, `Rule` will be used if empty. - -This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. - -#### store_mode - -Store Clash mode in cache file. - -#### store_selected - -!!! note "" - - The tag must be set for target outbounds. - -Store selected outbound for the `Selector` outbound in cache file. - -#### store_fakeip - -Store fakeip in cache file. - -#### cache_file - -Cache file path, `cache.db` will be used if empty. - -#### cache_id - -Cache ID. - -If not empty, `store_selected` will use a separate store keyed by it. - -### V2Ray API Fields - -!!! quote "" - - V2Ray API is not included by default, see [Installation](./#installation). - -#### listen - -gRPC API listening address. V2Ray API will be disabled if empty. - -#### stats - -Traffic statistics service settings. - -#### stats.enabled - -Enable statistics service. - -#### stats.inbounds - -Inbound list to count traffic. - -#### stats.outbounds - -Outbound list to count traffic. - -#### stats.users +### Fields -User list to count traffic. \ No newline at end of file +| Key | Format | +|--------------|----------------------------| +| `cache_file` | [Cache File](./cache-file) | +| `clash_api` | [Clash API](./clash-api) | +| `v2ray_api` | [V2Ray API](./v2ray-api) | \ No newline at end of file diff --git a/docs/configuration/experimental/index.zh.md b/docs/configuration/experimental/index.zh.md deleted file mode 100644 index 88a95852c9..0000000000 --- a/docs/configuration/experimental/index.zh.md +++ /dev/null @@ -1,137 +0,0 @@ -# 实验性 - -### 结构 - -```json -{ - "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" - }, - "v2ray_api": { - "listen": "127.0.0.1:8080", - "stats": { - "enabled": true, - "inbounds": [ - "socks-in" - ], - "outbounds": [ - "proxy", - "direct" - ], - "users": [ - "sekai" - ] - } - } - } -} -``` - -!!! note "" - - 流量统计和连接管理会降低性能。 - -### Clash API 字段 - -!!! quote "" - - 默认安装不包含 Clash API,参阅 [安装](/zh/#_2)。 - -#### external_controller - -RESTful web API 监听地址。如果为空,则禁用 Clash API。 - -#### external_ui - -到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。 - -#### external_ui_download_url - -静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。 - -默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。 - -#### external_ui_download_detour - -用于下载静态网页资源的出站的标签。 - -如果为空,将使用默认出站。 - -#### secret - -RESTful API 的密钥(可选) -通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证 -如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。 - -#### default_mode - -Clash 中的默认模式,默认使用 `Rule`。 - -此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。 - -#### store_mode - -将 Clash 模式存储在缓存文件中。 - -#### store_selected - -!!! note "" - - 必须为目标出站设置标签。 - -将 `Selector` 中出站的选定的目标出站存储在缓存文件中。 - -#### store_fakeip - -将 fakeip 存储在缓存文件中。 - -#### cache_file - -缓存文件路径,默认使用`cache.db`。 - -#### cache_id - -缓存 ID。 - -如果不为空,`store_selected` 将会使用以此为键的独立存储。 - -### V2Ray API 字段 - -!!! quote "" - - 默认安装不包含 V2Ray API,参阅 [安装](/zh/#_2)。 - -#### listen - -gRPC API 监听地址。如果为空,则禁用 V2Ray API。 - -#### stats - -流量统计服务设置。 - -#### stats.enabled - -启用统计服务。 - -#### stats.inbounds - -统计流量的入站列表。 - -#### stats.outbounds - -统计流量的出站列表。 - -#### stats.users - -统计流量的用户列表。 \ No newline at end of file diff --git a/docs/configuration/experimental/v2ray-api.md b/docs/configuration/experimental/v2ray-api.md new file mode 100644 index 0000000000..398884242e --- /dev/null +++ b/docs/configuration/experimental/v2ray-api.md @@ -0,0 +1,50 @@ +### Structure + +!!! quote "" + + V2Ray API is not included by default, see [Installation](./#installation). + +```json +{ + "listen": "127.0.0.1:8080", + "stats": { + "enabled": true, + "inbounds": [ + "socks-in" + ], + "outbounds": [ + "proxy", + "direct" + ], + "users": [ + "sekai" + ] + } +} +``` + +### Fields + +#### listen + +gRPC API listening address. V2Ray API will be disabled if empty. + +#### stats + +Traffic statistics service settings. + +#### stats.enabled + +Enable statistics service. + +#### stats.inbounds + +Inbound list to count traffic. + +#### stats.outbounds + +Outbound list to count traffic. + +#### stats.users + +User list to count traffic. \ No newline at end of file diff --git a/docs/configuration/route/geoip.md b/docs/configuration/route/geoip.md index b966a292a5..d6dfd23289 100644 --- a/docs/configuration/route/geoip.md +++ b/docs/configuration/route/geoip.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + ### Structure ```json diff --git a/docs/configuration/route/geoip.zh.md b/docs/configuration/route/geoip.zh.md deleted file mode 100644 index 3ee7042728..0000000000 --- a/docs/configuration/route/geoip.zh.md +++ /dev/null @@ -1,33 +0,0 @@ -### 结构 - -```json -{ - "route": { - "geoip": { - "path": "", - "download_url": "", - "download_detour": "" - } - } -} -``` - -### 字段 - -#### path - -指定 GeoIP 资源的路径。 - -默认 `geoip.db`。 - -#### download_url - -指定 GeoIP 资源的下载链接。 - -默认为 `https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db`。 - -#### download_detour - -用于下载 GeoIP 资源的出站的标签。 - -如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/geosite.md b/docs/configuration/route/geosite.md index db700c6af1..515e86b1ec 100644 --- a/docs/configuration/route/geosite.md +++ b/docs/configuration/route/geosite.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + ### Structure ```json diff --git a/docs/configuration/route/geosite.zh.md b/docs/configuration/route/geosite.zh.md deleted file mode 100644 index bee81fbf71..0000000000 --- a/docs/configuration/route/geosite.zh.md +++ /dev/null @@ -1,33 +0,0 @@ -### 结构 - -```json -{ - "route": { - "geosite": { - "path": "", - "download_url": "", - "download_detour": "" - } - } -} -``` - -### 字段 - -#### path - -指定 GeoSite 资源的路径。 - -默认 `geosite.db`。 - -#### download_url - -指定 GeoSite 资源的下载链接。 - -默认为 `https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db`。 - -#### download_detour - -用于下载 GeoSite 资源的出站的标签。 - -如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 846d497330..7c1787eaea 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -1,5 +1,15 @@ +--- +icon: material/alert-decagram +--- + # Route +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -8,6 +18,7 @@ "geoip": {}, "geosite": {}, "rules": [], + "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, @@ -19,11 +30,20 @@ ### Fields -| Key | Format | -|------------|------------------------------------| -| `geoip` | [GeoIP](./geoip) | -| `geosite` | [Geosite](./geosite) | -| `rules` | List of [Route Rule](./rule) | +| Key | Format | +|-----------|----------------------| +| `geoip` | [GeoIP](./geoip) | +| `geosite` | [Geosite](./geosite) | + +#### rules + +List of [Route Rule](./rule) + +#### rule_set + +!!! question "Since sing-box 1.8.0" + +List of [Rule Set](/configuration/rule-set) #### final diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index 8bef5bea9a..b5302727a3 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -1,5 +1,15 @@ +--- +icon: material/alert-decagram +--- + # 路由 +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -7,8 +17,8 @@ "route": { "geoip": {}, "geosite": {}, - "ip_rules": [], "rules": [], + "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, @@ -20,11 +30,21 @@ ### 字段 -| 键 | 格式 | -|------------|-------------------------| -| `geoip` | [GeoIP](./geoip) | -| `geosite` | [GeoSite](./geosite) | -| `rules` | 一组 [路由规则](./rule) | +| 键 | 格式 | +|------------|-----------------------------------| +| `geoip` | [GeoIP](./geoip) | +| `geosite` | [Geosite](./geosite) | + + +#### rule + +一组 [路由规则](./rule)。 + +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +一组 [规则集](/configuration/rule-set)。 #### final diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index abbfde6fdc..35c1d8fc24 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -1,3 +1,15 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -89,6 +101,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": "direct" }, @@ -160,23 +176,35 @@ Match domain using regular expression. #### geosite +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + Match geosite. #### source_geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + Match source geoip. #### geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + Match geoip. #### source_ip_cidr -Match source ip cidr. +Match source IP CIDR. #### ip_cidr -Match ip cidr. +Match IP CIDR. #### source_port @@ -250,6 +278,18 @@ Match WiFi SSID. Match WiFi BSSID. +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [Rule Set](/configuration/route/#rule_set). + +#### rule_set_ipcidr_match_source + +!!! question "Since sing-box 1.8.0" + +Make `ipcidr` in rule sets match the source IP. + #### invert Invert match result. @@ -276,4 +316,4 @@ Tag of the target outbound. ==Required== -Included default rules. +Included rules. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index f4ab7890a7..31ee08d13a 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -1,3 +1,15 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -87,6 +99,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": "direct" }, @@ -158,14 +174,26 @@ #### geosite -匹配 GeoSite。 +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-set)。 + +匹配 Geosite。 #### source_geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + 匹配源 GeoIP。 #### geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + 匹配 GeoIP。 #### source_ip_cidr @@ -248,6 +276,18 @@ 匹配 WiFi BSSID。 +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + +#### rule_set_ipcidr_match_source + +!!! question "自 sing-box 1.8.0 起" + +使规则集中的 `ipcidr` 规则匹配源 IP。 + #### invert 反选匹配结果。 @@ -274,4 +314,4 @@ ==必填== -包括的默认规则。 \ No newline at end of file +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md new file mode 100644 index 0000000000..6ab62eb2e3 --- /dev/null +++ b/docs/configuration/rule-set/headless-rule.md @@ -0,0 +1,207 @@ +--- +icon: material/new-box +--- + +### Structure + +!!! question "Since sing-box 1.8.0" + +```json +{ + "rules": [ + { + "query_type": [ + "A", + "HTTPS", + 32768 + ], + "network": [ + "tcp" + ], + "domain": [ + "test.com" + ], + "domain_suffix": [ + ".cn" + ], + "domain_keyword": [ + "test" + ], + "domain_regex": [ + "^stun\\..+" + ], + "source_ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "source_port": [ + 12345 + ], + "source_port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "port": [ + 80, + 443 + ], + "port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "process_name": [ + "curl" + ], + "process_path": [ + "/usr/bin/curl" + ], + "package_name": [ + "com.termux" + ], + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], + "invert": false + }, + { + "type": "logical", + "mode": "and", + "rules": [], + "invert": false + } + ] +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Default Fields + +!!! note "" + + The default rule uses the following matching logic: + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) && + (`port` || `port_range`) && + (`source_port` || `source_port_range`) && + `other fields` + +#### query_type + +DNS query type. Values can be integers or type name strings. + +#### network + +`tcp` or `udp`. + +#### domain + +Match full domain. + +#### domain_suffix + +Match domain suffix. + +#### domain_keyword + +Match domain using keyword. + +#### domain_regex + +Match domain using regular expression. + +#### source_ip_cidr + +Match source IP CIDR. + +#### ip_cidr + +!!! info "" + + `ip_cidr` is an alias for `source_ip_cidr` when the Rule Set is used in DNS rules or `rule_set_ipcidr_match_source` enabled in route rules. + +Match IP CIDR. + +#### source_port + +Match source port. + +#### source_port_range + +Match source port range. + +#### port + +Match port. + +#### port_range + +Match port range. + +#### process_name + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process name. + +#### process_path + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path. + +#### package_name + +Match android package name. + +#### wifi_ssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi SSID. + +#### wifi_bssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi BSSID. + +#### invert + +Invert match result. + +### Logical Fields + +#### type + +`logical` + +#### mode + +==Required== + +`and` or `or` + +#### rules + +==Required== + +Included rules. diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md new file mode 100644 index 0000000000..5aff55b371 --- /dev/null +++ b/docs/configuration/rule-set/index.md @@ -0,0 +1,97 @@ +--- +icon: material/new-box +--- + +# Rule Set + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "type": "", + "tag": "", + "format": "", + + ... // Typed Fields +} +``` + +#### Local Structure + +```json +{ + "type": "local", + + ... + + "path": "" +} +``` + +#### Remote Structure + +!!! info "" + + Remote rule-set will be cached if `experimental.cache_file.enabled`. + +```json +{ + "type": "remote", + + ..., + + "url": "", + "download_detour": "", + "update_interval": "" +} +``` + +### Fields + +#### type + +==Required== + +Type of Rule Set, `local` or `remote`. + +#### tag + +==Required== + +Tag of Rule Set. + +#### format + +==Required== + +Format of Rule Set, `source` or `binary`. + +### Local Fields + +#### path + +==Required== + +File path of Rule Set. + +### Remote Fields + +#### url + +==Required== + +Download URL of Rule Set. + +#### download_detour + +Tag of the outbound to download rule-set. + +Default outbound will be used if empty. + +#### update_interval + +Update interval of Rule Set. + +`1d` will be used if empty. diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md new file mode 100644 index 0000000000..116c1ee66f --- /dev/null +++ b/docs/configuration/rule-set/source-format.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +# Source Format + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "version": 1, + "rules": [] +} +``` + +### Compile + +Use `sing-box rule-set compile [--output .srs] .json` to compile source to binary rule-set. + +### Fields + +#### version + +==Required== + +Version of Rule Set, must be `1`. + +#### rules + +==Required== + +List of [Headless Rule](./headless-rule.md). diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000000..8eb3daee42 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,187 @@ +--- +icon: material/arrange-bring-forward +--- + +# Migration + +## 1.8.0 + +!!! warning "Unstable" + + This version is still under development, and the following migration guide may be changed in the future. + +### :material-close-box: Migrate cache file from Clash API to independent options + +!!! info "Reference" + + [Clash API](/configuration/experimental/clash-api) / + [Cache File](/configuration/experimental/cache-file) + +=== ":material-card-remove: Deprecated" + + ```json + { + "experimental": { + "clash_api": { + "cache_file": "cache.db", // default value + "cahce_id": "my_profile2", + "store_mode": true, + "store_selected": true, + "store_fakeip": true + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "experimental" : { + "cache_file": { + "enabled": true, + "path": "cache.db", // default value + "cache_id": "my_profile2", + "store_fakeip": true + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate GeoIP to rule sets + +!!! info "Reference" + + [GeoIP](/configuration/route/geoip) / + [Route](/configuration/route) / + [Route Rule](/configuration/route/rule) / + [DNS Rule](/configuration/dns/rule) / + [Rule Set](/configuration/rule-set) + +!!! tip + + `sing-box geoip` commands can help you convert custom GeoIP into rule sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geoip": "cn", + "outbound": "direct" + }, + { + "source_geoip": "cn", + "outbound": "block" + } + ], + "geoip": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geoip-cn", + "outbound": "direct" + }, + { + "rule_set": "geoip-us", + "rule_set_ipcidr_match_source": true, + "outbound": "block" + } + ], + "rule_set": [ + { + "tag": "geoip-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", + "download_detour": "proxy" + }, + { + "tag": "geoip-us", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate Geosite to rule sets + +!!! info "Reference" + + [Geosite](/configuration/route/geosite) / + [Route](/configuration/route) / + [Route Rule](/configuration/route/rule) / + [DNS Rule](/configuration/dns/rule) / + [Rule Set](/configuration/rule-set) + +!!! tip + + `sing-box geosite` commands can help you convert custom Geosite into rule sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geosite": "cn", + "outbound": "direct" + } + ], + "geosite": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geosite-cn", + "outbound": "direct" + } + ], + "rule_set": [ + { + "tag": "geosite-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 1d4b1d8b0e..c5dd7df335 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,12 +32,16 @@ theme: - content.code.copy - content.code.select - content.code.annotate + icon: + admonition: + question: material/new-box nav: - Home: - index.md + - Change Log: changelog.md + - Migration: migration.md - Deprecated: deprecated.md - Support: support.md - - Change Log: changelog.md - Installation: - Package Manager: installation/package-manager.md - Docker: installation/docker.md @@ -56,7 +60,7 @@ nav: - Proxy: - Server: manual/proxy/server.md - Client: manual/proxy/client.md -# - TUN: manual/proxy/tun.md + # - TUN: manual/proxy/tun.md - Proxy Protocol: - Shadowsocks: manual/proxy-protocol/shadowsocks.md - Trojan: manual/proxy-protocol/trojan.md @@ -79,8 +83,15 @@ nav: - Geosite: configuration/route/geosite.md - Route Rule: configuration/route/rule.md - Protocol Sniff: configuration/route/sniff.md + - Rule Set: + - configuration/rule-set/index.md + - Source Format: configuration/rule-set/source-format.md + - Headless Rule: configuration/rule-set/headless-rule.md - Experimental: - configuration/experimental/index.md + - Cache File: configuration/experimental/cache-file.md + - Clash API: configuration/experimental/clash-api.md + - V2Ray API: configuration/experimental/v2ray-api.md - Shared: - Listen Fields: configuration/shared/listen.md - Dial Fields: configuration/shared/dial.md @@ -180,9 +191,10 @@ plugins: name: 简体中文 nav_translations: Home: 开始 + Change Log: 更新日志 + Migration: 迁移指南 Deprecated: 废弃功能列表 Support: 支持 - Change Log: 更新日志 Installation: 安装 Package Manager: 包管理器 @@ -203,6 +215,10 @@ plugins: Route Rule: 路由规则 Protocol Sniff: 协议探测 + Rule Set: 规则集 + Source Format: 源文件格式 + Headless Rule: 无头规则 + Experimental: 实验性 Shared: 通用 @@ -215,10 +231,6 @@ plugins: Inbound: 入站 Outbound: 出站 - FAQ: 常见问题 - Known Issues: 已知问题 - Examples: 示例 - Linux Server Installation: Linux 服务器安装 - DNS Hijack: DNS 劫持 + Manual: 手册 reconfigure_material: true reconfigure_search: true \ No newline at end of file From ff26fb020f20fe2536cbb3870b3e303acaf9eac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 29 Nov 2023 23:24:37 +0800 Subject: [PATCH 032/103] documentation: Bump version --- docs/changelog.md | 50 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 94a7147147..78fe3e81db 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,43 @@ icon: material/alert-decagram # ChangeLog +#### 1.8.0-alpha.1 + +* Migrate cache file from Clash API to independent options **1** +* Introducing [Rule Set](/configuration/rule-set) **2** +* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** +* Allow nested logical rules **4** + +**1**: + +See [Cache File](/configuration/experimental/cache-file) and +[Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options). + +**2**: + +Rule set is independent collections of rules that can be compiled into binaries to improve performance. +Compared to legacy GeoIP and Geosite resources, +it can include more types of rules, load faster, +use less memory, and update automatically. + +See [Route#rule_set](/configuration/route/#rule_set), +[Route Rule](/configuration/route/rule), +[DNS Rule](/configuration/dns/rule), +[Rule Set](/configuration/rule-set), +[Source Format](/configuration/rule-set/source-format) and +[Headless Rule](/configuration/rule-set/headless-rule). + +For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and +[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets). + +**3**: + +New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets. + +**4**: + +Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules. + #### 1.7.0 * Fixes and improvements @@ -142,11 +179,13 @@ Only supported in graphical clients on Android and iOS. **1**: -Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound options. +Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound +options. **2** -Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server, see [TCP Brutal](/configuration/shared/tcp-brutal) for details. +Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server, +see [TCP Brutal](/configuration/shared/tcp-brutal) for details. #### 1.7.0-alpha.3 @@ -213,8 +252,8 @@ When `auto_route` is enabled and `strict_route` is disabled, the device can now **2**: -Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave. - +Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High +Sierra, 10.14 Mojave. #### 1.6.0-rc.4 @@ -227,7 +266,8 @@ Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008 **1**: -Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave. +Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High +Sierra, 10.14 Mojave. #### 1.6.0-beta.4 From ee086ea1864d96852d10bd56a193e4b73b7a3fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 30 Nov 2023 15:28:08 +0800 Subject: [PATCH 033/103] Make rule-set initialization parallel --- adapter/router.go | 10 ++++++++- go.mod | 2 +- go.sum | 4 ++-- route/router.go | 22 +++++++++++++++++--- route/rule_set.go | 45 ++++++++++++++++++++++++++++++++++++++++ route/rule_set_local.go | 3 ++- route/rule_set_remote.go | 30 +++++++++++++++------------ 7 files changed, 95 insertions(+), 21 deletions(-) diff --git a/adapter/router.go b/adapter/router.go index ca4d6547c4..52eb5c8fd1 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -2,12 +2,14 @@ package adapter import ( "context" + "net/http" "net/netip" "github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-dns" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" + N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" mdns "github.com/miekg/dns" @@ -83,8 +85,14 @@ type DNSRule interface { } type RuleSet interface { + StartContext(ctx context.Context, startContext RuleSetStartContext) error + Close() error HeadlessRule - Service +} + +type RuleSetStartContext interface { + HTTPClient(detour string, dialer N.Dialer) *http.Client + Close() } type InterfaceUpdateListener interface { diff --git a/go.mod b/go.mod index 095326d7f9..69f0946afc 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 github.com/sagernet/quic-go v0.40.0 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be + github.com/sagernet/sing v0.2.18-0.20231130092223-1f82310f0375 github.com/sagernet/sing-dns v0.1.11 github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 github.com/sagernet/sing-quic v0.1.5-0.20231123150216-00957d136203 diff --git a/go.sum b/go.sum index 6d8d994a8c..a3d6fd7329 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= -github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be h1:FigAM9kq7RRXmHvgn8w2a8tqCY5CMV5GIk0id84dI0o= -github.com/sagernet/sing v0.2.18-0.20231129075305-eb56a60214be/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= +github.com/sagernet/sing v0.2.18-0.20231130092223-1f82310f0375 h1:5Q5K/twBNT1Hrpjd5Ghft0Sv0V+eVfTZX17CiPItSV8= +github.com/sagernet/sing v0.2.18-0.20231130092223-1f82310f0375/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE= github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE= github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM= diff --git a/route/router.go b/route/router.go index 967061952e..b5ef63400f 100644 --- a/route/router.go +++ b/route/router.go @@ -39,6 +39,7 @@ import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" serviceNTP "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" @@ -490,11 +491,26 @@ func (r *Router) Start() error { if r.needWIFIState { r.updateWIFIState() } - for i, ruleSet := range r.ruleSets { - err := ruleSet.Start() + if len(r.ruleSets) > 0 { + ruleSetStartContext := NewRuleSetStartContext() + var ruleSetStartGroup task.Group + for i, ruleSet := range r.ruleSets { + ruleSetInPlace := ruleSet + ruleSetStartGroup.Append0(func(ctx context.Context) error { + err := ruleSetInPlace.StartContext(ctx, ruleSetStartContext) + if err != nil { + return E.Cause(err, "initialize rule-set[", i, "]") + } + return nil + }) + } + ruleSetStartGroup.Concurrency(5) + ruleSetStartGroup.FastFail() + err := ruleSetStartGroup.Run(r.ctx) if err != nil { - return E.Cause(err, "initialize rule-set[", i, "]") + return err } + ruleSetStartContext.Close() } for i, rule := range r.rules { err := rule.Start() diff --git a/route/rule_set.go b/route/rule_set.go index 76c78c6214..f644fb406f 100644 --- a/route/rule_set.go +++ b/route/rule_set.go @@ -2,12 +2,17 @@ package route import ( "context" + "net" + "net/http" + "sync" "github.com/sagernet/sing-box/adapter" 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" ) func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { @@ -20,3 +25,43 @@ func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.Contex return nil, E.New("unknown rule set type: ", options.Type) } } + +var _ adapter.RuleSetStartContext = (*RuleSetStartContext)(nil) + +type RuleSetStartContext struct { + access sync.Mutex + httpClientCache map[string]*http.Client +} + +func NewRuleSetStartContext() *RuleSetStartContext { + return &RuleSetStartContext{ + httpClientCache: make(map[string]*http.Client), + } +} + +func (c *RuleSetStartContext) 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)) + }, + }, + } + c.httpClientCache[detour] = httpClient + return httpClient +} + +func (c *RuleSetStartContext) Close() { + c.access.Lock() + defer c.access.Unlock() + for _, client := range c.httpClientCache { + client.CloseIdleConnections() + } +} diff --git a/route/rule_set_local.go b/route/rule_set_local.go index ccdb1704a8..b466012a37 100644 --- a/route/rule_set_local.go +++ b/route/rule_set_local.go @@ -1,6 +1,7 @@ package route import ( + "context" "os" "github.com/sagernet/sing-box/adapter" @@ -60,7 +61,7 @@ func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { return false } -func (s *LocalRuleSet) Start() error { +func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error { return nil } diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go index 2092ed7466..06725a24f4 100644 --- a/route/rule_set_remote.go +++ b/route/rule_set_remote.go @@ -63,7 +63,7 @@ func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { return false } -func (s *RemoteRuleSet) Start() error { +func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error { var dialer N.Dialer if s.options.RemoteOptions.DownloadDetour != "" { outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour) @@ -91,7 +91,7 @@ func (s *RemoteRuleSet) Start() error { } } if s.lastUpdated.IsZero() || time.Since(s.lastUpdated) > s.updateInterval { - err := s.fetchOnce() + err := s.fetchOnce(ctx, startContext) if err != nil { return E.Cause(err, "fetch rule-set ", s.options.Tag) } @@ -141,7 +141,7 @@ func (s *RemoteRuleSet) loopUpdate() { case <-s.ctx.Done(): return case <-s.updateTicker.C: - err := s.fetchOnce() + err := s.fetchOnce(s.ctx, nil) if err != nil { s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) } @@ -149,18 +149,22 @@ func (s *RemoteRuleSet) loopUpdate() { } } -func (s *RemoteRuleSet) fetchOnce() error { +func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext adapter.RuleSetStartContext) error { s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL) - httpClient := &http.Client{ - Transport: &http.Transport{ - ForceAttemptHTTP2: true, - TLSHandshakeTimeout: C.TCPTimeout, - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + var httpClient *http.Client + if startContext != nil { + httpClient = startContext.HTTPClient(s.options.RemoteOptions.DownloadDetour, s.dialer) + } else { + httpClient = &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, }, - }, + } } - defer httpClient.CloseIdleConnections() request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil) if err != nil { return err @@ -168,7 +172,7 @@ func (s *RemoteRuleSet) fetchOnce() error { if s.lastEtag != "" { request.Header.Set("If-None-Match", s.lastEtag) } - response, err := httpClient.Do(request.WithContext(s.ctx)) + response, err := httpClient.Do(request.WithContext(ctx)) if err != nil { return err } From f61ea0da893db36b450a91c26e5104e7877483f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 30 Nov 2023 15:58:00 +0800 Subject: [PATCH 034/103] Independent `source_ip_is_private` and `ip_is_private` rules --- docs/configuration/dns/rule.md | 8 ++++ docs/configuration/dns/rule.zh.md | 8 ++++ docs/configuration/route/rule.md | 16 ++++++++ docs/configuration/route/rule.zh.md | 16 ++++++++ docs/migration.md | 8 ++++ option/rule.go | 2 + option/rule_dns.go | 63 +++++++++++++++-------------- route/rule_default.go | 10 +++++ route/rule_dns.go | 5 +++ route/rule_item_ip_is_private.go | 44 ++++++++++++++++++++ 10 files changed, 149 insertions(+), 31 deletions(-) create mode 100644 route/rule_item_ip_is_private.go diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 896a3b4468..5c3e6086b6 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -5,6 +5,7 @@ icon: material/alert-decagram !!! quote "Changes in sing-box 1.8.0" :material-plus: [rule_set](#rule_set) + :material-plus: [source_ip_is_private](#source_ip_is_private) :material-delete-clock: [geoip](#geoip) :material-delete-clock: [geosite](#geosite) @@ -56,6 +57,7 @@ icon: material/alert-decagram "10.0.0.0/24", "192.168.0.1" ], + "source_ip_is_private": false, "source_port": [ 12345 ], @@ -198,6 +200,12 @@ Match source geoip. Match source IP CIDR. +#### source_ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public source IP. + #### source_port Match source port. diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index f990ed3ec5..362e7ce4fb 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -5,6 +5,7 @@ icon: material/alert-decagram !!! quote "sing-box 1.8.0 中的更改" :material-plus: [rule_set](#rule_set) + :material-plus: [source_ip_is_private](#source_ip_is_private) :material-delete-clock: [geoip](#geoip) :material-delete-clock: [geosite](#geosite) @@ -55,6 +56,7 @@ icon: material/alert-decagram "source_ip_cidr": [ "10.0.0.0/24" ], + "source_ip_is_private": false, "source_port": [ 12345 ], @@ -195,6 +197,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配源 IP CIDR。 +#### source_ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开源 IP。 + #### source_port 匹配源端口。 diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 35c1d8fc24..2eee990935 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -6,6 +6,8 @@ icon: material/alert-decagram :material-plus: [rule_set](#rule_set) :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-plus: [ip_is_private](#ip_is_private) :material-delete-clock: [source_geoip](#source_geoip) :material-delete-clock: [geoip](#geoip) :material-delete-clock: [geosite](#geosite) @@ -58,10 +60,12 @@ icon: material/alert-decagram "10.0.0.0/24", "192.168.0.1" ], + "source_ip_is_private": false, "ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], + "ip_is_private": false, "source_port": [ 12345 ], @@ -202,10 +206,22 @@ Match geoip. Match source IP CIDR. +#### ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public IP. + #### ip_cidr Match IP CIDR. +#### source_ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public source IP. + #### source_port Match source port. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 31ee08d13a..3bac97e6fb 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -6,6 +6,8 @@ icon: material/alert-decagram :material-plus: [rule_set](#rule_set) :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-plus: [ip_is_private](#ip_is_private) :material-delete-clock: [source_geoip](#source_geoip) :material-delete-clock: [geoip](#geoip) :material-delete-clock: [geosite](#geosite) @@ -57,9 +59,11 @@ icon: material/alert-decagram "source_ip_cidr": [ "10.0.0.0/24" ], + "source_ip_is_private": false, "ip_cidr": [ "10.0.0.0/24" ], + "ip_is_private": false, "source_port": [ 12345 ], @@ -200,10 +204,22 @@ icon: material/alert-decagram 匹配源 IP CIDR。 +#### source_ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开源 IP。 + #### ip_cidr 匹配 IP CIDR。 +#### ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开 IP。 + #### source_port 匹配源端口。 diff --git a/docs/migration.md b/docs/migration.md index 8eb3daee42..aec9b36007 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -68,6 +68,10 @@ icon: material/arrange-bring-forward { "route": { "rules": [ + { + "geoip": "private", + "outbound": "direct" + }, { "geoip": "cn", "outbound": "direct" @@ -90,6 +94,10 @@ icon: material/arrange-bring-forward { "route": { "rules": [ + { + "ip_is_private": true, + "outbound": "direct" + }, { "rule_set": "geoip-cn", "outbound": "direct" diff --git a/option/rule.go b/option/rule.go index bad605a0fc..1201d123ed 100644 --- a/option/rule.go +++ b/option/rule.go @@ -78,7 +78,9 @@ type DefaultRule struct { SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` GeoIP Listable[string] `json:"geoip,omitempty"` SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` SourcePort Listable[uint16] `json:"source_port,omitempty"` SourcePortRange Listable[string] `json:"source_port_range,omitempty"` Port Listable[uint16] `json:"port,omitempty"` diff --git a/option/rule_dns.go b/option/rule_dns.go index c02d09f761..50d9e61266 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -65,37 +65,38 @@ func (r DNSRule) IsValid() bool { } type DefaultDNSRule struct { - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - Outbound Listable[string] `json:"outbound,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - RuleSet Listable[string] `json:"rule_set,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` + Network Listable[string] `json:"network,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + Protocol Listable[string] `json:"protocol,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + Geosite Listable[string] `json:"geosite,omitempty"` + SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + User Listable[string] `json:"user,omitempty"` + UserID Listable[int32] `json:"user_id,omitempty"` + Outbound Listable[string] `json:"outbound,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` + Invert bool `json:"invert,omitempty"` + Server string `json:"server,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` } func (r DefaultDNSRule) IsValid() bool { diff --git a/route/rule_default.go b/route/rule_default.go index c0ef9eef60..1a190ce04b 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -120,6 +120,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } + if options.SourceIPIsPrivate { + item := NewIPIsPrivateItem(true) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } if len(options.IPCIDR) > 0 { item, err := NewIPCIDRItem(false, options.IPCIDR) if err != nil { @@ -128,6 +133,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } + if options.IPIsPrivate { + item := NewIPIsPrivateItem(false) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } if len(options.SourcePort) > 0 { item := NewPortItem(true, options.SourcePort) rule.sourcePortItems = append(rule.sourcePortItems, item) diff --git a/route/rule_dns.go b/route/rule_dns.go index f5f9fd3583..1f55d50edb 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -119,6 +119,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } + if options.SourceIPIsPrivate { + item := NewIPIsPrivateItem(true) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } if len(options.SourcePort) > 0 { item := NewPortItem(true, options.SourcePort) rule.sourcePortItems = append(rule.sourcePortItems, item) diff --git a/route/rule_item_ip_is_private.go b/route/rule_item_ip_is_private.go new file mode 100644 index 0000000000..4d511fdfb1 --- /dev/null +++ b/route/rule_item_ip_is_private.go @@ -0,0 +1,44 @@ +package route + +import ( + "net/netip" + + "github.com/sagernet/sing-box/adapter" + N "github.com/sagernet/sing/common/network" +) + +var _ RuleItem = (*IPIsPrivateItem)(nil) + +type IPIsPrivateItem struct { + isSource bool +} + +func NewIPIsPrivateItem(isSource bool) *IPIsPrivateItem { + return &IPIsPrivateItem{isSource} +} + +func (r *IPIsPrivateItem) Match(metadata *adapter.InboundContext) bool { + var destination netip.Addr + if r.isSource { + destination = metadata.Source.Addr + } else { + destination = metadata.Destination.Addr + } + if destination.IsValid() && !N.IsPublicAddr(destination) { + return true + } + for _, destinationAddress := range metadata.DestinationAddresses { + if !N.IsPublicAddr(destinationAddress) { + return true + } + } + return false +} + +func (r *IPIsPrivateItem) String() string { + if r.isSource { + return "source_ip_is_private=true" + } else { + return "ip_is_private=true" + } +} From 644100357c09f4e547db3e7ef398892ab6e472a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 30 Nov 2023 18:31:55 +0800 Subject: [PATCH 035/103] Fix router start order --- route/router.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/route/router.go b/route/router.go index b5ef63400f..0c57cdc438 100644 --- a/route/router.go +++ b/route/router.go @@ -491,6 +491,12 @@ func (r *Router) Start() error { if r.needWIFIState { r.updateWIFIState() } + if r.fakeIPStore != nil { + err := r.fakeIPStore.Start() + if err != nil { + return err + } + } if len(r.ruleSets) > 0 { ruleSetStartContext := NewRuleSetStartContext() var ruleSetStartGroup task.Group @@ -524,12 +530,6 @@ func (r *Router) Start() error { return E.Cause(err, "initialize DNS rule[", i, "]") } } - if r.fakeIPStore != nil { - err := r.fakeIPStore.Start() - if err != nil { - return err - } - } for i, transport := range r.transports { err := transport.Start() if err != nil { From 62185946b53c1f471428b4fa6515163813bfa402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 30 Nov 2023 18:44:48 +0800 Subject: [PATCH 036/103] Fix URLTest group early start --- outbound/urltest.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/outbound/urltest.go b/outbound/urltest.go index 45ce90b67e..2257857115 100644 --- a/outbound/urltest.go +++ b/outbound/urltest.go @@ -38,6 +38,7 @@ type URLTest struct { tolerance uint16 group *URLTestGroup interruptExternalConnections bool + started bool } func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (*URLTest, error) { @@ -83,6 +84,7 @@ func (s *URLTest) Start() error { } func (s *URLTest) PostStart() error { + s.started = true go s.CheckOutbounds() return nil } @@ -110,7 +112,9 @@ func (s *URLTest) CheckOutbounds() { } func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - s.group.Start() + if s.started { + s.group.Start() + } outbound := s.group.Select(network) conn, err := outbound.DialContext(ctx, network, destination) if err == nil { @@ -122,7 +126,9 @@ func (s *URLTest) DialContext(ctx context.Context, network string, destination M } func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - s.group.Start() + if s.started { + s.group.Start() + } outbound := s.group.Select(N.NetworkUDP) conn, err := outbound.ListenPacket(ctx, destination) if err == nil { From 3bb709db184a02a1ed7ea002cfd2592d5cb690b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 30 Nov 2023 11:05:15 +0800 Subject: [PATCH 037/103] documentation: Update rule-set example --- docs/configuration/dns/rule.md | 4 +- docs/configuration/dns/rule.zh.md | 4 +- docs/configuration/route/geoip.md | 2 +- docs/configuration/route/geosite.md | 2 +- docs/configuration/route/rule.md | 6 +- docs/configuration/route/rule.zh.md | 6 +- docs/manual/proxy/client.md | 184 ++++++++++++++++++++++++++++ 7 files changed, 196 insertions(+), 12 deletions(-) diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 5c3e6086b6..513da60e03 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -184,7 +184,7 @@ Match domain using regular expression. !!! failure "Deprecated in sing-box 1.8.0" - Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). Match geosite. @@ -192,7 +192,7 @@ Match geosite. !!! failure "Deprecated in sing-box 1.8.0" - GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). Match source geoip. diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 362e7ce4fb..f6c1f0ffc2 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -181,7 +181,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! failure "已在 sing-box 1.8.0 废弃" - Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-set)。 + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-sets)。 匹配 Geosite。 @@ -189,7 +189,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! failure "已在 sing-box 1.8.0 废弃" - GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-sets)。 匹配源 GeoIP。 diff --git a/docs/configuration/route/geoip.md b/docs/configuration/route/geoip.md index d6dfd23289..8a2ed1d4a1 100644 --- a/docs/configuration/route/geoip.md +++ b/docs/configuration/route/geoip.md @@ -4,7 +4,7 @@ icon: material/delete-clock !!! failure "Deprecated in sing-box 1.8.0" - GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). ### Structure diff --git a/docs/configuration/route/geosite.md b/docs/configuration/route/geosite.md index 515e86b1ec..0463057153 100644 --- a/docs/configuration/route/geosite.md +++ b/docs/configuration/route/geosite.md @@ -4,7 +4,7 @@ icon: material/delete-clock !!! failure "Deprecated in sing-box 1.8.0" - Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). ### Structure diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 2eee990935..aefc607c24 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -182,7 +182,7 @@ Match domain using regular expression. !!! failure "Deprecated in sing-box 1.8.0" - Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-set). + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). Match geosite. @@ -190,7 +190,7 @@ Match geosite. !!! failure "Deprecated in sing-box 1.8.0" - GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). Match source geoip. @@ -198,7 +198,7 @@ Match source geoip. !!! failure "Deprecated in sing-box 1.8.0" - GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-set). + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). Match geoip. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 3bac97e6fb..f735de4836 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -180,7 +180,7 @@ icon: material/alert-decagram !!! failure "已在 sing-box 1.8.0 废弃" - Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-set)。 + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-sets)。 匹配 Geosite。 @@ -188,7 +188,7 @@ icon: material/alert-decagram !!! failure "已在 sing-box 1.8.0 废弃" - GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-sets)。 匹配源 GeoIP。 @@ -196,7 +196,7 @@ icon: material/alert-decagram !!! failure "已在 sing-box 1.8.0 废弃" - GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-set)。 + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-sets)。 匹配 GeoIP。 diff --git a/docs/manual/proxy/client.md b/docs/manual/proxy/client.md index 60db02deb6..11bc40ceb4 100644 --- a/docs/manual/proxy/client.md +++ b/docs/manual/proxy/client.md @@ -343,6 +343,83 @@ flowchart TB } ``` +=== ":material-dns: DNS rules (1.8.0+)" + + !!! info + + DNS rules are optional if FakeIP is used. + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "address": "tls://8.8.8.8" + }, + { + "tag": "local", + "address": "223.5.5.5", + "detour": "direct" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + }, + { + "clash_mode": "Direct", + "server": "local" + }, + { + "clash_mode": "Global", + "server": "google" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": [ + "geosite-cn", + "geosite-category-companies@cn" + ] + } + ], + "server": "local" + } + ] + }, + "route": { + "rule_set": [ + { + "type": "remote", + "tag": "geosite-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-geolocation-!cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" + }, + { + "type": "remote", + "tag": "geosite-category-companies@cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-companies@cn.srs" + } + ] + } + } + ``` + === ":material-router-network: Route rules" ```json @@ -422,4 +499,111 @@ flowchart TB ] } } + ``` + +=== ":material-router-network: Route rules (1.8.0+)" + + ```json + { + "outbounds": [ + { + "type": "direct", + "tag": "direct" + }, + { + "type": "block", + "tag": "block" + } + ], + "route": { + "rules": [ + { + "type": "logical", + "mode": "or", + "rules": [ + { + "protocol": "dns" + }, + { + "port": 53 + } + ], + "outbound": "dns" + }, + { + "ip_is_private": true, + "outbound": "direct" + }, + { + "clash_mode": "Direct", + "outbound": "direct" + }, + { + "clash_mode": "Global", + "outbound": "default" + }, + { + "type": "logical", + "mode": "or", + "rules": [ + { + "port": 853 + }, + { + "network": "udp", + "port": 443 + }, + { + "protocol": "stun" + } + ], + "outbound": "block" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": [ + "geoip-cn", + "geosite-cn", + "geosite-category-companies@cn" + ] + } + ], + "outbound": "direct" + } + ], + "rule_set": [ + { + "type": "remote", + "tag": "geoip-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-geolocation-!cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" + }, + { + "type": "remote", + "tag": "geosite-category-companies@cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-companies@cn.srs" + } + ] + } + } ``` \ No newline at end of file From efdb34de9134e4068ef7be80dcd818cc0bbfaa18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 30 Nov 2023 15:58:10 +0800 Subject: [PATCH 038/103] documentation: Bump version --- docs/changelog.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 78fe3e81db..a7229a7b51 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,16 @@ icon: material/alert-decagram # ChangeLog +#### 1.8.0-alpha.5 + +* Parallel rule-set initialization +* Independent `source_ip_is_private` and `ip_is_private` rules **1** + +**1**: + +The `private` GeoIP country never existed and was actually implemented inside V2Ray. +Since GeoIP was deprecated, we made this rule independent, see [Migration](/migration/#migrate-geoip-to-rule-sets). + #### 1.8.0-alpha.1 * Migrate cache file from Clash API to independent options **1** From 1bf48d792e78aadd0e017fd2a8e8f00e13dd91d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Nov 2023 12:00:28 +0800 Subject: [PATCH 039/103] Migrate to independent cache file --- adapter/experimental.go | 10 ++- box.go | 53 ++++++++++-- .../{clashapi => }/cachefile/cache.go | 85 +++++++++++++------ .../{clashapi => }/cachefile/fakeip.go | 0 experimental/clashapi/cache.go | 11 ++- experimental/clashapi/server.go | 66 ++++---------- experimental/libbox/command_group.go | 21 ++--- experimental/libbox/command_urltest.go | 21 ++--- option/clash.go | 31 ------- option/experimental.go | 47 +++++++++- option/group.go | 15 ++++ option/v2ray.go | 12 --- outbound/builder.go | 2 +- outbound/selector.go | 15 ++-- route/router.go | 2 +- transport/fakeip/store.go | 15 ++-- 16 files changed, 227 insertions(+), 179 deletions(-) rename experimental/{clashapi => }/cachefile/cache.go (81%) rename experimental/{clashapi => }/cachefile/fakeip.go (100%) delete mode 100644 option/clash.go create mode 100644 option/group.go diff --git a/adapter/experimental.go b/adapter/experimental.go index 697fa9f199..ac523e4e74 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -13,15 +13,17 @@ type ClashServer interface { PreStarter Mode() string ModeList() []string - StoreSelected() bool - StoreFakeIP() bool - CacheFile() ClashCacheFile HistoryStorage() *urltest.HistoryStorage RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker) } -type ClashCacheFile interface { +type CacheFile interface { + Service + PreStarter + + StoreFakeIP() bool + LoadMode() string StoreMode(mode string) error LoadSelected(group string) string diff --git a/box.go b/box.go index c10222ddf7..d3838ca337 100644 --- a/box.go +++ b/box.go @@ -10,6 +10,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/inbound" "github.com/sagernet/sing-box/log" @@ -32,7 +33,8 @@ type Box struct { outbounds []adapter.Outbound logFactory log.Factory logger log.ContextLogger - preServices map[string]adapter.Service + preServices1 map[string]adapter.Service + preServices2 map[string]adapter.Service postServices map[string]adapter.Service done chan struct{} } @@ -45,17 +47,21 @@ type Options struct { } func New(options Options) (*Box, error) { + createdAt := time.Now() ctx := options.Context if ctx == nil { ctx = context.Background() } ctx = service.ContextWithDefaultRegistry(ctx) ctx = pause.ContextWithDefaultManager(ctx) - createdAt := time.Now() experimentalOptions := common.PtrValueOrDefault(options.Experimental) applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) + var needCacheFile bool var needClashAPI bool var needV2RayAPI bool + if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil { + needCacheFile = true + } if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil { needClashAPI = true } @@ -145,8 +151,14 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "initialize platform interface") } } - preServices := make(map[string]adapter.Service) + preServices1 := make(map[string]adapter.Service) + preServices2 := make(map[string]adapter.Service) postServices := make(map[string]adapter.Service) + if needCacheFile { + cacheFile := cachefile.NewCacheFile(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) + preServices1["cache file"] = cacheFile + service.MustRegister[adapter.CacheFile](ctx, cacheFile) + } if needClashAPI { clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options) @@ -155,7 +167,7 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "create clash api server") } router.SetClashServer(clashServer) - preServices["clash api"] = clashServer + preServices2["clash api"] = clashServer } if needV2RayAPI { v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI)) @@ -163,7 +175,7 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "create v2ray api server") } router.SetV2RayServer(v2rayServer) - preServices["v2ray api"] = v2rayServer + preServices2["v2ray api"] = v2rayServer } return &Box{ router: router, @@ -172,7 +184,8 @@ func New(options Options) (*Box, error) { createdAt: createdAt, logFactory: logFactory, logger: logFactory.Logger(), - preServices: preServices, + preServices1: preServices1, + preServices2: preServices2, postServices: postServices, done: make(chan struct{}), }, nil @@ -217,7 +230,16 @@ func (s *Box) Start() error { } func (s *Box) preStart() error { - for serviceName, service := range s.preServices { + for serviceName, service := range s.preServices1 { + if preService, isPreService := service.(adapter.PreStarter); isPreService { + s.logger.Trace("pre-start ", serviceName) + err := preService.PreStart() + if err != nil { + return E.Cause(err, "pre-starting ", serviceName) + } + } + } + for serviceName, service := range s.preServices2 { if preService, isPreService := service.(adapter.PreStarter); isPreService { s.logger.Trace("pre-start ", serviceName) err := preService.PreStart() @@ -238,7 +260,14 @@ func (s *Box) start() error { if err != nil { return err } - for serviceName, service := range s.preServices { + for serviceName, service := range s.preServices1 { + s.logger.Trace("starting ", serviceName) + err = service.Start() + if err != nil { + return E.Cause(err, "start ", serviceName) + } + } + for serviceName, service := range s.preServices2 { s.logger.Trace("starting ", serviceName) err = service.Start() if err != nil { @@ -313,7 +342,13 @@ func (s *Box) Close() error { return E.Cause(err, "close router") }) } - for serviceName, service := range s.preServices { + for serviceName, service := range s.preServices1 { + s.logger.Trace("closing ", serviceName) + errors = E.Append(errors, service.Close(), func(err error) error { + return E.Cause(err, "close ", serviceName) + }) + } + for serviceName, service := range s.preServices2 { s.logger.Trace("closing ", serviceName) errors = E.Append(errors, service.Close(), func(err error) error { return E.Cause(err, "close ", serviceName) diff --git a/experimental/clashapi/cachefile/cache.go b/experimental/cachefile/cache.go similarity index 81% rename from experimental/clashapi/cachefile/cache.go rename to experimental/cachefile/cache.go index 0911829749..2616ded5c1 100644 --- a/experimental/clashapi/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/bbolt" bboltErrors "github.com/sagernet/bbolt/errors" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service/filemanager" @@ -31,11 +32,15 @@ var ( cacheIDDefault = []byte("default") ) -var _ adapter.ClashCacheFile = (*CacheFile)(nil) +var _ adapter.CacheFile = (*CacheFile)(nil) type CacheFile struct { + ctx context.Context + path string + cacheID []byte + storeFakeIP bool + DB *bbolt.DB - cacheID []byte saveAccess sync.RWMutex saveDomain map[netip.Addr]string saveAddress4 map[string]netip.Addr @@ -43,7 +48,29 @@ type CacheFile struct { saveMetadataTimer *time.Timer } -func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) { +func NewCacheFile(ctx context.Context, options option.CacheFileOptions) *CacheFile { + var path string + if options.Path != "" { + path = options.Path + } else { + path = "cache.db" + } + var cacheIDBytes []byte + if options.CacheID != "" { + cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...) + } + return &CacheFile{ + ctx: ctx, + path: filemanager.BasePath(ctx, path), + cacheID: cacheIDBytes, + storeFakeIP: options.StoreFakeIP, + saveDomain: make(map[netip.Addr]string), + saveAddress4: make(map[string]netip.Addr), + saveAddress6: make(map[string]netip.Addr), + } +} + +func (c *CacheFile) start() error { const fileMode = 0o666 options := bbolt.Options{Timeout: time.Second} var ( @@ -51,7 +78,7 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) err error ) for i := 0; i < 10; i++ { - db, err = bbolt.Open(path, fileMode, &options) + db, err = bbolt.Open(c.path, fileMode, &options) if err == nil { break } @@ -59,23 +86,20 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) continue } if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) { - rmErr := os.Remove(path) + rmErr := os.Remove(c.path) if rmErr != nil { - return nil, err + return err } } time.Sleep(100 * time.Millisecond) } if err != nil { - return nil, err + return err } - err = filemanager.Chown(ctx, path) + err = filemanager.Chown(c.ctx, c.path) if err != nil { - return nil, E.Cause(err, "platform chown") - } - var cacheIDBytes []byte - if cacheID != "" { - cacheIDBytes = append([]byte{0}, []byte(cacheID)...) + db.Close() + return E.Cause(err, "platform chown") } err = db.Batch(func(tx *bbolt.Tx) error { return tx.ForEach(func(name []byte, b *bbolt.Bucket) error { @@ -97,15 +121,30 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) }) }) if err != nil { - return nil, err + db.Close() + return err } - return &CacheFile{ - DB: db, - cacheID: cacheIDBytes, - saveDomain: make(map[netip.Addr]string), - saveAddress4: make(map[string]netip.Addr), - saveAddress6: make(map[string]netip.Addr), - }, nil + c.DB = db + return nil +} + +func (c *CacheFile) PreStart() error { + return c.start() +} + +func (c *CacheFile) Start() error { + return nil +} + +func (c *CacheFile) Close() error { + if c.DB == nil { + return nil + } + return c.DB.Close() +} + +func (c *CacheFile) StoreFakeIP() bool { + return c.storeFakeIP } func (c *CacheFile) LoadMode() string { @@ -218,7 +257,3 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { } }) } - -func (c *CacheFile) Close() error { - return c.DB.Close() -} diff --git a/experimental/clashapi/cachefile/fakeip.go b/experimental/cachefile/fakeip.go similarity index 100% rename from experimental/clashapi/cachefile/fakeip.go rename to experimental/cachefile/fakeip.go diff --git a/experimental/clashapi/cache.go b/experimental/clashapi/cache.go index 7582fde5ae..9c088a82f7 100644 --- a/experimental/clashapi/cache.go +++ b/experimental/clashapi/cache.go @@ -1,23 +1,26 @@ package clashapi import ( + "context" "net/http" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/service" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) -func cacheRouter(router adapter.Router) http.Handler { +func cacheRouter(ctx context.Context) http.Handler { r := chi.NewRouter() - r.Post("/fakeip/flush", flushFakeip(router)) + r.Post("/fakeip/flush", flushFakeip(ctx)) return r } -func flushFakeip(router adapter.Router) func(w http.ResponseWriter, r *http.Request) { +func flushFakeip(ctx context.Context) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if cacheFile := router.ClashServer().CacheFile(); cacheFile != nil { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile != nil { err := cacheFile.FakeIPReset() if err != nil { render.Status(r, http.StatusInternalServerError) diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 6a3d6f66f7..c40ff93846 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -15,7 +15,6 @@ import ( "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" - "github.com/sagernet/sing-box/experimental/clashapi/cachefile" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -49,12 +48,6 @@ type Server struct { mode string modeList []string modeUpdateHook chan<- struct{} - storeMode bool - storeSelected bool - storeFakeIP bool - cacheFilePath string - cacheID string - cacheFile adapter.ClashCacheFile externalController bool externalUI string @@ -76,9 +69,6 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ trafficManager: trafficManager, modeList: options.ModeList, externalController: options.ExternalController != "", - storeMode: options.StoreMode, - storeSelected: options.StoreSelected, - storeFakeIP: options.StoreFakeIP, externalUIDownloadURL: options.ExternalUIDownloadURL, externalUIDownloadDetour: options.ExternalUIDownloadDetour, } @@ -94,18 +84,10 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ server.modeList = append([]string{defaultMode}, server.modeList...) } server.mode = defaultMode - if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.ExternalController == "" { - cachePath := os.ExpandEnv(options.CacheFile) - if cachePath == "" { - cachePath = "cache.db" - } - if foundPath, loaded := C.FindPath(cachePath); loaded { - cachePath = foundPath - } else { - cachePath = filemanager.BasePath(ctx, cachePath) - } - server.cacheFilePath = cachePath - server.cacheID = options.CacheID + //goland:noinspection GoDeprecation + //nolint:staticcheck + if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" { + return nil, E.New("cache_file and related fields in Clash API is deprecated in sing-box 1.8.0, use experimental.cache_file instead.") } cors := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, @@ -128,7 +110,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/script", scriptRouter()) r.Mount("/profile", profileRouter()) - r.Mount("/cache", cacheRouter(router)) + r.Mount("/cache", cacheRouter(ctx)) r.Mount("/dns", dnsRouter(router)) server.setupMetaAPI(r) @@ -147,19 +129,13 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ } func (s *Server) PreStart() error { - if s.cacheFilePath != "" { - cacheFile, err := cachefile.Open(s.ctx, s.cacheFilePath, s.cacheID) - if err != nil { - return E.Cause(err, "open cache file") - } - s.cacheFile = cacheFile - if s.storeMode { - mode := s.cacheFile.LoadMode() - if common.Any(s.modeList, func(it string) bool { - return strings.EqualFold(it, mode) - }) { - s.mode = mode - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + mode := cacheFile.LoadMode() + if common.Any(s.modeList, func(it string) bool { + return strings.EqualFold(it, mode) + }) { + s.mode = mode } } return nil @@ -187,7 +163,6 @@ func (s *Server) Close() error { return common.Close( common.PtrOrNil(s.httpServer), s.trafficManager, - s.cacheFile, s.urlTestHistory, ) } @@ -224,8 +199,9 @@ func (s *Server) SetMode(newMode string) { } } s.router.ClearDNSCache() - if s.storeMode { - err := s.cacheFile.StoreMode(newMode) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreMode(newMode) if err != nil { s.logger.Error(E.Cause(err, "save mode")) } @@ -233,18 +209,6 @@ func (s *Server) SetMode(newMode string) { s.logger.Info("updated mode: ", newMode) } -func (s *Server) StoreSelected() bool { - return s.storeSelected -} - -func (s *Server) StoreFakeIP() bool { - return s.storeFakeIP -} - -func (s *Server) CacheFile() adapter.ClashCacheFile { - return s.cacheFile -} - func (s *Server) HistoryStorage() *urltest.HistoryStorage { return s.urlTestHistory } diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go index 934820889e..2fc69b98b4 100644 --- a/experimental/libbox/command_group.go +++ b/experimental/libbox/command_group.go @@ -159,11 +159,7 @@ func readGroups(reader io.Reader) (OutboundGroupIterator, error) { func writeGroups(writer io.Writer, boxService *BoxService) error { historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx) - var cacheFile adapter.ClashCacheFile - if clashServer := boxService.instance.Router().ClashServer(); clashServer != nil { - cacheFile = clashServer.CacheFile() - } - + cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx) outbounds := boxService.instance.Router().Outbounds() var iGroups []adapter.OutboundGroup for _, it := range outbounds { @@ -288,16 +284,15 @@ func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return writeError(conn, E.New("service not ready")) } - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - err = cacheFile.StoreGroupExpand(groupTag, isExpand) - if err != nil { - return writeError(conn, err) - } + cacheFile := service.FromContext[adapter.CacheFile](serviceNow.ctx) + if cacheFile != nil { + err = cacheFile.StoreGroupExpand(groupTag, isExpand) + if err != nil { + return writeError(conn, err) } } return writeError(conn, nil) diff --git a/experimental/libbox/command_urltest.go b/experimental/libbox/command_urltest.go index 88e86a8f8c..3563d8c6a3 100644 --- a/experimental/libbox/command_urltest.go +++ b/experimental/libbox/command_urltest.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/service" ) func (c *CommandClient) URLTest(groupTag string) error { @@ -37,11 +38,11 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return nil } - abstractOutboundGroup, isLoaded := service.instance.Router().Outbound(groupTag) + abstractOutboundGroup, isLoaded := serviceNow.instance.Router().Outbound(groupTag) if !isLoaded { return writeError(conn, E.New("outbound group not found: ", groupTag)) } @@ -53,15 +54,9 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if isURLTest { go urlTest.CheckOutbounds() } else { - var historyStorage *urltest.HistoryStorage - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - historyStorage = clashServer.HistoryStorage() - } else { - return writeError(conn, E.New("Clash API is required for URLTest on non-URLTest group")) - } - + historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx) outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { - itOutbound, _ := service.instance.Router().Outbound(it) + itOutbound, _ := serviceNow.instance.Router().Outbound(it) return itOutbound }), func(it adapter.Outbound) bool { if it == nil { @@ -73,12 +68,12 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { } return true }) - b, _ := batch.New(service.ctx, batch.WithConcurrencyNum[any](10)) + b, _ := batch.New(serviceNow.ctx, batch.WithConcurrencyNum[any](10)) for _, detour := range outbounds { outboundToTest := detour outboundTag := outboundToTest.Tag() b.Go(outboundTag, func() (any, error) { - t, err := urltest.URLTest(service.ctx, "", outboundToTest) + t, err := urltest.URLTest(serviceNow.ctx, "", outboundToTest) if err != nil { historyStorage.DeleteURLTestHistory(outboundTag) } else { diff --git a/option/clash.go b/option/clash.go deleted file mode 100644 index 63ee2aebdf..0000000000 --- a/option/clash.go +++ /dev/null @@ -1,31 +0,0 @@ -package option - -type ClashAPIOptions struct { - ExternalController string `json:"external_controller,omitempty"` - ExternalUI string `json:"external_ui,omitempty"` - ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` - ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` - Secret string `json:"secret,omitempty"` - DefaultMode string `json:"default_mode,omitempty"` - StoreMode bool `json:"store_mode,omitempty"` - StoreSelected bool `json:"store_selected,omitempty"` - StoreFakeIP bool `json:"store_fakeip,omitempty"` - CacheFile string `json:"cache_file,omitempty"` - CacheID string `json:"cache_id,omitempty"` - - ModeList []string `json:"-"` -} - -type SelectorOutboundOptions struct { - Outbounds []string `json:"outbounds"` - Default string `json:"default,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} - -type URLTestOutboundOptions struct { - Outbounds []string `json:"outbounds"` - URL string `json:"url,omitempty"` - Interval Duration `json:"interval,omitempty"` - Tolerance uint16 `json:"tolerance,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} diff --git a/option/experimental.go b/option/experimental.go index a5b6acbd32..72751a590e 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -1,7 +1,48 @@ package option type ExperimentalOptions struct { - ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` - V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` - Debug *DebugOptions `json:"debug,omitempty"` + CacheFile *CacheFileOptions `json:"cache_file,omitempty"` + ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` + V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` + Debug *DebugOptions `json:"debug,omitempty"` +} + +type CacheFileOptions struct { + Enabled bool `json:"enabled,omitempty"` + Path string `json:"path,omitempty"` + CacheID string `json:"cache_id,omitempty"` + StoreFakeIP bool `json:"store_fakeip,omitempty"` +} + +type ClashAPIOptions struct { + ExternalController string `json:"external_controller,omitempty"` + ExternalUI string `json:"external_ui,omitempty"` + ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` + ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` + Secret string `json:"secret,omitempty"` + DefaultMode string `json:"default_mode,omitempty"` + ModeList []string `json:"-"` + + // Deprecated: migrated to global cache file + StoreMode bool `json:"store_mode,omitempty"` + // Deprecated: migrated to global cache file + StoreSelected bool `json:"store_selected,omitempty"` + // Deprecated: migrated to global cache file + StoreFakeIP bool `json:"store_fakeip,omitempty"` + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` +} + +type V2RayAPIOptions struct { + Listen string `json:"listen,omitempty"` + Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` +} + +type V2RayStatsServiceOptions struct { + Enabled bool `json:"enabled,omitempty"` + Inbounds []string `json:"inbounds,omitempty"` + Outbounds []string `json:"outbounds,omitempty"` + Users []string `json:"users,omitempty"` } diff --git a/option/group.go b/option/group.go new file mode 100644 index 0000000000..58824e808b --- /dev/null +++ b/option/group.go @@ -0,0 +1,15 @@ +package option + +type SelectorOutboundOptions struct { + Outbounds []string `json:"outbounds"` + Default string `json:"default,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} + +type URLTestOutboundOptions struct { + Outbounds []string `json:"outbounds"` + URL string `json:"url,omitempty"` + Interval Duration `json:"interval,omitempty"` + Tolerance uint16 `json:"tolerance,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} diff --git a/option/v2ray.go b/option/v2ray.go index 37f7b8c4b0..774a651d1d 100644 --- a/option/v2ray.go +++ b/option/v2ray.go @@ -1,13 +1 @@ package option - -type V2RayAPIOptions struct { - Listen string `json:"listen,omitempty"` - Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` -} - -type V2RayStatsServiceOptions struct { - Enabled bool `json:"enabled,omitempty"` - Inbounds []string `json:"inbounds,omitempty"` - Outbounds []string `json:"outbounds,omitempty"` - Users []string `json:"users,omitempty"` -} diff --git a/outbound/builder.go b/outbound/builder.go index 141758d870..e4d6a80e06 100644 --- a/outbound/builder.go +++ b/outbound/builder.go @@ -56,7 +56,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t case C.TypeHysteria2: return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options) case C.TypeSelector: - return NewSelector(router, logger, tag, options.SelectorOptions) + return NewSelector(ctx, router, logger, tag, options.SelectorOptions) case C.TypeURLTest: return NewURLTest(ctx, router, logger, tag, options.URLTestOptions) default: diff --git a/outbound/selector.go b/outbound/selector.go index c66591cdee..e801daeadc 100644 --- a/outbound/selector.go +++ b/outbound/selector.go @@ -12,6 +12,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" ) var ( @@ -21,6 +22,7 @@ var ( type Selector struct { myOutboundAdapter + ctx context.Context tags []string defaultTag string outbounds map[string]adapter.Outbound @@ -29,7 +31,7 @@ type Selector struct { interruptExternalConnections bool } -func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { +func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { outbound := &Selector{ myOutboundAdapter: myOutboundAdapter{ protocol: C.TypeSelector, @@ -38,6 +40,7 @@ func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, op tag: tag, dependencies: options.Outbounds, }, + ctx: ctx, tags: options.Outbounds, defaultTag: options.Default, outbounds: make(map[string]adapter.Outbound), @@ -67,8 +70,9 @@ func (s *Selector) Start() error { } if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - selected := clashServer.CacheFile().LoadSelected(s.tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + selected := cacheFile.LoadSelected(s.tag) if selected != "" { detour, loaded := s.outbounds[selected] if loaded { @@ -110,8 +114,9 @@ func (s *Selector) SelectOutbound(tag string) bool { } s.selected = detour if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - err := clashServer.CacheFile().StoreSelected(s.tag, tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreSelected(s.tag, tag) if err != nil { s.logger.Error("store selected: ", err) } diff --git a/route/router.go b/route/router.go index 972c4ec048..e50a51a955 100644 --- a/route/router.go +++ b/route/router.go @@ -261,7 +261,7 @@ func NewRouter( if fakeIPOptions.Inet6Range != nil { inet6Range = *fakeIPOptions.Inet6Range } - router.fakeIPStore = fakeip.NewStore(router, router.logger, inet4Range, inet6Range) + router.fakeIPStore = fakeip.NewStore(ctx, router.logger, inet4Range, inet6Range) } usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor() diff --git a/transport/fakeip/store.go b/transport/fakeip/store.go index 96f6bf031e..83677b0d05 100644 --- a/transport/fakeip/store.go +++ b/transport/fakeip/store.go @@ -1,17 +1,19 @@ package fakeip import ( + "context" "net/netip" "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" ) var _ adapter.FakeIPStore = (*Store)(nil) type Store struct { - router adapter.Router + ctx context.Context logger logger.Logger inet4Range netip.Prefix inet6Range netip.Prefix @@ -20,9 +22,9 @@ type Store struct { inet6Current netip.Addr } -func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { +func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { return &Store{ - router: router, + ctx: ctx, logger: logger, inet4Range: inet4Range, inet6Range: inet6Range, @@ -31,10 +33,9 @@ func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Pref func (s *Store) Start() error { var storage adapter.FakeIPStorage - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreFakeIP() { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - storage = cacheFile - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil && cacheFile.StoreFakeIP() { + storage = cacheFile } if storage == nil { storage = NewMemoryStorage() From 6c6af936d0bdd8afd651ef3b979fda93d90e3cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Nov 2023 23:47:32 +0800 Subject: [PATCH 040/103] Allow nested logical rules --- experimental/clashapi.go | 42 +++++++++++++++++++++++------------ option/rule.go | 21 +++++++++++++----- option/rule_dns.go | 25 +++++++++++++++------ route/router.go | 4 ++-- route/router_geo_resources.go | 12 ++++------ route/rule_default.go | 8 +++---- route/rule_dns.go | 8 +++---- 7 files changed, 76 insertions(+), 44 deletions(-) diff --git a/experimental/clashapi.go b/experimental/clashapi.go index 894d40a74a..805fbd5be7 100644 --- a/experimental/clashapi.go +++ b/experimental/clashapi.go @@ -5,6 +5,7 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" @@ -27,24 +28,37 @@ func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.O func CalculateClashModeList(options option.Options) []string { var clashMode []string - for _, dnsRule := range common.PtrValueOrDefault(options.DNS).Rules { - if dnsRule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, dnsRule.DefaultOptions.ClashMode) { - clashMode = append(clashMode, dnsRule.DefaultOptions.ClashMode) - } - for _, defaultRule := range dnsRule.LogicalOptions.Rules { - if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) { - clashMode = append(clashMode, defaultRule.ClashMode) + clashMode = append(clashMode, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...) + clashMode = append(clashMode, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...) + clashMode = common.FilterNotDefault(common.Uniq(clashMode)) + return clashMode +} + +func extraClashModeFromRule(rules []option.Rule) []string { + var clashMode []string + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if rule.DefaultOptions.ClashMode != "" { + clashMode = append(clashMode, rule.DefaultOptions.ClashMode) } + case C.RuleTypeLogical: + clashMode = append(clashMode, extraClashModeFromRule(rule.LogicalOptions.Rules)...) } } - for _, rule := range common.PtrValueOrDefault(options.Route).Rules { - if rule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, rule.DefaultOptions.ClashMode) { - clashMode = append(clashMode, rule.DefaultOptions.ClashMode) - } - for _, defaultRule := range rule.LogicalOptions.Rules { - if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) { - clashMode = append(clashMode, defaultRule.ClashMode) + return clashMode +} + +func extraClashModeFromDNSRule(rules []option.DNSRule) []string { + var clashMode []string + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if rule.DefaultOptions.ClashMode != "" { + clashMode = append(clashMode, rule.DefaultOptions.ClashMode) } + case C.RuleTypeLogical: + clashMode = append(clashMode, extraClashModeFromDNSRule(rule.LogicalOptions.Rules)...) } } return clashMode diff --git a/option/rule.go b/option/rule.go index 8caba96e57..4f4042025f 100644 --- a/option/rule.go +++ b/option/rule.go @@ -53,6 +53,17 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error { return nil } +func (r Rule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + type DefaultRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` @@ -92,12 +103,12 @@ func (r DefaultRule) IsValid() bool { } type LogicalRule struct { - Mode string `json:"mode"` - Rules []DefaultRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Mode string `json:"mode"` + Rules []Rule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` } func (r LogicalRule) IsValid() bool { - return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid) + return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid) } diff --git a/option/rule_dns.go b/option/rule_dns.go index ba572b9aa2..fca3432293 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -53,6 +53,17 @@ func (r *DNSRule) UnmarshalJSON(bytes []byte) error { return nil } +func (r DNSRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown DNS rule type: " + r.Type) + } +} + type DefaultDNSRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` @@ -96,14 +107,14 @@ func (r DefaultDNSRule) IsValid() bool { } type LogicalDNSRule struct { - Mode string `json:"mode"` - Rules []DefaultDNSRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + Mode string `json:"mode"` + Rules []DNSRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Server string `json:"server,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` } func (r LogicalDNSRule) IsValid() bool { - return len(r.Rules) > 0 && common.All(r.Rules, DefaultDNSRule.IsValid) + return len(r.Rules) > 0 && common.All(r.Rules, DNSRule.IsValid) } diff --git a/route/router.go b/route/router.go index e50a51a955..b48d2346d9 100644 --- a/route/router.go +++ b/route/router.go @@ -127,14 +127,14 @@ func NewRouter( Logger: router.dnsLogger, }) for i, ruleOptions := range options.Rules { - routeRule, err := NewRule(router, router.logger, ruleOptions) + routeRule, err := NewRule(router, router.logger, ruleOptions, true) if err != nil { return nil, E.Cause(err, "parse rule[", i, "]") } router.rules = append(router.rules, routeRule) } for i, dnsRuleOptions := range dnsOptions.Rules { - dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions) + dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions, true) if err != nil { return nil, E.Cause(err, "parse dns rule[", i, "]") } diff --git a/route/router_geo_resources.go b/route/router_geo_resources.go index 8715cf922c..638d00df62 100644 --- a/route/router_geo_resources.go +++ b/route/router_geo_resources.go @@ -252,10 +252,8 @@ func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool return true } case C.RuleTypeLogical: - for _, subRule := range rule.LogicalOptions.Rules { - if cond(subRule) { - return true - } + if hasRule(rule.LogicalOptions.Rules, cond) { + return true } } } @@ -270,10 +268,8 @@ func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bo return true } case C.RuleTypeLogical: - for _, subRule := range rule.LogicalOptions.Rules { - if cond(subRule) { - return true - } + if hasDNSRule(rule.LogicalOptions.Rules, cond) { + return true } } } diff --git a/route/rule_default.go b/route/rule_default.go index 2d62f97a46..8c8473abf5 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -8,13 +8,13 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule) (adapter.Rule, error) { +func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule, checkOutbound bool) (adapter.Rule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - if options.DefaultOptions.Outbound == "" { + if options.DefaultOptions.Outbound == "" && checkOutbound { return nil, E.New("missing outbound field") } return NewDefaultRule(router, logger, options.DefaultOptions) @@ -22,7 +22,7 @@ func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rul if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - if options.LogicalOptions.Outbound == "" { + if options.LogicalOptions.Outbound == "" && checkOutbound { return nil, E.New("missing outbound field") } return NewLogicalRule(router, logger, options.LogicalOptions) @@ -220,7 +220,7 @@ func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options opt return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDefaultRule(router, logger, subRule) + rule, err := NewRule(router, logger, subRule, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } diff --git a/route/rule_dns.go b/route/rule_dns.go index 5132f02456..b444932582 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -8,13 +8,13 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule) (adapter.DNSRule, error) { +func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule, checkServer bool) (adapter.DNSRule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - if options.DefaultOptions.Server == "" { + if options.DefaultOptions.Server == "" && checkServer { return nil, E.New("missing server field") } return NewDefaultDNSRule(router, logger, options.DefaultOptions) @@ -22,7 +22,7 @@ func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option. if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - if options.LogicalOptions.Server == "" { + if options.LogicalOptions.Server == "" && checkServer { return nil, E.New("missing server field") } return NewLogicalDNSRule(router, logger, options.LogicalOptions) @@ -228,7 +228,7 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDefaultDNSRule(router, logger, subRule) + rule, err := NewDNSRule(router, logger, subRule, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } From 4ae9b09d77b8ea62cbbdadd134a42163801b8478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 29 Nov 2023 17:35:40 +0800 Subject: [PATCH 041/103] Update buffer usage --- go.mod | 2 +- go.sum | 4 ++-- outbound/dns.go | 4 +--- route/router.go | 2 -- transport/vless/vision.go | 2 +- 5 files changed, 5 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 0d238773dc..eb90f235f4 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 github.com/sagernet/quic-go v0.40.0 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.2.18-0.20231201054122-bca74039ead5 + github.com/sagernet/sing v0.2.18-0.20231201060417-575186ed63c2 github.com/sagernet/sing-dns v0.1.11 github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 github.com/sagernet/sing-quic v0.1.5-0.20231123150216-00957d136203 diff --git a/go.sum b/go.sum index fb2fe317f0..6641d6692a 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= -github.com/sagernet/sing v0.2.18-0.20231201054122-bca74039ead5 h1:luykfsWNqFh9sdLXlkCQtkuzLUPRd3BMsdQJt0REB1g= -github.com/sagernet/sing v0.2.18-0.20231201054122-bca74039ead5/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= +github.com/sagernet/sing v0.2.18-0.20231201060417-575186ed63c2 h1:1ydWkFgLURGlrnwRdjyrpo9lp1g5Qq7XrNBghMntWTs= +github.com/sagernet/sing v0.2.18-0.20231201060417-575186ed63c2/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE= github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE= github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM= diff --git a/outbound/dns.go b/outbound/dns.go index 3b2ad5e3ca..81b55b222a 100644 --- a/outbound/dns.go +++ b/outbound/dns.go @@ -194,9 +194,7 @@ func (d *DNS) newPacketConnection(ctx context.Context, conn N.PacketConn, readWa group.Append0(func(ctx context.Context) error { var buffer *buf.Buffer readWaiter.InitializeReadWaiter(func() *buf.Buffer { - buffer = buf.NewSize(dns.FixedPacketSize) - buffer.FullReset() - return buffer + return buf.NewSize(dns.FixedPacketSize) }) defer readWaiter.InitializeReadWaiter(nil) for { diff --git a/route/router.go b/route/router.go index b48d2346d9..09a3e8e6e9 100644 --- a/route/router.go +++ b/route/router.go @@ -646,7 +646,6 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad if metadata.InboundOptions.SniffEnabled { buffer := buf.NewPacket() - buffer.FullReset() sniffMetadata, err := sniff.PeekStream(ctx, conn, buffer, time.Duration(metadata.InboundOptions.SniffTimeout), sniff.StreamDomainNameQuery, sniff.TLSClientHello, sniff.HTTPHost) if sniffMetadata != nil { metadata.Protocol = sniffMetadata.Protocol @@ -762,7 +761,6 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m if metadata.InboundOptions.SniffEnabled || metadata.Destination.Addr.IsUnspecified() { buffer := buf.NewPacket() - buffer.FullReset() destination, err := conn.ReadPacket(buffer) if err != nil { buffer.Release() diff --git a/transport/vless/vision.go b/transport/vless/vision.go index 3851f6d52e..6919ce8176 100644 --- a/transport/vless/vision.go +++ b/transport/vless/vision.go @@ -134,7 +134,7 @@ func (c *VisionConn) Read(p []byte) (n int, err error) { buffers = common.Map(buffers, func(it *buf.Buffer) *buf.Buffer { return it.ToOwned() }) - chunkBuffer.FullReset() + chunkBuffer.Reset() } if c.remainingContent == 0 && c.remainingPadding == 0 { if c.currentCommand == commandPaddingEnd { From 5d225457ef56ede9da955e8fa3992f37ca57b1c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 1 Dec 2023 13:24:12 +0800 Subject: [PATCH 042/103] Add rule-set --- .gitignore | 1 + adapter/experimental.go | 66 ++- adapter/inbound.go | 17 +- adapter/router.go | 25 +- box.go | 4 + cmd/sing-box/cmd_format.go | 39 -- cmd/sing-box/cmd_geoip.go | 43 ++ cmd/sing-box/cmd_geoip_export.go | 98 ++++ cmd/sing-box/cmd_geoip_list.go | 31 ++ cmd/sing-box/cmd_geoip_lookup.go | 47 ++ cmd/sing-box/cmd_geosite.go | 41 ++ cmd/sing-box/cmd_geosite_export.go | 81 +++ cmd/sing-box/cmd_geosite_list.go | 50 ++ cmd/sing-box/cmd_geosite_lookup.go | 97 ++++ cmd/sing-box/cmd_geosite_matcher.go | 56 ++ cmd/sing-box/cmd_merge.go | 2 +- cmd/sing-box/cmd_rule_set.go | 14 + cmd/sing-box/cmd_rule_set_compile.go | 80 +++ cmd/sing-box/cmd_rule_set_format.go | 87 ++++ cmd/sing-box/cmd_tools.go | 6 +- cmd/sing-box/cmd_tools_connect.go | 2 +- common/dialer/router.go | 12 +- common/srs/binary.go | 485 ++++++++++++++++++ common/srs/ip_set.go | 116 +++++ constant/rule.go | 8 + experimental/cachefile/cache.go | 35 ++ experimental/cachefile/fakeip.go | 2 +- experimental/clashapi/proxies.go | 6 +- experimental/clashapi/server_resources.go | 6 +- .../clashapi/trafficontrol/tracker.go | 8 +- option/experimental.go | 8 +- option/route.go | 1 + option/rule.go | 58 ++- option/rule_dns.go | 1 + option/rule_set.go | 230 +++++++++ option/time_unit.go | 226 ++++++++ option/types.go | 10 +- route/router.go | 75 ++- route/router_dns.go | 1 + route/rule_abstract.go | 81 +-- route/rule_default.go | 7 +- route/rule_dns.go | 7 +- route/rule_headless.go | 173 +++++++ route/rule_item_cidr.go | 19 +- route/rule_item_domain.go | 7 + route/rule_item_rule_set.go | 55 ++ route/rule_set.go | 67 +++ route/rule_set_local.go | 74 +++ route/rule_set_remote.go | 236 +++++++++ 49 files changed, 2762 insertions(+), 139 deletions(-) create mode 100644 cmd/sing-box/cmd_geoip.go create mode 100644 cmd/sing-box/cmd_geoip_export.go create mode 100644 cmd/sing-box/cmd_geoip_list.go create mode 100644 cmd/sing-box/cmd_geoip_lookup.go create mode 100644 cmd/sing-box/cmd_geosite.go create mode 100644 cmd/sing-box/cmd_geosite_export.go create mode 100644 cmd/sing-box/cmd_geosite_list.go create mode 100644 cmd/sing-box/cmd_geosite_lookup.go create mode 100644 cmd/sing-box/cmd_geosite_matcher.go create mode 100644 cmd/sing-box/cmd_rule_set.go create mode 100644 cmd/sing-box/cmd_rule_set_compile.go create mode 100644 cmd/sing-box/cmd_rule_set_format.go create mode 100644 common/srs/binary.go create mode 100644 common/srs/ip_set.go create mode 100644 option/rule_set.go create mode 100644 option/time_unit.go create mode 100644 route/rule_headless.go create mode 100644 route/rule_item_rule_set.go create mode 100644 route/rule_set.go create mode 100644 route/rule_set_local.go create mode 100644 route/rule_set_remote.go diff --git a/.gitignore b/.gitignore index 6630f428ce..55bdab3a0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.idea/ /vendor/ /*.json +/*.srs /*.db /site/ /bin/ diff --git a/adapter/experimental.go b/adapter/experimental.go index ac523e4e74..c26ee8c1cf 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -1,11 +1,16 @@ package adapter import ( + "bytes" "context" + "encoding/binary" + "io" "net" + "time" "github.com/sagernet/sing-box/common/urltest" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" ) type ClashServer interface { @@ -23,6 +28,7 @@ type CacheFile interface { PreStarter StoreFakeIP() bool + FakeIPStorage LoadMode() string StoreMode(mode string) error @@ -30,7 +36,65 @@ type CacheFile interface { StoreSelected(group string, selected string) error LoadGroupExpand(group string) (isExpand bool, loaded bool) StoreGroupExpand(group string, expand bool) error - FakeIPStorage + LoadRuleSet(tag string) *SavedRuleSet + SaveRuleSet(tag string, set *SavedRuleSet) error +} + +type SavedRuleSet struct { + Content []byte + LastUpdated time.Time + LastEtag string +} + +func (s *SavedRuleSet) MarshalBinary() ([]byte, error) { + var buffer bytes.Buffer + err := binary.Write(&buffer, binary.BigEndian, uint8(1)) + if err != nil { + return nil, err + } + err = rw.WriteUVariant(&buffer, uint64(len(s.Content))) + if err != nil { + return nil, err + } + buffer.Write(s.Content) + err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix()) + if err != nil { + return nil, err + } + err = rw.WriteVString(&buffer, s.LastEtag) + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func (s *SavedRuleSet) UnmarshalBinary(data []byte) error { + reader := bytes.NewReader(data) + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return err + } + contentLen, err := rw.ReadUVariant(reader) + if err != nil { + return err + } + s.Content = make([]byte, contentLen) + _, err = io.ReadFull(reader, s.Content) + if err != nil { + return err + } + var lastUpdated int64 + err = binary.Read(reader, binary.BigEndian, &lastUpdated) + if err != nil { + return err + } + s.LastUpdated = time.Unix(lastUpdated, 0) + s.LastEtag, err = rw.ReadVString(reader) + if err != nil { + return err + } + return nil } type Tracker interface { diff --git a/adapter/inbound.go b/adapter/inbound.go index 2d24083c4a..f32b804d21 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -46,11 +46,24 @@ type InboundContext struct { SourceGeoIPCode string GeoIPCode string ProcessInfo *process.Info + QueryType uint16 FakeIP bool - // dns cache + // rule cache - QueryType uint16 + IPCIDRMatchSource bool + SourceAddressMatch bool + SourcePortMatch bool + DestinationAddressMatch bool + DestinationPortMatch bool +} + +func (c *InboundContext) ResetRuleCache() { + c.IPCIDRMatchSource = false + c.SourceAddressMatch = false + c.SourcePortMatch = false + c.DestinationAddressMatch = false + c.DestinationPortMatch = false } type inboundContextKey struct{} diff --git a/adapter/router.go b/adapter/router.go index e4c3904d51..707a06cde9 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -2,12 +2,14 @@ package adapter import ( "context" + "net/http" "net/netip" "github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-dns" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" + N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" mdns "github.com/miekg/dns" @@ -15,10 +17,11 @@ import ( type Router interface { Service + PostStarter Outbounds() []Outbound Outbound(tag string) (Outbound, bool) - DefaultOutbound(network string) Outbound + DefaultOutbound(network string) (Outbound, error) FakeIPStore() FakeIPStore @@ -27,6 +30,8 @@ type Router interface { GeoIPReader() *geoip.Reader LoadGeosite(code string) (Rule, error) + RuleSet(tag string) (RuleSet, bool) + Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) @@ -61,11 +66,15 @@ func RouterFromContext(ctx context.Context) Router { return service.FromContext[Router](ctx) } +type HeadlessRule interface { + Match(metadata *InboundContext) bool +} + type Rule interface { + HeadlessRule Service Type() string UpdateGeosite() error - Match(metadata *InboundContext) bool Outbound() string String() string } @@ -76,6 +85,18 @@ type DNSRule interface { RewriteTTL() *uint32 } +type RuleSet interface { + StartContext(ctx context.Context, startContext RuleSetStartContext) error + PostStart() error + Close() error + HeadlessRule +} + +type RuleSetStartContext interface { + HTTPClient(detour string, dialer N.Dialer) *http.Client + Close() +} + type InterfaceUpdateListener interface { InterfaceUpdated() } diff --git a/box.go b/box.go index d3838ca337..7eb2856683 100644 --- a/box.go +++ b/box.go @@ -307,6 +307,10 @@ func (s *Box) postStart() error { } } } + err := s.router.PostStart() + if err != nil { + return E.Cause(err, "post-start router") + } return nil } diff --git a/cmd/sing-box/cmd_format.go b/cmd/sing-box/cmd_format.go index 10a5497cd4..c5e939e4a9 100644 --- a/cmd/sing-box/cmd_format.go +++ b/cmd/sing-box/cmd_format.go @@ -7,7 +7,6 @@ import ( "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/spf13/cobra" @@ -69,41 +68,3 @@ func format() error { } return nil } - -func formatOne(configPath string) error { - configContent, err := os.ReadFile(configPath) - if err != nil { - return E.Cause(err, "read config") - } - var options option.Options - err = options.UnmarshalJSON(configContent) - if err != nil { - return E.Cause(err, "decode config") - } - buffer := new(bytes.Buffer) - encoder := json.NewEncoder(buffer) - encoder.SetIndent("", " ") - err = encoder.Encode(options) - if err != nil { - return E.Cause(err, "encode config") - } - if !commandFormatFlagWrite { - os.Stdout.WriteString(buffer.String() + "\n") - return nil - } - if bytes.Equal(configContent, buffer.Bytes()) { - return nil - } - output, err := os.Create(configPath) - if err != nil { - return E.Cause(err, "open output") - } - _, err = output.Write(buffer.Bytes()) - output.Close() - if err != nil { - return E.Cause(err, "write output") - } - outputPath, _ := filepath.Abs(configPath) - os.Stderr.WriteString(outputPath + "\n") - return nil -} diff --git a/cmd/sing-box/cmd_geoip.go b/cmd/sing-box/cmd_geoip.go new file mode 100644 index 0000000000..dbbbff135e --- /dev/null +++ b/cmd/sing-box/cmd_geoip.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var ( + geoipReader *maxminddb.Reader + commandGeoIPFlagFile string +) + +var commandGeoip = &cobra.Command{ + Use: "geoip", + Short: "GeoIP tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geoipPreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.PersistentFlags().StringVarP(&commandGeoIPFlagFile, "file", "f", "geoip.db", "geoip file") + mainCommand.AddCommand(commandGeoip) +} + +func geoipPreRun() error { + reader, err := maxminddb.Open(commandGeoIPFlagFile) + if err != nil { + return err + } + if reader.Metadata.DatabaseType != "sing-geoip" { + reader.Close() + return E.New("incorrect database type, expected sing-geoip, got ", reader.Metadata.DatabaseType) + } + geoipReader = reader + return nil +} diff --git a/cmd/sing-box/cmd_geoip_export.go b/cmd/sing-box/cmd_geoip_export.go new file mode 100644 index 0000000000..d170d10b72 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_export.go @@ -0,0 +1,98 @@ +package main + +import ( + "io" + "net" + "os" + "strings" + + "github.com/sagernet/sing-box/common/json" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var flagGeoipExportOutput string + +const flagGeoipExportDefaultOutput = "geoip-.srs" + +var commandGeoipExport = &cobra.Command{ + Use: "export ", + Short: "Export geoip country as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoipExport.Flags().StringVarP(&flagGeoipExportOutput, "output", "o", flagGeoipExportDefaultOutput, "Output path") + commandGeoip.AddCommand(commandGeoipExport) +} + +func geoipExport(countryCode string) error { + networks := geoipReader.Networks(maxminddb.SkipAliasedNetworks) + countryMap := make(map[string][]*net.IPNet) + var ( + ipNet *net.IPNet + nextCountryCode string + err error + ) + for networks.Next() { + ipNet, err = networks.Network(&nextCountryCode) + if err != nil { + return err + } + countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet) + } + ipNets := countryMap[strings.ToLower(countryCode)] + if len(ipNets) == 0 { + return E.New("country code not found: ", countryCode) + } + + var ( + outputFile *os.File + outputWriter io.Writer + ) + if flagGeoipExportOutput == "stdout" { + outputWriter = os.Stdout + } else if flagGeoipExportOutput == flagGeoipExportDefaultOutput { + outputFile, err = os.Create("geoip-" + countryCode + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(flagGeoipExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + headlessRule.IPCIDR = make([]string, 0, len(ipNets)) + for _, cidr := range ipNets { + headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String()) + } + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geoip_list.go b/cmd/sing-box/cmd_geoip_list.go new file mode 100644 index 0000000000..54dd426ea9 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_list.go @@ -0,0 +1,31 @@ +package main + +import ( + "os" + + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var commandGeoipList = &cobra.Command{ + Use: "list", + Short: "List geoip country codes", + Run: func(cmd *cobra.Command, args []string) { + err := listGeoip() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipList) +} + +func listGeoip() error { + for _, code := range geoipReader.Metadata.Languages { + os.Stdout.WriteString(code + "\n") + } + return nil +} diff --git a/cmd/sing-box/cmd_geoip_lookup.go b/cmd/sing-box/cmd_geoip_lookup.go new file mode 100644 index 0000000000..d5157bb404 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_lookup.go @@ -0,0 +1,47 @@ +package main + +import ( + "net/netip" + "os" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + + "github.com/spf13/cobra" +) + +var commandGeoipLookup = &cobra.Command{ + Use: "lookup
", + Short: "Lookup if an IP address is contained in the GeoIP database", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipLookup(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipLookup) +} + +func geoipLookup(address string) error { + addr, err := netip.ParseAddr(address) + if err != nil { + return E.Cause(err, "parse address") + } + if !N.IsPublicAddr(addr) { + os.Stdout.WriteString("private\n") + return nil + } + var code string + _ = geoipReader.Lookup(addr.AsSlice(), &code) + if code != "" { + os.Stdout.WriteString(code + "\n") + return nil + } + os.Stdout.WriteString("unknown\n") + return nil +} diff --git a/cmd/sing-box/cmd_geosite.go b/cmd/sing-box/cmd_geosite.go new file mode 100644 index 0000000000..95db935797 --- /dev/null +++ b/cmd/sing-box/cmd_geosite.go @@ -0,0 +1,41 @@ +package main + +import ( + "github.com/sagernet/sing-box/common/geosite" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var ( + commandGeoSiteFlagFile string + geositeReader *geosite.Reader + geositeCodeList []string +) + +var commandGeoSite = &cobra.Command{ + Use: "geosite", + Short: "Geosite tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geositePreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.PersistentFlags().StringVarP(&commandGeoSiteFlagFile, "file", "f", "geosite.db", "geosite file") + mainCommand.AddCommand(commandGeoSite) +} + +func geositePreRun() error { + reader, codeList, err := geosite.Open(commandGeoSiteFlagFile) + if err != nil { + return E.Cause(err, "open geosite file") + } + geositeReader = reader + geositeCodeList = codeList + return nil +} diff --git a/cmd/sing-box/cmd_geosite_export.go b/cmd/sing-box/cmd_geosite_export.go new file mode 100644 index 0000000000..71f1018d8f --- /dev/null +++ b/cmd/sing-box/cmd_geosite_export.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/json" + "io" + "os" + + "github.com/sagernet/sing-box/common/geosite" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "github.com/spf13/cobra" +) + +var commandGeositeExportOutput string + +const commandGeositeExportDefaultOutput = "geosite-.json" + +var commandGeositeExport = &cobra.Command{ + Use: "export ", + Short: "Export geosite category as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geositeExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeositeExport.Flags().StringVarP(&commandGeositeExportOutput, "output", "o", commandGeositeExportDefaultOutput, "Output path") + commandGeoSite.AddCommand(commandGeositeExport) +} + +func geositeExport(category string) error { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + var ( + outputFile *os.File + outputWriter io.Writer + ) + if commandGeositeExportOutput == "stdout" { + outputWriter = os.Stdout + } else if commandGeositeExportOutput == commandGeositeExportDefaultOutput { + outputFile, err = os.Create("geosite-" + category + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(commandGeositeExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + defaultRule := geosite.Compile(sourceSet) + headlessRule.Domain = defaultRule.Domain + headlessRule.DomainSuffix = defaultRule.DomainSuffix + headlessRule.DomainKeyword = defaultRule.DomainKeyword + headlessRule.DomainRegex = defaultRule.DomainRegex + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geosite_list.go b/cmd/sing-box/cmd_geosite_list.go new file mode 100644 index 0000000000..cedb7adfd2 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_list.go @@ -0,0 +1,50 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + F "github.com/sagernet/sing/common/format" + + "github.com/spf13/cobra" +) + +var commandGeositeList = &cobra.Command{ + Use: "list ", + Short: "List geosite categories", + Run: func(cmd *cobra.Command, args []string) { + err := geositeList() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeList) +} + +func geositeList() error { + var geositeEntry []struct { + category string + items int + } + for _, category := range geositeCodeList { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + geositeEntry = append(geositeEntry, struct { + category string + items int + }{category, len(sourceSet)}) + } + sort.SliceStable(geositeEntry, func(i, j int) bool { + return geositeEntry[i].items < geositeEntry[j].items + }) + for _, entry := range geositeEntry { + os.Stdout.WriteString(F.ToString(entry.category, " (", entry.items, ")\n")) + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_lookup.go b/cmd/sing-box/cmd_geosite_lookup.go new file mode 100644 index 0000000000..f648ce62a4 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_lookup.go @@ -0,0 +1,97 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandGeositeLookup = &cobra.Command{ + Use: "lookup [category] ", + Short: "Check if a domain is in the geosite", + Args: cobra.RangeArgs(1, 2), + Run: func(cmd *cobra.Command, args []string) { + var ( + source string + target string + ) + switch len(args) { + case 1: + target = args[0] + case 2: + source = args[0] + target = args[1] + } + err := geositeLookup(source, target) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeLookup) +} + +func geositeLookup(source string, target string) error { + var sourceMatcherList []struct { + code string + matcher *searchGeositeMatcher + } + if source != "" { + sourceSet, err := geositeReader.Read(source) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+source) + } + sourceMatcherList = []struct { + code string + matcher *searchGeositeMatcher + }{ + { + code: source, + matcher: sourceMatcher, + }, + } + + } else { + for _, code := range geositeCodeList { + sourceSet, err := geositeReader.Read(code) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+code) + } + sourceMatcherList = append(sourceMatcherList, struct { + code string + matcher *searchGeositeMatcher + }{ + code: code, + matcher: sourceMatcher, + }) + } + } + sort.SliceStable(sourceMatcherList, func(i, j int) bool { + return sourceMatcherList[i].code < sourceMatcherList[j].code + }) + + for _, matcherItem := range sourceMatcherList { + if matchRule := matcherItem.matcher.Match(target); matchRule != "" { + os.Stdout.WriteString("Match code (") + os.Stdout.WriteString(matcherItem.code) + os.Stdout.WriteString(") ") + os.Stdout.WriteString(matchRule) + os.Stdout.WriteString("\n") + } + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_matcher.go b/cmd/sing-box/cmd_geosite_matcher.go new file mode 100644 index 0000000000..791dba2499 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_matcher.go @@ -0,0 +1,56 @@ +package main + +import ( + "regexp" + "strings" + + "github.com/sagernet/sing-box/common/geosite" +) + +type searchGeositeMatcher struct { + domainMap map[string]bool + suffixList []string + keywordList []string + regexList []string +} + +func newSearchGeositeMatcher(items []geosite.Item) (*searchGeositeMatcher, error) { + options := geosite.Compile(items) + domainMap := make(map[string]bool) + for _, domain := range options.Domain { + domainMap[domain] = true + } + rule := &searchGeositeMatcher{ + domainMap: domainMap, + suffixList: options.DomainSuffix, + keywordList: options.DomainKeyword, + regexList: options.DomainRegex, + } + return rule, nil +} + +func (r *searchGeositeMatcher) Match(domain string) string { + if r.domainMap[domain] { + return "domain=" + domain + } + for _, suffix := range r.suffixList { + if strings.HasSuffix(domain, suffix) { + return "domain_suffix=" + suffix + } + } + for _, keyword := range r.keywordList { + if strings.Contains(domain, keyword) { + return "domain_keyword=" + keyword + } + } + for _, regexStr := range r.regexList { + regex, err := regexp.Compile(regexStr) + if err != nil { + continue + } + if regex.MatchString(domain) { + return "domain_regex=" + regexStr + } + } + return "" +} diff --git a/cmd/sing-box/cmd_merge.go b/cmd/sing-box/cmd_merge.go index 0aff750182..4fb07b8688 100644 --- a/cmd/sing-box/cmd_merge.go +++ b/cmd/sing-box/cmd_merge.go @@ -18,7 +18,7 @@ import ( ) var commandMerge = &cobra.Command{ - Use: "merge [output]", + Use: "merge ", Short: "Merge configurations", Run: func(cmd *cobra.Command, args []string) { err := merge(args[0]) diff --git a/cmd/sing-box/cmd_rule_set.go b/cmd/sing-box/cmd_rule_set.go new file mode 100644 index 0000000000..f4112a087b --- /dev/null +++ b/cmd/sing-box/cmd_rule_set.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +var commandRuleSet = &cobra.Command{ + Use: "rule-set", + Short: "Manage rule sets", +} + +func init() { + mainCommand.AddCommand(commandRuleSet) +} diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go new file mode 100644 index 0000000000..de318095ac --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -0,0 +1,80 @@ +package main + +import ( + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "github.com/spf13/cobra" +) + +var flagRuleSetCompileOutput string + +const flagRuleSetCompileDefaultOutput = ".srs" + +var commandRuleSetCompile = &cobra.Command{ + Use: "compile [source-path]", + Short: "Compile rule-set json to binary", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := compileRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSet.AddCommand(commandRuleSetCompile) + commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file") +} + +func compileRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + decoder := json.NewDecoder(json.NewCommentFilter(reader)) + decoder.DisallowUnknownFields() + var plainRuleSet option.PlainRuleSetCompat + err = decoder.Decode(&plainRuleSet) + if err != nil { + return err + } + ruleSet := plainRuleSet.Upgrade() + var outputPath string + if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { + if strings.HasSuffix(sourcePath, ".json") { + outputPath = sourcePath[:len(sourcePath)-5] + ".srs" + } else { + outputPath = sourcePath + ".srs" + } + } else { + outputPath = flagRuleSetCompileOutput + } + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + err = srs.Write(outputFile, ruleSet) + if err != nil { + outputFile.Close() + os.Remove(outputPath) + return err + } + outputFile.Close() + return nil +} diff --git a/cmd/sing-box/cmd_rule_set_format.go b/cmd/sing-box/cmd_rule_set_format.go new file mode 100644 index 0000000000..dc3ee6aabd --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_format.go @@ -0,0 +1,87 @@ +package main + +import ( + "bytes" + "io" + "os" + "path/filepath" + + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandRuleSetFormatFlagWrite bool + +var commandRuleSetFormat = &cobra.Command{ + Use: "format ", + Short: "Format rule-set json", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := formatRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") + commandRuleSet.AddCommand(commandRuleSetFormat) +} + +func formatRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + content, err := io.ReadAll(reader) + if err != nil { + return err + } + decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content))) + decoder.DisallowUnknownFields() + var plainRuleSet option.PlainRuleSetCompat + err = decoder.Decode(&plainRuleSet) + if err != nil { + return err + } + ruleSet := plainRuleSet.Upgrade() + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(ruleSet) + if err != nil { + return E.Cause(err, "encode config") + } + outputPath, _ := filepath.Abs(sourcePath) + if !commandRuleSetFormatFlagWrite || sourcePath == "stdin" { + os.Stdout.WriteString(buffer.String() + "\n") + return nil + } + if bytes.Equal(content, buffer.Bytes()) { + return nil + } + output, err := os.Create(sourcePath) + if err != nil { + return E.Cause(err, "open output") + } + _, err = output.Write(buffer.Bytes()) + output.Close() + if err != nil { + return E.Cause(err, "write output") + } + os.Stderr.WriteString(outputPath + "\n") + return nil +} diff --git a/cmd/sing-box/cmd_tools.go b/cmd/sing-box/cmd_tools.go index 460a50cd1c..c45f585576 100644 --- a/cmd/sing-box/cmd_tools.go +++ b/cmd/sing-box/cmd_tools.go @@ -38,11 +38,7 @@ func createPreStartedClient() (*box.Box, error) { func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) { if outboundTag == "" { - outbound := instance.Router().DefaultOutbound(N.NetworkName(network)) - if outbound == nil { - return nil, E.New("missing default outbound") - } - return outbound, nil + return instance.Router().DefaultOutbound(N.NetworkName(network)) } else { outbound, loaded := instance.Router().Outbound(outboundTag) if !loaded { diff --git a/cmd/sing-box/cmd_tools_connect.go b/cmd/sing-box/cmd_tools_connect.go index b904ebc9f5..3ea04bcd40 100644 --- a/cmd/sing-box/cmd_tools_connect.go +++ b/cmd/sing-box/cmd_tools_connect.go @@ -18,7 +18,7 @@ import ( var commandConnectFlagNetwork string var commandConnect = &cobra.Command{ - Use: "connect [address]", + Use: "connect
", Short: "Connect to an address", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { diff --git a/common/dialer/router.go b/common/dialer/router.go index 1d5586546e..2531607753 100644 --- a/common/dialer/router.go +++ b/common/dialer/router.go @@ -18,11 +18,19 @@ func NewRouter(router adapter.Router) N.Dialer { } func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - return d.router.DefaultOutbound(network).DialContext(ctx, network, destination) + dialer, err := d.router.DefaultOutbound(network) + if err != nil { + return nil, err + } + return dialer.DialContext(ctx, network, destination) } func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return d.router.DefaultOutbound(N.NetworkUDP).ListenPacket(ctx, destination) + dialer, err := d.router.DefaultOutbound(N.NetworkUDP) + if err != nil { + return nil, err + } + return dialer.ListenPacket(ctx, destination) } func (d *RouterDialer) Upstream() any { diff --git a/common/srs/binary.go b/common/srs/binary.go new file mode 100644 index 0000000000..dd994c2c29 --- /dev/null +++ b/common/srs/binary.go @@ -0,0 +1,485 @@ +package srs + +import ( + "compress/zlib" + "encoding/binary" + "io" + "net/netip" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/rw" + + "go4.org/netipx" +) + +var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS + +const ( + ruleItemQueryType uint8 = iota + ruleItemNetwork + ruleItemDomain + ruleItemDomainKeyword + ruleItemDomainRegex + ruleItemSourceIPCIDR + ruleItemIPCIDR + ruleItemSourcePort + ruleItemSourcePortRange + ruleItemPort + ruleItemPortRange + ruleItemProcessName + ruleItemProcessPath + ruleItemPackageName + ruleItemWIFISSID + ruleItemWIFIBSSID + ruleItemFinal uint8 = 0xFF +) + +func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) { + var magicBytes [3]byte + _, err = io.ReadFull(reader, magicBytes[:]) + if err != nil { + return + } + if magicBytes != MagicBytes { + err = E.New("invalid sing-box rule set file") + return + } + var version uint8 + err = binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return ruleSet, err + } + if version != 1 { + return ruleSet, E.New("unsupported version: ", version) + } + zReader, err := zlib.NewReader(reader) + if err != nil { + return + } + length, err := rw.ReadUVariant(zReader) + if err != nil { + return + } + ruleSet.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + ruleSet.Rules[i], err = readRule(zReader, recovery) + if err != nil { + err = E.Cause(err, "read rule[", i, "]") + return + } + } + return +} + +func Write(writer io.Writer, ruleSet option.PlainRuleSet) error { + _, err := writer.Write(MagicBytes[:]) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) + if err != nil { + return err + } + err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules))) + if err != nil { + return err + } + for _, rule := range ruleSet.Rules { + err = writeRule(zWriter, rule) + if err != nil { + return err + } + } + return zWriter.Close() +} + +func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) { + var ruleType uint8 + err = binary.Read(reader, binary.BigEndian, &ruleType) + if err != nil { + return + } + switch ruleType { + case 0: + rule.DefaultOptions, err = readDefaultRule(reader, recovery) + case 1: + rule.LogicalOptions, err = readLogicalRule(reader, recovery) + default: + err = E.New("unknown rule type: ", ruleType) + } + return +} + +func writeRule(writer io.Writer, rule option.HeadlessRule) error { + switch rule.Type { + case C.RuleTypeDefault: + return writeDefaultRule(writer, rule.DefaultOptions) + case C.RuleTypeLogical: + return writeLogicalRule(writer, rule.LogicalOptions) + default: + panic("unknown rule type: " + rule.Type) + } +} + +func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) { + var lastItemType uint8 + for { + var itemType uint8 + err = binary.Read(reader, binary.BigEndian, &itemType) + if err != nil { + return + } + switch itemType { + case ruleItemQueryType: + var rawQueryType []uint16 + rawQueryType, err = readRuleItemUint16(reader) + if err != nil { + return + } + rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType { + return option.DNSQueryType(it) + }) + case ruleItemNetwork: + rule.Network, err = readRuleItemString(reader) + case ruleItemDomain: + var matcher *domain.Matcher + matcher, err = domain.ReadMatcher(reader) + if err != nil { + return + } + rule.DomainMatcher = matcher + case ruleItemDomainKeyword: + rule.DomainKeyword, err = readRuleItemString(reader) + case ruleItemDomainRegex: + rule.DomainRegex, err = readRuleItemString(reader) + case ruleItemSourceIPCIDR: + rule.SourceIPSet, err = readIPSet(reader) + if err != nil { + return + } + if recovery { + rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemIPCIDR: + rule.IPSet, err = readIPSet(reader) + if err != nil { + return + } + if recovery { + rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemSourcePort: + rule.SourcePort, err = readRuleItemUint16(reader) + case ruleItemSourcePortRange: + rule.SourcePortRange, err = readRuleItemString(reader) + case ruleItemPort: + rule.Port, err = readRuleItemUint16(reader) + case ruleItemPortRange: + rule.PortRange, err = readRuleItemString(reader) + case ruleItemProcessName: + rule.ProcessName, err = readRuleItemString(reader) + case ruleItemProcessPath: + rule.ProcessPath, err = readRuleItemString(reader) + case ruleItemPackageName: + rule.PackageName, err = readRuleItemString(reader) + case ruleItemWIFISSID: + rule.WIFISSID, err = readRuleItemString(reader) + case ruleItemWIFIBSSID: + rule.WIFIBSSID, err = readRuleItemString(reader) + case ruleItemFinal: + err = binary.Read(reader, binary.BigEndian, &rule.Invert) + return + default: + err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType) + } + if err != nil { + return + } + lastItemType = itemType + } +} + +func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error { + err := binary.Write(writer, binary.BigEndian, uint8(0)) + if err != nil { + return err + } + if len(rule.QueryType) > 0 { + err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 { + return uint16(it) + })) + if err != nil { + return err + } + } + if len(rule.Network) > 0 { + err = writeRuleItemString(writer, ruleItemNetwork, rule.Network) + if err != nil { + return err + } + } + if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 { + err = binary.Write(writer, binary.BigEndian, ruleItemDomain) + if err != nil { + return err + } + err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer) + if err != nil { + return err + } + } + if len(rule.DomainKeyword) > 0 { + err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword) + if err != nil { + return err + } + } + if len(rule.DomainRegex) > 0 { + err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex) + if err != nil { + return err + } + } + if len(rule.SourceIPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR) + if err != nil { + return E.Cause(err, "source_ipcidr") + } + } + if len(rule.IPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR) + if err != nil { + return E.Cause(err, "ipcidr") + } + } + if len(rule.SourcePort) > 0 { + err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort) + if err != nil { + return err + } + } + if len(rule.SourcePortRange) > 0 { + err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange) + if err != nil { + return err + } + } + if len(rule.Port) > 0 { + err = writeRuleItemUint16(writer, ruleItemPort, rule.Port) + if err != nil { + return err + } + } + if len(rule.PortRange) > 0 { + err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange) + if err != nil { + return err + } + } + if len(rule.ProcessName) > 0 { + err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName) + if err != nil { + return err + } + } + if len(rule.ProcessPath) > 0 { + err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath) + if err != nil { + return err + } + } + if len(rule.PackageName) > 0 { + err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName) + if err != nil { + return err + } + } + if len(rule.WIFISSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) + if err != nil { + return err + } + } + if len(rule.WIFIBSSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, ruleItemFinal) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, rule.Invert) + if err != nil { + return err + } + return nil +} + +func readRuleItemString(reader io.Reader) ([]string, error) { + length, err := rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + value := make([]string, length) + for i := uint64(0); i < length; i++ { + value[i], err = rw.ReadVString(reader) + if err != nil { + return nil, err + } + } + return value, nil +} + +func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error { + err := binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(value))) + if err != nil { + return err + } + for _, item := range value { + err = rw.WriteVString(writer, item) + if err != nil { + return err + } + } + return nil +} + +func readRuleItemUint16(reader io.Reader) ([]uint16, error) { + length, err := rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + value := make([]uint16, length) + for i := uint64(0); i < length; i++ { + err = binary.Read(reader, binary.BigEndian, &value[i]) + if err != nil { + return nil, err + } + } + return value, nil +} + +func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error { + err := binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(value))) + if err != nil { + return err + } + for _, item := range value { + err = binary.Write(writer, binary.BigEndian, item) + if err != nil { + return err + } + } + return nil +} + +func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error { + var builder netipx.IPSetBuilder + for i, prefixString := range value { + prefix, err := netip.ParsePrefix(prefixString) + if err == nil { + builder.AddPrefix(prefix) + continue + } + addr, addrErr := netip.ParseAddr(prefixString) + if addrErr == nil { + builder.Add(addr) + continue + } + return E.Cause(err, "parse [", i, "]") + } + ipSet, err := builder.IPSet() + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + return writeIPSet(writer, ipSet) +} + +func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { + var mode uint8 + err = binary.Read(reader, binary.BigEndian, &mode) + if err != nil { + return + } + switch mode { + case 0: + logicalRule.Mode = C.LogicalTypeAnd + case 1: + logicalRule.Mode = C.LogicalTypeOr + default: + err = E.New("unknown logical mode: ", mode) + return + } + length, err := rw.ReadUVariant(reader) + if err != nil { + return + } + logicalRule.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + logicalRule.Rules[i], err = readRule(reader, recovery) + if err != nil { + err = E.Cause(err, "read logical rule [", i, "]") + return + } + } + err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert) + if err != nil { + return + } + return +} + +func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + switch logicalRule.Mode { + case C.LogicalTypeAnd: + err = binary.Write(writer, binary.BigEndian, uint8(0)) + case C.LogicalTypeOr: + err = binary.Write(writer, binary.BigEndian, uint8(1)) + default: + panic("unknown logical mode: " + logicalRule.Mode) + } + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules))) + if err != nil { + return err + } + for _, rule := range logicalRule.Rules { + err = writeRule(writer, rule) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, logicalRule.Invert) + if err != nil { + return err + } + return nil +} diff --git a/common/srs/ip_set.go b/common/srs/ip_set.go new file mode 100644 index 0000000000..b346da26f6 --- /dev/null +++ b/common/srs/ip_set.go @@ -0,0 +1,116 @@ +package srs + +import ( + "encoding/binary" + "io" + "net/netip" + "unsafe" + + "github.com/sagernet/sing/common/rw" + + "go4.org/netipx" +) + +type myIPSet struct { + rr []myIPRange +} + +type myIPRange struct { + from netip.Addr + to netip.Addr +} + +func readIPSet(reader io.Reader) (*netipx.IPSet, error) { + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return nil, err + } + var length uint64 + err = binary.Read(reader, binary.BigEndian, &length) + if err != nil { + return nil, err + } + mySet := &myIPSet{ + rr: make([]myIPRange, length), + } + for i := uint64(0); i < length; i++ { + var ( + fromLen uint64 + toLen uint64 + fromAddr netip.Addr + toAddr netip.Addr + ) + fromLen, err = rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + fromBytes := make([]byte, fromLen) + _, err = io.ReadFull(reader, fromBytes) + if err != nil { + return nil, err + } + err = fromAddr.UnmarshalBinary(fromBytes) + if err != nil { + return nil, err + } + toLen, err = rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + toBytes := make([]byte, toLen) + _, err = io.ReadFull(reader, toBytes) + if err != nil { + return nil, err + } + err = toAddr.UnmarshalBinary(toBytes) + if err != nil { + return nil, err + } + mySet.rr[i] = myIPRange{fromAddr, toAddr} + } + return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil +} + +func writeIPSet(writer io.Writer, set *netipx.IPSet) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + mySet := (*myIPSet)(unsafe.Pointer(set)) + err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr))) + if err != nil { + return err + } + for _, rr := range mySet.rr { + var ( + fromBinary []byte + toBinary []byte + ) + fromBinary, err = rr.from.MarshalBinary() + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(fromBinary))) + if err != nil { + return err + } + _, err = writer.Write(fromBinary) + if err != nil { + return err + } + toBinary, err = rr.to.MarshalBinary() + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(toBinary))) + if err != nil { + return err + } + _, err = writer.Write(toBinary) + if err != nil { + return err + } + } + return nil +} diff --git a/constant/rule.go b/constant/rule.go index 3c741995f8..5a8eaf127f 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -9,3 +9,11 @@ const ( LogicalTypeAnd = "and" LogicalTypeOr = "or" ) + +const ( + RuleSetTypeLocal = "local" + RuleSetTypeRemote = "remote" + RuleSetVersion1 = 1 + RuleSetFormatSource = "source" + RuleSetFormatBinary = "binary" +) diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index 2616ded5c1..001390d391 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -22,11 +22,13 @@ var ( bucketSelected = []byte("selected") bucketExpand = []byte("group_expand") bucketMode = []byte("clash_mode") + bucketRuleSet = []byte("rule_set") bucketNameList = []string{ string(bucketSelected), string(bucketExpand), string(bucketMode), + string(bucketRuleSet), } cacheIDDefault = []byte("default") @@ -257,3 +259,36 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { } }) } + +func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedRuleSet { + var savedSet adapter.SavedRuleSet + err := c.DB.View(func(t *bbolt.Tx) error { + bucket := c.bucket(t, bucketRuleSet) + if bucket == nil { + return os.ErrNotExist + } + setBinary := bucket.Get([]byte(tag)) + if len(setBinary) == 0 { + return os.ErrInvalid + } + return savedSet.UnmarshalBinary(setBinary) + }) + if err != nil { + return nil + } + return &savedSet +} + +func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedRuleSet) error { + return c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := c.createBucket(t, bucketRuleSet) + if err != nil { + return err + } + setBinary, err := set.MarshalBinary() + if err != nil { + return err + } + return bucket.Put([]byte(tag), setBinary) + }) +} diff --git a/experimental/cachefile/fakeip.go b/experimental/cachefile/fakeip.go index 2242342a36..e998ebb859 100644 --- a/experimental/cachefile/fakeip.go +++ b/experimental/cachefile/fakeip.go @@ -25,7 +25,7 @@ func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata { err := c.DB.Batch(func(tx *bbolt.Tx) error { bucket := tx.Bucket(bucketFakeIP) if bucket == nil { - return nil + return os.ErrNotExist } metadataBinary := bucket.Get(keyMetadata) if len(metadataBinary) == 0 { diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go index 050efd8d17..cf96931a85 100644 --- a/experimental/clashapi/proxies.go +++ b/experimental/clashapi/proxies.go @@ -100,8 +100,10 @@ func getProxies(server *Server, router adapter.Router) func(w http.ResponseWrite allProxies = append(allProxies, detour.Tag()) } - defaultTag := router.DefaultOutbound(N.NetworkTCP).Tag() - if defaultTag == "" { + var defaultTag string + if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + defaultTag = defaultOutbound.Tag() + } else { defaultTag = allProxies[0] } diff --git a/experimental/clashapi/server_resources.go b/experimental/clashapi/server_resources.go index ad36641e05..d6d22b5390 100644 --- a/experimental/clashapi/server_resources.go +++ b/experimental/clashapi/server_resources.go @@ -51,7 +51,11 @@ func (s *Server) downloadExternalUI() error { } detour = outbound } else { - detour = s.router.DefaultOutbound(N.NetworkTCP) + outbound, err := s.router.DefaultOutbound(N.NetworkTCP) + if err != nil { + return err + } + detour = outbound } httpClient := &http.Client{ Transport: &http.Transport{ diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go index 3dc5a367e7..b7c20eb075 100644 --- a/experimental/clashapi/trafficontrol/tracker.go +++ b/experimental/clashapi/trafficontrol/tracker.go @@ -94,7 +94,9 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad var chain []string var next string if rule == nil { - next = router.DefaultOutbound(N.NetworkTCP).Tag() + if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + next = defaultOutbound.Tag() + } } else { next = rule.Outbound() } @@ -181,7 +183,9 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route var chain []string var next string if rule == nil { - next = router.DefaultOutbound(N.NetworkUDP).Tag() + if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil { + next = defaultOutbound.Tag() + } } else { next = rule.Outbound() } diff --git a/option/experimental.go b/option/experimental.go index 72751a590e..c685f51f54 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -23,16 +23,16 @@ type ClashAPIOptions struct { DefaultMode string `json:"default_mode,omitempty"` ModeList []string `json:"-"` + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` // Deprecated: migrated to global cache file StoreMode bool `json:"store_mode,omitempty"` // Deprecated: migrated to global cache file StoreSelected bool `json:"store_selected,omitempty"` // Deprecated: migrated to global cache file StoreFakeIP bool `json:"store_fakeip,omitempty"` - // Deprecated: migrated to global cache file - CacheFile string `json:"cache_file,omitempty"` - // Deprecated: migrated to global cache file - CacheID string `json:"cache_id,omitempty"` } type V2RayAPIOptions struct { diff --git a/option/route.go b/option/route.go index 43150576e2..e313fcf242 100644 --- a/option/route.go +++ b/option/route.go @@ -4,6 +4,7 @@ type RouteOptions struct { GeoIP *GeoIPOptions `json:"geoip,omitempty"` Geosite *GeositeOptions `json:"geosite,omitempty"` Rules []Rule `json:"rules,omitempty"` + RuleSet []RuleSet `json:"rule_set,omitempty"` Final string `json:"final,omitempty"` FindProcess bool `json:"find_process,omitempty"` AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` diff --git a/option/rule.go b/option/rule.go index 4f4042025f..bad605a0fc 100644 --- a/option/rule.go +++ b/option/rule.go @@ -65,34 +65,36 @@ func (r Rule) IsValid() bool { } type DefaultRule struct { - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - GeoIP Listable[string] `json:"geoip,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - IPCIDR Listable[string] `json:"ip_cidr,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + Network Listable[string] `json:"network,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + Protocol Listable[string] `json:"protocol,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + Geosite Listable[string] `json:"geosite,omitempty"` + SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` + GeoIP Listable[string] `json:"geoip,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + User Listable[string] `json:"user,omitempty"` + UserID Listable[int32] `json:"user_id,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` } func (r DefaultRule) IsValid() bool { diff --git a/option/rule_dns.go b/option/rule_dns.go index fca3432293..c02d09f761 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -91,6 +91,7 @@ type DefaultDNSRule struct { ClashMode string `json:"clash_mode,omitempty"` WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` Invert bool `json:"invert,omitempty"` Server string `json:"server,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` diff --git a/option/rule_set.go b/option/rule_set.go new file mode 100644 index 0000000000..7a6f7e9282 --- /dev/null +++ b/option/rule_set.go @@ -0,0 +1,230 @@ +package option + +import ( + "reflect" + + "github.com/sagernet/sing-box/common/json" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + + "go4.org/netipx" +) + +type _RuleSet struct { + Type string `json:"type"` + Tag string `json:"tag"` + Format string `json:"format"` + LocalOptions LocalRuleSet `json:"-"` + RemoteOptions RemoteRuleSet `json:"-"` +} + +type RuleSet _RuleSet + +func (r RuleSet) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleSetTypeLocal: + v = r.LocalOptions + case C.RuleSetTypeRemote: + v = r.RemoteOptions + default: + return nil, E.New("unknown rule set type: " + r.Type) + } + return MarshallObjects((_RuleSet)(r), v) +} + +func (r *RuleSet) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_RuleSet)(r)) + if err != nil { + return err + } + if r.Tag == "" { + return E.New("missing rule_set.[].tag") + } + switch r.Format { + case "": + return E.New("missing rule_set.[].format") + case C.RuleSetFormatSource, C.RuleSetFormatBinary: + default: + return E.New("unknown rule set format: " + r.Format) + } + var v any + switch r.Type { + case C.RuleSetTypeLocal: + v = &r.LocalOptions + case C.RuleSetTypeRemote: + v = &r.RemoteOptions + case "": + return E.New("missing rule_set.[].type") + default: + return E.New("unknown rule set type: " + r.Type) + } + err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v) + if err != nil { + return E.Cause(err, "rule set") + } + return nil +} + +type LocalRuleSet struct { + Path string `json:"path,omitempty"` +} + +type RemoteRuleSet struct { + URL string `json:"url"` + DownloadDetour string `json:"download_detour,omitempty"` + UpdateInterval Duration `json:"update_interval,omitempty"` +} + +type _HeadlessRule struct { + Type string `json:"type,omitempty"` + DefaultOptions DefaultHeadlessRule `json:"-"` + LogicalOptions LogicalHeadlessRule `json:"-"` +} + +type HeadlessRule _HeadlessRule + +func (r HeadlessRule) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleTypeDefault: + r.Type = "" + v = r.DefaultOptions + case C.RuleTypeLogical: + v = r.LogicalOptions + default: + return nil, E.New("unknown rule type: " + r.Type) + } + return MarshallObjects((_HeadlessRule)(r), v) +} + +func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_HeadlessRule)(r)) + if err != nil { + return err + } + var v any + switch r.Type { + case "", C.RuleTypeDefault: + r.Type = C.RuleTypeDefault + v = &r.DefaultOptions + case C.RuleTypeLogical: + v = &r.LogicalOptions + default: + return E.New("unknown rule type: " + r.Type) + } + err = UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v) + if err != nil { + return E.Cause(err, "route rule-set rule") + } + return nil +} + +func (r HeadlessRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault, "": + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + +type DefaultHeadlessRule struct { + QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` + Network Listable[string] `json:"network,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + Invert bool `json:"invert,omitempty"` + + DomainMatcher *domain.Matcher `json:"-"` + SourceIPSet *netipx.IPSet `json:"-"` + IPSet *netipx.IPSet `json:"-"` +} + +func (r DefaultHeadlessRule) IsValid() bool { + var defaultValue DefaultHeadlessRule + defaultValue.Invert = r.Invert + return !reflect.DeepEqual(r, defaultValue) +} + +type LogicalHeadlessRule struct { + Mode string `json:"mode"` + Rules []HeadlessRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` +} + +func (r LogicalHeadlessRule) IsValid() bool { + return len(r.Rules) > 0 && common.All(r.Rules, HeadlessRule.IsValid) +} + +type _PlainRuleSetCompat struct { + Version int `json:"version"` + Options PlainRuleSet `json:"-"` +} + +type PlainRuleSetCompat _PlainRuleSetCompat + +func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { + var v any + switch r.Version { + case C.RuleSetVersion1: + v = r.Options + default: + return nil, E.New("unknown rule set version: ", r.Version) + } + return MarshallObjects((_PlainRuleSetCompat)(r), v) +} + +func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_PlainRuleSetCompat)(r)) + if err != nil { + return err + } + var v any + switch r.Version { + case C.RuleSetVersion1: + v = &r.Options + case 0: + return E.New("missing rule set version") + default: + return E.New("unknown rule set version: ", r.Version) + } + err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) + if err != nil { + return E.Cause(err, "rule set") + } + return nil +} + +func (r PlainRuleSetCompat) Upgrade() PlainRuleSet { + var result PlainRuleSet + switch r.Version { + case C.RuleSetVersion1: + result = r.Options + default: + panic("unknown rule set version: " + F.ToString(r.Version)) + } + return result +} + +type PlainRuleSet struct { + Rules []HeadlessRule `json:"rules,omitempty"` +} diff --git a/option/time_unit.go b/option/time_unit.go new file mode 100644 index 0000000000..5e531dadf1 --- /dev/null +++ b/option/time_unit.go @@ -0,0 +1,226 @@ +package option + +import ( + "errors" + "time" +) + +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +const durationDay = 24 * time.Hour + +var unitMap = map[string]uint64{ + "ns": uint64(time.Nanosecond), + "us": uint64(time.Microsecond), + "µs": uint64(time.Microsecond), // U+00B5 = micro symbol + "μs": uint64(time.Microsecond), // U+03BC = Greek letter mu + "ms": uint64(time.Millisecond), + "s": uint64(time.Second), + "m": uint64(time.Minute), + "h": uint64(time.Hour), + "d": uint64(durationDay), +} + +// ParseDuration parses a duration string. +// A duration string is a possibly signed sequence of +// decimal numbers, each with optional fraction and a unit suffix, +// such as "300ms", "-1.5h" or "2h45m". +// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". +func ParseDuration(s string) (Duration, error) { + // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ + orig := s + var d uint64 + neg := false + + // Consume [-+]? + if s != "" { + c := s[0] + if c == '-' || c == '+' { + neg = c == '-' + s = s[1:] + } + } + // Special case: if all that is left is "0", this is zero. + if s == "0" { + return 0, nil + } + if s == "" { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + for s != "" { + var ( + v, f uint64 // integers before, after decimal point + scale float64 = 1 // value = v + f/scale + ) + + var err error + + // The next character must be [0-9.] + if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + // Consume [0-9]* + pl := len(s) + v, s, err = leadingInt(s) + if err != nil { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + pre := pl != len(s) // whether we consumed anything before a period + + // Consume (\.[0-9]*)? + post := false + if s != "" && s[0] == '.' { + s = s[1:] + pl := len(s) + f, scale, s = leadingFraction(s) + post = pl != len(s) + } + if !pre && !post { + // no digits (e.g. ".s" or "-.s") + return 0, errors.New("time: invalid duration " + quote(orig)) + } + + // Consume unit. + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c == '.' || '0' <= c && c <= '9' { + break + } + } + if i == 0 { + return 0, errors.New("time: missing unit in duration " + quote(orig)) + } + u := s[:i] + s = s[i:] + unit, ok := unitMap[u] + if !ok { + return 0, errors.New("time: unknown unit " + quote(u) + " in duration " + quote(orig)) + } + if v > 1<<63/unit { + // overflow + return 0, errors.New("time: invalid duration " + quote(orig)) + } + v *= unit + if f > 0 { + // float64 is needed to be nanosecond accurate for fractions of hours. + // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit) + v += uint64(float64(f) * (float64(unit) / scale)) + if v > 1<<63 { + // overflow + return 0, errors.New("time: invalid duration " + quote(orig)) + } + } + d += v + if d > 1<<63 { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + } + if neg { + return -Duration(d), nil + } + if d > 1<<63-1 { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + return Duration(d), nil +} + +var errLeadingInt = errors.New("time: bad [0-9]*") // never printed + +// leadingInt consumes the leading [0-9]* from s. +func leadingInt[bytes []byte | string](s bytes) (x uint64, rem bytes, err error) { + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if x > 1<<63/10 { + // overflow + return 0, rem, errLeadingInt + } + x = x*10 + uint64(c) - '0' + if x > 1<<63 { + // overflow + return 0, rem, errLeadingInt + } + } + return x, s[i:], nil +} + +// leadingFraction consumes the leading [0-9]* from s. +// It is used only for fractions, so does not return an error on overflow, +// it just stops accumulating precision. +func leadingFraction(s string) (x uint64, scale float64, rem string) { + i := 0 + scale = 1 + overflow := false + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if overflow { + continue + } + if x > (1<<63-1)/10 { + // It's possible for overflow to give a positive number, so take care. + overflow = true + continue + } + y := x*10 + uint64(c) - '0' + if y > 1<<63 { + overflow = true + continue + } + x = y + scale *= 10 + } + return x, scale, s[i:] +} + +// These are borrowed from unicode/utf8 and strconv and replicate behavior in +// that package, since we can't take a dependency on either. +const ( + lowerhex = "0123456789abcdef" + runeSelf = 0x80 + runeError = '\uFFFD' +) + +func quote(s string) string { + buf := make([]byte, 1, len(s)+2) // slice will be at least len(s) + quotes + buf[0] = '"' + for i, c := range s { + if c >= runeSelf || c < ' ' { + // This means you are asking us to parse a time.Duration or + // time.Location with unprintable or non-ASCII characters in it. + // We don't expect to hit this case very often. We could try to + // reproduce strconv.Quote's behavior with full fidelity but + // given how rarely we expect to hit these edge cases, speed and + // conciseness are better. + var width int + if c == runeError { + width = 1 + if i+2 < len(s) && s[i:i+3] == string(runeError) { + width = 3 + } + } else { + width = len(string(c)) + } + for j := 0; j < width; j++ { + buf = append(buf, `\x`...) + buf = append(buf, lowerhex[s[i+j]>>4]) + buf = append(buf, lowerhex[s[i+j]&0xF]) + } + } else { + if c == '"' || c == '\\' { + buf = append(buf, '\\') + } + buf = append(buf, string(c)...) + } + } + buf = append(buf, '"') + return string(buf) +} diff --git a/option/types.go b/option/types.go index f2fed66309..2f029098af 100644 --- a/option/types.go +++ b/option/types.go @@ -164,7 +164,7 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error { if err != nil { return err } - duration, err := time.ParseDuration(value) + duration, err := ParseDuration(value) if err != nil { return err } @@ -174,6 +174,14 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error { type DNSQueryType uint16 +func (t DNSQueryType) String() string { + typeName, loaded := mDNS.TypeToString[uint16(t)] + if loaded { + return typeName + } + return F.ToString(uint16(t)) +} + func (t DNSQueryType) MarshalJSON() ([]byte, error) { typeName, loaded := mDNS.TypeToString[uint16(t)] if loaded { diff --git a/route/router.go b/route/router.go index 09a3e8e6e9..9783b07875 100644 --- a/route/router.go +++ b/route/router.go @@ -39,6 +39,7 @@ import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" serviceNTP "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" @@ -67,6 +68,8 @@ type Router struct { dnsClient *dns.Client defaultDomainStrategy dns.DomainStrategy dnsRules []adapter.DNSRule + ruleSets []adapter.RuleSet + ruleSetMap map[string]adapter.RuleSet defaultTransport dns.Transport transports []dns.Transport transportMap map[string]dns.Transport @@ -106,6 +109,7 @@ func NewRouter( outboundByTag: make(map[string]adapter.Outbound), rules: make([]adapter.Rule, 0, len(options.Rules)), dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)), + ruleSetMap: make(map[string]adapter.RuleSet), needGeoIPDatabase: hasRule(options.Rules, isGeoIPRule) || hasDNSRule(dnsOptions.Rules, isGeoIPDNSRule), needGeositeDatabase: hasRule(options.Rules, isGeositeRule) || hasDNSRule(dnsOptions.Rules, isGeositeDNSRule), geoIPOptions: common.PtrValueOrDefault(options.GeoIP), @@ -140,6 +144,14 @@ func NewRouter( } router.dnsRules = append(router.dnsRules, dnsRule) } + for i, ruleSetOptions := range options.RuleSet { + ruleSet, err := NewRuleSet(ctx, router, router.logger, ruleSetOptions) + if err != nil { + return nil, E.Cause(err, "parse rule-set[", i, "]") + } + router.ruleSets = append(router.ruleSets, ruleSet) + router.ruleSetMap[ruleSetOptions.Tag] = ruleSet + } transports := make([]dns.Transport, len(dnsOptions.Servers)) dummyTransportMap := make(map[string]dns.Transport) @@ -479,6 +491,33 @@ func (r *Router) Start() error { if r.needWIFIState { r.updateWIFIState() } + if r.fakeIPStore != nil { + err := r.fakeIPStore.Start() + if err != nil { + return err + } + } + if len(r.ruleSets) > 0 { + ruleSetStartContext := NewRuleSetStartContext() + var ruleSetStartGroup task.Group + for i, ruleSet := range r.ruleSets { + ruleSetInPlace := ruleSet + ruleSetStartGroup.Append0(func(ctx context.Context) error { + err := ruleSetInPlace.StartContext(ctx, ruleSetStartContext) + if err != nil { + return E.Cause(err, "initialize rule-set[", i, "]") + } + return nil + }) + } + ruleSetStartGroup.Concurrency(5) + ruleSetStartGroup.FastFail() + err := ruleSetStartGroup.Run(r.ctx) + if err != nil { + return err + } + ruleSetStartContext.Close() + } for i, rule := range r.rules { err := rule.Start() if err != nil { @@ -491,12 +530,6 @@ func (r *Router) Start() error { return E.Cause(err, "initialize DNS rule[", i, "]") } } - if r.fakeIPStore != nil { - err := r.fakeIPStore.Start() - if err != nil { - return err - } - } for i, transport := range r.transports { err := transport.Start() if err != nil { @@ -512,6 +545,18 @@ func (r *Router) Start() error { return nil } +func (r *Router) PostStart() error { + if len(r.ruleSets) > 0 { + for i, ruleSet := range r.ruleSets { + err := ruleSet.PostStart() + if err != nil { + return E.Cause(err, "post start rule-set[", i, "]") + } + } + } + return nil +} + func (r *Router) Close() error { var err error for i, rule := range r.rules { @@ -576,11 +621,17 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { return outbound, loaded } -func (r *Router) DefaultOutbound(network string) adapter.Outbound { +func (r *Router) DefaultOutbound(network string) (adapter.Outbound, error) { if network == N.NetworkTCP { - return r.defaultOutboundForConnection + if r.defaultOutboundForConnection == nil { + return nil, E.New("missing default outbound for TCP connections") + } + return r.defaultOutboundForConnection, nil } else { - return r.defaultOutboundForPacketConnection + if r.defaultOutboundForPacketConnection == nil { + return nil, E.New("missing default outbound for UDP connections") + } + return r.defaultOutboundForPacketConnection, nil } } @@ -588,6 +639,11 @@ func (r *Router) FakeIPStore() adapter.FakeIPStore { return r.fakeIPStore } +func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) { + ruleSet, loaded := r.ruleSetMap[tag] + return ruleSet, loaded +} + func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { @@ -876,6 +932,7 @@ func (r *Router) match0(ctx context.Context, metadata *adapter.InboundContext, d } } for i, rule := range r.rules { + metadata.ResetRuleCache() if rule.Match(metadata) { detour := rule.Outbound() r.logger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour) diff --git a/route/router_dns.go b/route/router_dns.go index 1532df94f8..b52fa9cc87 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -43,6 +43,7 @@ func (r *Router) matchDNS(ctx context.Context) (context.Context, dns.Transport, panic("no context") } for i, rule := range r.dnsRules { + metadata.ResetRuleCache() if rule.Match(metadata) { detour := rule.Outbound() transport, loaded := r.transportMap[detour] diff --git a/route/rule_abstract.go b/route/rule_abstract.go index 38d4d57d41..7a4a759e63 100644 --- a/route/rule_abstract.go +++ b/route/rule_abstract.go @@ -1,6 +1,7 @@ package route import ( + "io" "strings" "github.com/sagernet/sing-box/adapter" @@ -16,6 +17,7 @@ type abstractDefaultRule struct { destinationAddressItems []RuleItem destinationPortItems []RuleItem allItems []RuleItem + ruleSetItem RuleItem invert bool outbound string } @@ -61,64 +63,64 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { return true } - for _, item := range r.items { - if !item.Match(metadata) { - return r.invert - } - } - - if len(r.sourceAddressItems) > 0 { - var sourceAddressMatch bool + if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch { for _, item := range r.sourceAddressItems { if item.Match(metadata) { - sourceAddressMatch = true + metadata.SourceAddressMatch = true break } } - if !sourceAddressMatch { - return r.invert - } } - if len(r.sourcePortItems) > 0 { - var sourcePortMatch bool + if len(r.sourcePortItems) > 0 && !metadata.SourceAddressMatch { for _, item := range r.sourcePortItems { if item.Match(metadata) { - sourcePortMatch = true + metadata.SourcePortMatch = true break } } - if !sourcePortMatch { - return r.invert - } } - if len(r.destinationAddressItems) > 0 { - var destinationAddressMatch bool + if len(r.destinationAddressItems) > 0 && !metadata.SourceAddressMatch { for _, item := range r.destinationAddressItems { if item.Match(metadata) { - destinationAddressMatch = true + metadata.DestinationAddressMatch = true break } } - if !destinationAddressMatch { - return r.invert - } } - if len(r.destinationPortItems) > 0 { - var destinationPortMatch bool + if len(r.destinationPortItems) > 0 && !metadata.SourceAddressMatch { for _, item := range r.destinationPortItems { if item.Match(metadata) { - destinationPortMatch = true + metadata.DestinationPortMatch = true break } } - if !destinationPortMatch { + } + + for _, item := range r.items { + if !item.Match(metadata) { return r.invert } } + if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch { + return r.invert + } + + if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch { + return r.invert + } + + if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch { + return r.invert + } + + if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch { + return r.invert + } + return !r.invert } @@ -135,7 +137,7 @@ func (r *abstractDefaultRule) String() string { } type abstractLogicalRule struct { - rules []adapter.Rule + rules []adapter.HeadlessRule mode string invert bool outbound string @@ -146,7 +148,10 @@ func (r *abstractLogicalRule) Type() string { } func (r *abstractLogicalRule) UpdateGeosite() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (adapter.Rule, bool) { + rule, loaded := it.(adapter.Rule) + return rule, loaded + }) { err := rule.UpdateGeosite() if err != nil { return err @@ -156,7 +161,10 @@ func (r *abstractLogicalRule) UpdateGeosite() error { } func (r *abstractLogicalRule) Start() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (common.Starter, bool) { + rule, loaded := it.(common.Starter) + return rule, loaded + }) { err := rule.Start() if err != nil { return err @@ -166,7 +174,10 @@ func (r *abstractLogicalRule) Start() error { } func (r *abstractLogicalRule) Close() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (io.Closer, bool) { + rule, loaded := it.(io.Closer) + return rule, loaded + }) { err := rule.Close() if err != nil { return err @@ -177,11 +188,13 @@ func (r *abstractLogicalRule) Close() error { func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { if r.mode == C.LogicalTypeAnd { - return common.All(r.rules, func(it adapter.Rule) bool { + return common.All(r.rules, func(it adapter.HeadlessRule) bool { + metadata.ResetRuleCache() return it.Match(metadata) }) != r.invert } else { - return common.Any(r.rules, func(it adapter.Rule) bool { + return common.Any(r.rules, func(it adapter.HeadlessRule) bool { + metadata.ResetRuleCache() return it.Match(metadata) }) != r.invert } diff --git a/route/rule_default.go b/route/rule_default.go index 8c8473abf5..c0ef9eef60 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -194,6 +194,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.RuleSet) > 0 { + item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } @@ -206,7 +211,7 @@ type LogicalRule struct { func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { r := &LogicalRule{ abstractLogicalRule{ - rules: make([]adapter.Rule, len(options.Rules)), + rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, outbound: options.Outbound, }, diff --git a/route/rule_dns.go b/route/rule_dns.go index b444932582..f5f9fd3583 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -190,6 +190,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.RuleSet) > 0 { + item := NewRuleSetItem(router, options.RuleSet, false) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } @@ -212,7 +217,7 @@ type LogicalDNSRule struct { func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { r := &LogicalDNSRule{ abstractLogicalRule: abstractLogicalRule{ - rules: make([]adapter.Rule, len(options.Rules)), + rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, outbound: options.Server, }, diff --git a/route/rule_headless.go b/route/rule_headless.go new file mode 100644 index 0000000000..9df2ee3036 --- /dev/null +++ b/route/rule_headless.go @@ -0,0 +1,173 @@ +package route + +import ( + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func NewHeadlessRule(router adapter.Router, options option.HeadlessRule) (adapter.HeadlessRule, error) { + switch options.Type { + case "", C.RuleTypeDefault: + if !options.DefaultOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewDefaultHeadlessRule(router, options.DefaultOptions) + case C.RuleTypeLogical: + if !options.LogicalOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewLogicalHeadlessRule(router, options.LogicalOptions) + default: + return nil, E.New("unknown rule type: ", options.Type) + } +} + +var _ adapter.HeadlessRule = (*DefaultHeadlessRule)(nil) + +type DefaultHeadlessRule struct { + abstractDefaultRule +} + +func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) { + rule := &DefaultHeadlessRule{ + abstractDefaultRule{ + invert: options.Invert, + }, + } + if len(options.Network) > 0 { + item := NewNetworkItem(options.Network) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 { + item := NewDomainItem(options.Domain, options.DomainSuffix) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.DomainMatcher != nil { + item := NewRawDomainItem(options.DomainMatcher) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainKeyword) > 0 { + item := NewDomainKeywordItem(options.DomainKeyword) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainRegex) > 0 { + item, err := NewDomainRegexItem(options.DomainRegex) + if err != nil { + return nil, E.Cause(err, "domain_regex") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourceIPCIDR) > 0 { + item, err := NewIPCIDRItem(true, options.SourceIPCIDR) + if err != nil { + return nil, E.Cause(err, "source_ipcidr") + } + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.SourceIPSet != nil { + item := NewRawIPCIDRItem(true, options.SourceIPSet) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.IPCIDR) > 0 { + item, err := NewIPCIDRItem(false, options.IPCIDR) + if err != nil { + return nil, E.Cause(err, "ipcidr") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.IPSet != nil { + item := NewRawIPCIDRItem(false, options.IPSet) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePort) > 0 { + item := NewPortItem(true, options.SourcePort) + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePortRange) > 0 { + item, err := NewPortRangeItem(true, options.SourcePortRange) + if err != nil { + return nil, E.Cause(err, "source_port_range") + } + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Port) > 0 { + item := NewPortItem(false, options.Port) + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PortRange) > 0 { + item, err := NewPortRangeItem(false, options.PortRange) + if err != nil { + return nil, E.Cause(err, "port_range") + } + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessName) > 0 { + item := NewProcessItem(options.ProcessName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessPath) > 0 { + item := NewProcessPathItem(options.ProcessPath) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PackageName) > 0 { + item := NewPackageNameItem(options.PackageName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(router, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFIBSSID) > 0 { + item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + return rule, nil +} + +var _ adapter.HeadlessRule = (*LogicalHeadlessRule)(nil) + +type LogicalHeadlessRule struct { + abstractLogicalRule +} + +func NewLogicalHeadlessRule(router adapter.Router, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) { + r := &LogicalHeadlessRule{ + abstractLogicalRule{ + rules: make([]adapter.HeadlessRule, len(options.Rules)), + invert: options.Invert, + }, + } + switch options.Mode { + case C.LogicalTypeAnd: + r.mode = C.LogicalTypeAnd + case C.LogicalTypeOr: + r.mode = C.LogicalTypeOr + default: + return nil, E.New("unknown logical mode: ", options.Mode) + } + for i, subRule := range options.Rules { + rule, err := NewHeadlessRule(router, subRule) + if err != nil { + return nil, E.Cause(err, "sub rule[", i, "]") + } + r.rules[i] = rule + } + return r, nil +} diff --git a/route/rule_item_cidr.go b/route/rule_item_cidr.go index b72d1e10b1..e17d87def8 100644 --- a/route/rule_item_cidr.go +++ b/route/rule_item_cidr.go @@ -31,7 +31,7 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { builder.Add(addr) continue } - return nil, E.Cause(err, "parse ip_cidr [", i, "]") + return nil, E.Cause(err, "parse [", i, "]") } var description string if isSource { @@ -57,8 +57,23 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { }, nil } +func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem { + var description string + if isSource { + description = "source_ipcidr=" + } else { + description = "ipcidr=" + } + description += "" + return &IPCIDRItem{ + ipSet: ipSet, + isSource: isSource, + description: description, + } +} + func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool { - if r.isSource { + if r.isSource || metadata.QueryType != 0 || metadata.IPCIDRMatchSource { return r.ipSet.Contains(metadata.Source.Addr) } else { if metadata.Destination.IsIP() { diff --git a/route/rule_item_domain.go b/route/rule_item_domain.go index 6602441deb..d2a11181b0 100644 --- a/route/rule_item_domain.go +++ b/route/rule_item_domain.go @@ -43,6 +43,13 @@ func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem { } } +func NewRawDomainItem(matcher *domain.Matcher) *DomainItem { + return &DomainItem{ + matcher, + "domain/domain_suffix=", + } +} + func (r *DomainItem) Match(metadata *adapter.InboundContext) bool { var domainHost string if metadata.Domain != "" { diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go new file mode 100644 index 0000000000..959b2f6110 --- /dev/null +++ b/route/rule_item_rule_set.go @@ -0,0 +1,55 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*RuleSetItem)(nil) + +type RuleSetItem struct { + router adapter.Router + tagList []string + setList []adapter.HeadlessRule + ipcidrMatchSource bool +} + +func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool) *RuleSetItem { + return &RuleSetItem{ + router: router, + tagList: tagList, + ipcidrMatchSource: ipCIDRMatchSource, + } +} + +func (r *RuleSetItem) Start() error { + for _, tag := range r.tagList { + ruleSet, loaded := r.router.RuleSet(tag) + if !loaded { + return E.New("rule-set not found: ", tag) + } + r.setList = append(r.setList, ruleSet) + } + return nil +} + +func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { + metadata.IPCIDRMatchSource = r.ipcidrMatchSource + for _, ruleSet := range r.setList { + if ruleSet.Match(metadata) { + return true + } + } + return false +} + +func (r *RuleSetItem) String() string { + if len(r.tagList) == 1 { + return F.ToString("rule_set=", r.tagList[0]) + } else { + return F.ToString("rule_set=[", strings.Join(r.tagList, " "), "]") + } +} diff --git a/route/rule_set.go b/route/rule_set.go new file mode 100644 index 0000000000..f644fb406f --- /dev/null +++ b/route/rule_set.go @@ -0,0 +1,67 @@ +package route + +import ( + "context" + "net" + "net/http" + "sync" + + "github.com/sagernet/sing-box/adapter" + 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" +) + +func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { + switch options.Type { + case C.RuleSetTypeLocal: + return NewLocalRuleSet(router, options) + case C.RuleSetTypeRemote: + return NewRemoteRuleSet(ctx, router, logger, options), nil + default: + return nil, E.New("unknown rule set type: ", options.Type) + } +} + +var _ adapter.RuleSetStartContext = (*RuleSetStartContext)(nil) + +type RuleSetStartContext struct { + access sync.Mutex + httpClientCache map[string]*http.Client +} + +func NewRuleSetStartContext() *RuleSetStartContext { + return &RuleSetStartContext{ + httpClientCache: make(map[string]*http.Client), + } +} + +func (c *RuleSetStartContext) 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)) + }, + }, + } + c.httpClientCache[detour] = httpClient + return httpClient +} + +func (c *RuleSetStartContext) Close() { + c.access.Lock() + defer c.access.Unlock() + for _, client := range c.httpClientCache { + client.CloseIdleConnections() + } +} diff --git a/route/rule_set_local.go b/route/rule_set_local.go new file mode 100644 index 0000000000..3c6b4236a9 --- /dev/null +++ b/route/rule_set_local.go @@ -0,0 +1,74 @@ +package route + +import ( + "context" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ adapter.RuleSet = (*LocalRuleSet)(nil) + +type LocalRuleSet struct { + rules []adapter.HeadlessRule +} + +func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { + setFile, err := os.Open(options.LocalOptions.Path) + if err != nil { + return nil, err + } + var plainRuleSet option.PlainRuleSet + switch options.Format { + case C.RuleSetFormatSource, "": + var compat option.PlainRuleSetCompat + decoder := json.NewDecoder(json.NewCommentFilter(setFile)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&compat) + if err != nil { + return nil, err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + plainRuleSet, err = srs.Read(setFile, false) + if err != nil { + return nil, err + } + default: + return nil, E.New("unknown rule set format: ", options.Format) + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(router, ruleOptions) + if err != nil { + return nil, E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + return &LocalRuleSet{rules}, nil +} + +func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} + +func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error { + return nil +} + +func (s *LocalRuleSet) PostStart() error { + return nil +} + +func (s *LocalRuleSet) Close() error { + return nil +} diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go new file mode 100644 index 0000000000..9a67f382aa --- /dev/null +++ b/route/rule_set_remote.go @@ -0,0 +1,236 @@ +package route + +import ( + "bytes" + "context" + "io" + "net" + "net/http" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/common/srs" + 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" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" +) + +var _ adapter.RuleSet = (*RemoteRuleSet)(nil) + +type RemoteRuleSet struct { + ctx context.Context + cancel context.CancelFunc + router adapter.Router + logger logger.ContextLogger + options option.RuleSet + updateInterval time.Duration + dialer N.Dialer + rules []adapter.HeadlessRule + lastUpdated time.Time + lastEtag string + updateTicker *time.Ticker + pauseManager pause.Manager +} + +func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { + ctx, cancel := context.WithCancel(ctx) + var updateInterval time.Duration + if options.RemoteOptions.UpdateInterval > 0 { + updateInterval = time.Duration(options.RemoteOptions.UpdateInterval) + } else { + updateInterval = 24 * time.Hour + } + return &RemoteRuleSet{ + ctx: ctx, + cancel: cancel, + router: router, + logger: logger, + options: options, + updateInterval: updateInterval, + pauseManager: pause.ManagerFromContext(ctx), + } +} + +func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} + +func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error { + var dialer N.Dialer + if s.options.RemoteOptions.DownloadDetour != "" { + outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour) + if !loaded { + return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour) + } + dialer = outbound + } else { + outbound, err := s.router.DefaultOutbound(N.NetworkTCP) + if err != nil { + return err + } + dialer = outbound + } + s.dialer = dialer + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + if savedSet := cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil { + err := s.loadBytes(savedSet.Content) + if err != nil { + return E.Cause(err, "restore cached rule-set") + } + s.lastUpdated = savedSet.LastUpdated + s.lastEtag = savedSet.LastEtag + } + } + s.updateTicker = time.NewTicker(s.updateInterval) + go s.loopUpdate() + return nil +} + +func (s *RemoteRuleSet) PostStart() error { + if s.lastUpdated.IsZero() { + err := s.fetchOnce(s.ctx, nil) + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } + } + return nil +} + +func (s *RemoteRuleSet) loadBytes(content []byte) error { + var ( + plainRuleSet option.PlainRuleSet + err error + ) + switch s.options.Format { + case C.RuleSetFormatSource, "": + var compat option.PlainRuleSetCompat + decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content))) + decoder.DisallowUnknownFields() + err = decoder.Decode(&compat) + if err != nil { + return err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + plainRuleSet, err = srs.Read(bytes.NewReader(content), false) + if err != nil { + return err + } + default: + return E.New("unknown rule set format: ", s.options.Format) + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(s.router, ruleOptions) + if err != nil { + return E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + s.rules = rules + return nil +} + +func (s *RemoteRuleSet) loopUpdate() { + if time.Since(s.lastUpdated) > s.updateInterval { + err := s.fetchOnce(s.ctx, nil) + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } + } + for { + select { + case <-s.ctx.Done(): + return + case <-s.updateTicker.C: + s.pauseManager.WaitActive() + err := s.fetchOnce(s.ctx, nil) + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } + } + } +} + +func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext adapter.RuleSetStartContext) error { + s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL) + var httpClient *http.Client + if startContext != nil { + httpClient = startContext.HTTPClient(s.options.RemoteOptions.DownloadDetour, s.dialer) + } else { + httpClient = &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + }, + } + } + request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil) + if err != nil { + return err + } + if s.lastEtag != "" { + request.Header.Set("If-None-Match", s.lastEtag) + } + response, err := httpClient.Do(request.WithContext(ctx)) + if err != nil { + return err + } + switch response.StatusCode { + case http.StatusOK: + case http.StatusNotModified: + s.logger.Info("update rule-set ", s.options.Tag, ": not modified") + return nil + default: + return E.New("unexpected status: ", response.Status) + } + content, err := io.ReadAll(response.Body) + if err != nil { + response.Body.Close() + return err + } + err = s.loadBytes(content) + if err != nil { + response.Body.Close() + return err + } + response.Body.Close() + eTagHeader := response.Header.Get("Etag") + if eTagHeader != "" { + s.lastEtag = eTagHeader + } + s.lastUpdated = time.Now() + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err = cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedRuleSet{ + LastUpdated: s.lastUpdated, + Content: content, + LastEtag: s.lastEtag, + }) + if err != nil { + s.logger.Error("save rule-set cache: ", err) + } + } + s.logger.Info("updated rule-set ", s.options.Tag) + return nil +} + +func (s *RemoteRuleSet) Close() error { + s.updateTicker.Stop() + s.cancel() + return nil +} From aa72c6ee6fe95eccb24b99321f3d9070a6fd50e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 1 Dec 2023 13:24:39 +0800 Subject: [PATCH 043/103] Independent `source_ip_is_private` and `ip_is_private` rules --- option/rule.go | 2 + option/rule_dns.go | 63 ++++++++++++++++---------------- route/rule_default.go | 10 +++++ route/rule_dns.go | 5 +++ route/rule_item_ip_is_private.go | 44 ++++++++++++++++++++++ 5 files changed, 93 insertions(+), 31 deletions(-) create mode 100644 route/rule_item_ip_is_private.go diff --git a/option/rule.go b/option/rule.go index bad605a0fc..1201d123ed 100644 --- a/option/rule.go +++ b/option/rule.go @@ -78,7 +78,9 @@ type DefaultRule struct { SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` GeoIP Listable[string] `json:"geoip,omitempty"` SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` SourcePort Listable[uint16] `json:"source_port,omitempty"` SourcePortRange Listable[string] `json:"source_port_range,omitempty"` Port Listable[uint16] `json:"port,omitempty"` diff --git a/option/rule_dns.go b/option/rule_dns.go index c02d09f761..50d9e61266 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -65,37 +65,38 @@ func (r DNSRule) IsValid() bool { } type DefaultDNSRule struct { - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - Outbound Listable[string] `json:"outbound,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - RuleSet Listable[string] `json:"rule_set,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` + Network Listable[string] `json:"network,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + Protocol Listable[string] `json:"protocol,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + Geosite Listable[string] `json:"geosite,omitempty"` + SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + User Listable[string] `json:"user,omitempty"` + UserID Listable[int32] `json:"user_id,omitempty"` + Outbound Listable[string] `json:"outbound,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` + Invert bool `json:"invert,omitempty"` + Server string `json:"server,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` } func (r DefaultDNSRule) IsValid() bool { diff --git a/route/rule_default.go b/route/rule_default.go index c0ef9eef60..1a190ce04b 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -120,6 +120,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } + if options.SourceIPIsPrivate { + item := NewIPIsPrivateItem(true) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } if len(options.IPCIDR) > 0 { item, err := NewIPCIDRItem(false, options.IPCIDR) if err != nil { @@ -128,6 +133,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } + if options.IPIsPrivate { + item := NewIPIsPrivateItem(false) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } if len(options.SourcePort) > 0 { item := NewPortItem(true, options.SourcePort) rule.sourcePortItems = append(rule.sourcePortItems, item) diff --git a/route/rule_dns.go b/route/rule_dns.go index f5f9fd3583..1f55d50edb 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -119,6 +119,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } + if options.SourceIPIsPrivate { + item := NewIPIsPrivateItem(true) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } if len(options.SourcePort) > 0 { item := NewPortItem(true, options.SourcePort) rule.sourcePortItems = append(rule.sourcePortItems, item) diff --git a/route/rule_item_ip_is_private.go b/route/rule_item_ip_is_private.go new file mode 100644 index 0000000000..4d511fdfb1 --- /dev/null +++ b/route/rule_item_ip_is_private.go @@ -0,0 +1,44 @@ +package route + +import ( + "net/netip" + + "github.com/sagernet/sing-box/adapter" + N "github.com/sagernet/sing/common/network" +) + +var _ RuleItem = (*IPIsPrivateItem)(nil) + +type IPIsPrivateItem struct { + isSource bool +} + +func NewIPIsPrivateItem(isSource bool) *IPIsPrivateItem { + return &IPIsPrivateItem{isSource} +} + +func (r *IPIsPrivateItem) Match(metadata *adapter.InboundContext) bool { + var destination netip.Addr + if r.isSource { + destination = metadata.Source.Addr + } else { + destination = metadata.Destination.Addr + } + if destination.IsValid() && !N.IsPublicAddr(destination) { + return true + } + for _, destinationAddress := range metadata.DestinationAddresses { + if !N.IsPublicAddr(destinationAddress) { + return true + } + } + return false +} + +func (r *IPIsPrivateItem) String() string { + if r.isSource { + return "source_ip_is_private=true" + } else { + return "ip_is_private=true" + } +} From 8d58e881051f2470146e67592d660a1207f0048a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 1 Dec 2023 13:24:18 +0800 Subject: [PATCH 044/103] Update documentation --- docs/configuration/dns/rule.md | 40 +++- docs/configuration/dns/rule.zh.md | 40 +++- docs/configuration/experimental/cache-file.md | 34 +++ docs/configuration/experimental/clash-api.md | 121 ++++++++++ docs/configuration/experimental/index.md | 145 ++---------- docs/configuration/experimental/index.zh.md | 137 ------------ docs/configuration/experimental/v2ray-api.md | 50 +++++ docs/configuration/route/geoip.md | 8 + docs/configuration/route/geoip.zh.md | 33 --- docs/configuration/route/geosite.md | 8 + docs/configuration/route/geosite.zh.md | 33 --- docs/configuration/route/index.md | 30 ++- docs/configuration/route/index.zh.md | 32 ++- docs/configuration/route/rule.md | 62 +++++- docs/configuration/route/rule.zh.md | 60 ++++- docs/configuration/rule-set/headless-rule.md | 207 ++++++++++++++++++ docs/configuration/rule-set/index.md | 97 ++++++++ docs/configuration/rule-set/source-format.md | 34 +++ docs/manual/proxy/client.md | 184 ++++++++++++++++ docs/migration.md | 195 +++++++++++++++++ mkdocs.yml | 28 ++- 21 files changed, 1220 insertions(+), 358 deletions(-) create mode 100644 docs/configuration/experimental/cache-file.md create mode 100644 docs/configuration/experimental/clash-api.md delete mode 100644 docs/configuration/experimental/index.zh.md create mode 100644 docs/configuration/experimental/v2ray-api.md delete mode 100644 docs/configuration/route/geoip.zh.md delete mode 100644 docs/configuration/route/geosite.zh.md create mode 100644 docs/configuration/rule-set/headless-rule.md create mode 100644 docs/configuration/rule-set/index.md create mode 100644 docs/configuration/rule-set/source-format.md create mode 100644 docs/migration.md diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 18e352b407..513da60e03 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -1,3 +1,14 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -46,6 +57,7 @@ "10.0.0.0/24", "192.168.0.1" ], + "source_ip_is_private": false, "source_port": [ 12345 ], @@ -85,6 +97,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": [ "direct" @@ -166,15 +182,29 @@ Match domain using regular expression. #### geosite +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). + Match geosite. #### source_geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + Match source geoip. #### source_ip_cidr -Match source ip cidr. +Match source IP CIDR. + +#### source_ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public source IP. #### source_port @@ -250,6 +280,12 @@ Match WiFi SSID. Match WiFi BSSID. +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [Rule Set](/configuration/route/#rule_set). + #### invert Invert match result. @@ -286,4 +322,4 @@ Rewrite TTL in DNS responses. #### rules -Included default rules. \ No newline at end of file +Included rules. \ No newline at end of file diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 98bfa8ab9a..f6c1f0ffc2 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -1,3 +1,14 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -45,6 +56,7 @@ "source_ip_cidr": [ "10.0.0.0/24" ], + "source_ip_is_private": false, "source_port": [ 12345 ], @@ -84,6 +96,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": [ "direct" @@ -163,16 +179,30 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### geosite -匹配 GeoSite。 +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-sets)。 + +匹配 Geosite。 #### source_geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-sets)。 + 匹配源 GeoIP。 #### source_ip_cidr 匹配源 IP CIDR。 +#### source_ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开源 IP。 + #### source_port 匹配源端口。 @@ -245,6 +275,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配 WiFi BSSID。 +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + #### invert 反选匹配结果。 @@ -281,4 +317,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### rules -包括的默认规则。 \ No newline at end of file +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md new file mode 100644 index 0000000000..66e30ef9b0 --- /dev/null +++ b/docs/configuration/experimental/cache-file.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "enabled": true, + "path": "", + "cache_id": "", + "store_fakeip": false +} +``` + +### Fields + +#### enabled + +Enable cache file. + +#### path + +Path to the cache file. + +`cache.db` will be used if empty. + +#### cache_id + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. diff --git a/docs/configuration/experimental/clash-api.md b/docs/configuration/experimental/clash-api.md new file mode 100644 index 0000000000..a06fe15480 --- /dev/null +++ b/docs/configuration/experimental/clash-api.md @@ -0,0 +1,121 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-delete-alert: [store_mode](#store_mode) + :material-delete-alert: [store_selected](#store_selected) + :material-delete-alert: [store_fakeip](#store_fakeip) + :material-delete-alert: [cache_file](#cache_file) + :material-delete-alert: [cache_id](#cache_id) + + +!!! quote "" + + Clash API is not included by default, see [Installation](./#installation). + +### Structure + +```json +{ + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" +} +``` + +### Fields + +#### external_controller + +RESTful web API listening address. Clash API will be disabled if empty. + +#### external_ui + +A relative path to the configuration directory or an absolute path to a +directory in which you put some static web resource. sing-box will then +serve it at `http://{{external-controller}}/ui`. + + + +#### external_ui_download_url + +ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. + +`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. + +#### external_ui_download_detour + +The tag of the outbound to download the external UI. + +Default outbound will be used if empty. + +#### secret + +Secret for the RESTful API (optional) +Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` +ALWAYS set a secret if RESTful API is listening on 0.0.0.0 + +#### default_mode + +Default mode in clash, `Rule` will be used if empty. + +This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. + +#### store_mode + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_mode` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +Store Clash mode in cache file. + +#### store_selected + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +!!! note "" + + The tag must be set for target outbounds. + +Store selected outbound for the `Selector` outbound in cache file. + +#### store_fakeip + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and migrated to `cache_file.store_fakeip`. + +Store fakeip in cache file. + +#### cache_file + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_file` is deprecated in Clash API and migrated to `cache_file.enabled` and `cache_file.path`. + +Cache file path, `cache.db` will be used if empty. + +#### cache_id + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_id` is deprecated in Clash API and migrated to `cache_file.cache_id`. + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. \ No newline at end of file diff --git a/docs/configuration/experimental/index.md b/docs/configuration/experimental/index.md index 308e851c3b..1057e59b36 100644 --- a/docs/configuration/experimental/index.md +++ b/docs/configuration/experimental/index.md @@ -1,139 +1,30 @@ +--- +icon: material/alert-decagram +--- + # Experimental +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [cache_file](#cache_file) + :material-alert-decagram: [clash_api](#clash_api) + ### Structure ```json { "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" - }, - "v2ray_api": { - "listen": "127.0.0.1:8080", - "stats": { - "enabled": true, - "inbounds": [ - "socks-in" - ], - "outbounds": [ - "proxy", - "direct" - ], - "users": [ - "sekai" - ] - } - } + "cache_file": {}, + "clash_api": {}, + "v2ray_api": {} } } ``` -!!! note "" - - Traffic statistics and connection management can degrade performance. - -### Clash API Fields - -!!! quote "" - - Clash API is not included by default, see [Installation](./#installation). - -#### external_controller - -RESTful web API listening address. Clash API will be disabled if empty. - -#### external_ui - -A relative path to the configuration directory or an absolute path to a -directory in which you put some static web resource. sing-box will then -serve it at `http://{{external-controller}}/ui`. - -#### external_ui_download_url - -ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. - -`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. - -#### external_ui_download_detour - -The tag of the outbound to download the external UI. - -Default outbound will be used if empty. - -#### secret - -Secret for the RESTful API (optional) -Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` -ALWAYS set a secret if RESTful API is listening on 0.0.0.0 - -#### default_mode - -Default mode in clash, `Rule` will be used if empty. - -This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. - -#### store_mode - -Store Clash mode in cache file. - -#### store_selected - -!!! note "" - - The tag must be set for target outbounds. - -Store selected outbound for the `Selector` outbound in cache file. - -#### store_fakeip - -Store fakeip in cache file. - -#### cache_file - -Cache file path, `cache.db` will be used if empty. - -#### cache_id - -Cache ID. - -If not empty, `store_selected` will use a separate store keyed by it. - -### V2Ray API Fields - -!!! quote "" - - V2Ray API is not included by default, see [Installation](./#installation). - -#### listen - -gRPC API listening address. V2Ray API will be disabled if empty. - -#### stats - -Traffic statistics service settings. - -#### stats.enabled - -Enable statistics service. - -#### stats.inbounds - -Inbound list to count traffic. - -#### stats.outbounds - -Outbound list to count traffic. - -#### stats.users +### Fields -User list to count traffic. \ No newline at end of file +| Key | Format | +|--------------|----------------------------| +| `cache_file` | [Cache File](./cache-file) | +| `clash_api` | [Clash API](./clash-api) | +| `v2ray_api` | [V2Ray API](./v2ray-api) | \ No newline at end of file diff --git a/docs/configuration/experimental/index.zh.md b/docs/configuration/experimental/index.zh.md deleted file mode 100644 index 88a95852c9..0000000000 --- a/docs/configuration/experimental/index.zh.md +++ /dev/null @@ -1,137 +0,0 @@ -# 实验性 - -### 结构 - -```json -{ - "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" - }, - "v2ray_api": { - "listen": "127.0.0.1:8080", - "stats": { - "enabled": true, - "inbounds": [ - "socks-in" - ], - "outbounds": [ - "proxy", - "direct" - ], - "users": [ - "sekai" - ] - } - } - } -} -``` - -!!! note "" - - 流量统计和连接管理会降低性能。 - -### Clash API 字段 - -!!! quote "" - - 默认安装不包含 Clash API,参阅 [安装](/zh/#_2)。 - -#### external_controller - -RESTful web API 监听地址。如果为空,则禁用 Clash API。 - -#### external_ui - -到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。 - -#### external_ui_download_url - -静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。 - -默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。 - -#### external_ui_download_detour - -用于下载静态网页资源的出站的标签。 - -如果为空,将使用默认出站。 - -#### secret - -RESTful API 的密钥(可选) -通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证 -如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。 - -#### default_mode - -Clash 中的默认模式,默认使用 `Rule`。 - -此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。 - -#### store_mode - -将 Clash 模式存储在缓存文件中。 - -#### store_selected - -!!! note "" - - 必须为目标出站设置标签。 - -将 `Selector` 中出站的选定的目标出站存储在缓存文件中。 - -#### store_fakeip - -将 fakeip 存储在缓存文件中。 - -#### cache_file - -缓存文件路径,默认使用`cache.db`。 - -#### cache_id - -缓存 ID。 - -如果不为空,`store_selected` 将会使用以此为键的独立存储。 - -### V2Ray API 字段 - -!!! quote "" - - 默认安装不包含 V2Ray API,参阅 [安装](/zh/#_2)。 - -#### listen - -gRPC API 监听地址。如果为空,则禁用 V2Ray API。 - -#### stats - -流量统计服务设置。 - -#### stats.enabled - -启用统计服务。 - -#### stats.inbounds - -统计流量的入站列表。 - -#### stats.outbounds - -统计流量的出站列表。 - -#### stats.users - -统计流量的用户列表。 \ No newline at end of file diff --git a/docs/configuration/experimental/v2ray-api.md b/docs/configuration/experimental/v2ray-api.md new file mode 100644 index 0000000000..398884242e --- /dev/null +++ b/docs/configuration/experimental/v2ray-api.md @@ -0,0 +1,50 @@ +### Structure + +!!! quote "" + + V2Ray API is not included by default, see [Installation](./#installation). + +```json +{ + "listen": "127.0.0.1:8080", + "stats": { + "enabled": true, + "inbounds": [ + "socks-in" + ], + "outbounds": [ + "proxy", + "direct" + ], + "users": [ + "sekai" + ] + } +} +``` + +### Fields + +#### listen + +gRPC API listening address. V2Ray API will be disabled if empty. + +#### stats + +Traffic statistics service settings. + +#### stats.enabled + +Enable statistics service. + +#### stats.inbounds + +Inbound list to count traffic. + +#### stats.outbounds + +Outbound list to count traffic. + +#### stats.users + +User list to count traffic. \ No newline at end of file diff --git a/docs/configuration/route/geoip.md b/docs/configuration/route/geoip.md index b966a292a5..8a2ed1d4a1 100644 --- a/docs/configuration/route/geoip.md +++ b/docs/configuration/route/geoip.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + ### Structure ```json diff --git a/docs/configuration/route/geoip.zh.md b/docs/configuration/route/geoip.zh.md deleted file mode 100644 index 3ee7042728..0000000000 --- a/docs/configuration/route/geoip.zh.md +++ /dev/null @@ -1,33 +0,0 @@ -### 结构 - -```json -{ - "route": { - "geoip": { - "path": "", - "download_url": "", - "download_detour": "" - } - } -} -``` - -### 字段 - -#### path - -指定 GeoIP 资源的路径。 - -默认 `geoip.db`。 - -#### download_url - -指定 GeoIP 资源的下载链接。 - -默认为 `https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db`。 - -#### download_detour - -用于下载 GeoIP 资源的出站的标签。 - -如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/geosite.md b/docs/configuration/route/geosite.md index db700c6af1..0463057153 100644 --- a/docs/configuration/route/geosite.md +++ b/docs/configuration/route/geosite.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). + ### Structure ```json diff --git a/docs/configuration/route/geosite.zh.md b/docs/configuration/route/geosite.zh.md deleted file mode 100644 index bee81fbf71..0000000000 --- a/docs/configuration/route/geosite.zh.md +++ /dev/null @@ -1,33 +0,0 @@ -### 结构 - -```json -{ - "route": { - "geosite": { - "path": "", - "download_url": "", - "download_detour": "" - } - } -} -``` - -### 字段 - -#### path - -指定 GeoSite 资源的路径。 - -默认 `geosite.db`。 - -#### download_url - -指定 GeoSite 资源的下载链接。 - -默认为 `https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db`。 - -#### download_detour - -用于下载 GeoSite 资源的出站的标签。 - -如果为空,将使用默认出站。 \ No newline at end of file diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 846d497330..7c1787eaea 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -1,5 +1,15 @@ +--- +icon: material/alert-decagram +--- + # Route +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -8,6 +18,7 @@ "geoip": {}, "geosite": {}, "rules": [], + "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, @@ -19,11 +30,20 @@ ### Fields -| Key | Format | -|------------|------------------------------------| -| `geoip` | [GeoIP](./geoip) | -| `geosite` | [Geosite](./geosite) | -| `rules` | List of [Route Rule](./rule) | +| Key | Format | +|-----------|----------------------| +| `geoip` | [GeoIP](./geoip) | +| `geosite` | [Geosite](./geosite) | + +#### rules + +List of [Route Rule](./rule) + +#### rule_set + +!!! question "Since sing-box 1.8.0" + +List of [Rule Set](/configuration/rule-set) #### final diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index 8bef5bea9a..b5302727a3 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -1,5 +1,15 @@ +--- +icon: material/alert-decagram +--- + # 路由 +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -7,8 +17,8 @@ "route": { "geoip": {}, "geosite": {}, - "ip_rules": [], "rules": [], + "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, @@ -20,11 +30,21 @@ ### 字段 -| 键 | 格式 | -|------------|-------------------------| -| `geoip` | [GeoIP](./geoip) | -| `geosite` | [GeoSite](./geosite) | -| `rules` | 一组 [路由规则](./rule) | +| 键 | 格式 | +|------------|-----------------------------------| +| `geoip` | [GeoIP](./geoip) | +| `geosite` | [Geosite](./geosite) | + + +#### rule + +一组 [路由规则](./rule)。 + +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +一组 [规则集](/configuration/rule-set)。 #### final diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index abbfde6fdc..aefc607c24 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -1,3 +1,17 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-plus: [ip_is_private](#ip_is_private) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -46,10 +60,12 @@ "10.0.0.0/24", "192.168.0.1" ], + "source_ip_is_private": false, "ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], + "ip_is_private": false, "source_port": [ 12345 ], @@ -89,6 +105,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": "direct" }, @@ -160,23 +180,47 @@ Match domain using regular expression. #### geosite +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). + Match geosite. #### source_geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + Match source geoip. #### geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + Match geoip. #### source_ip_cidr -Match source ip cidr. +Match source IP CIDR. + +#### ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public IP. #### ip_cidr -Match ip cidr. +Match IP CIDR. + +#### source_ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public source IP. #### source_port @@ -250,6 +294,18 @@ Match WiFi SSID. Match WiFi BSSID. +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [Rule Set](/configuration/route/#rule_set). + +#### rule_set_ipcidr_match_source + +!!! question "Since sing-box 1.8.0" + +Make `ipcidr` in rule sets match the source IP. + #### invert Invert match result. @@ -276,4 +332,4 @@ Tag of the target outbound. ==Required== -Included default rules. +Included rules. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index f4ab7890a7..f735de4836 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -1,3 +1,17 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-plus: [ip_is_private](#ip_is_private) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -45,9 +59,11 @@ "source_ip_cidr": [ "10.0.0.0/24" ], + "source_ip_is_private": false, "ip_cidr": [ "10.0.0.0/24" ], + "ip_is_private": false, "source_port": [ 12345 ], @@ -87,6 +103,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": "direct" }, @@ -158,24 +178,48 @@ #### geosite -匹配 GeoSite。 +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geosite-to-rule-sets)。 + +匹配 Geosite。 #### source_geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-sets)。 + 匹配源 GeoIP。 #### geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIp 已废弃且可能在不久的将来移除,参阅 [迁移指南](/migration/#migrate-geoip-to-rule-sets)。 + 匹配 GeoIP。 #### source_ip_cidr 匹配源 IP CIDR。 +#### source_ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开源 IP。 + #### ip_cidr 匹配 IP CIDR。 +#### ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开 IP。 + #### source_port 匹配源端口。 @@ -248,6 +292,18 @@ 匹配 WiFi BSSID。 +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + +#### rule_set_ipcidr_match_source + +!!! question "自 sing-box 1.8.0 起" + +使规则集中的 `ipcidr` 规则匹配源 IP。 + #### invert 反选匹配结果。 @@ -274,4 +330,4 @@ ==必填== -包括的默认规则。 \ No newline at end of file +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md new file mode 100644 index 0000000000..6ab62eb2e3 --- /dev/null +++ b/docs/configuration/rule-set/headless-rule.md @@ -0,0 +1,207 @@ +--- +icon: material/new-box +--- + +### Structure + +!!! question "Since sing-box 1.8.0" + +```json +{ + "rules": [ + { + "query_type": [ + "A", + "HTTPS", + 32768 + ], + "network": [ + "tcp" + ], + "domain": [ + "test.com" + ], + "domain_suffix": [ + ".cn" + ], + "domain_keyword": [ + "test" + ], + "domain_regex": [ + "^stun\\..+" + ], + "source_ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "source_port": [ + 12345 + ], + "source_port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "port": [ + 80, + 443 + ], + "port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "process_name": [ + "curl" + ], + "process_path": [ + "/usr/bin/curl" + ], + "package_name": [ + "com.termux" + ], + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], + "invert": false + }, + { + "type": "logical", + "mode": "and", + "rules": [], + "invert": false + } + ] +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Default Fields + +!!! note "" + + The default rule uses the following matching logic: + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) && + (`port` || `port_range`) && + (`source_port` || `source_port_range`) && + `other fields` + +#### query_type + +DNS query type. Values can be integers or type name strings. + +#### network + +`tcp` or `udp`. + +#### domain + +Match full domain. + +#### domain_suffix + +Match domain suffix. + +#### domain_keyword + +Match domain using keyword. + +#### domain_regex + +Match domain using regular expression. + +#### source_ip_cidr + +Match source IP CIDR. + +#### ip_cidr + +!!! info "" + + `ip_cidr` is an alias for `source_ip_cidr` when the Rule Set is used in DNS rules or `rule_set_ipcidr_match_source` enabled in route rules. + +Match IP CIDR. + +#### source_port + +Match source port. + +#### source_port_range + +Match source port range. + +#### port + +Match port. + +#### port_range + +Match port range. + +#### process_name + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process name. + +#### process_path + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path. + +#### package_name + +Match android package name. + +#### wifi_ssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi SSID. + +#### wifi_bssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi BSSID. + +#### invert + +Invert match result. + +### Logical Fields + +#### type + +`logical` + +#### mode + +==Required== + +`and` or `or` + +#### rules + +==Required== + +Included rules. diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md new file mode 100644 index 0000000000..5aff55b371 --- /dev/null +++ b/docs/configuration/rule-set/index.md @@ -0,0 +1,97 @@ +--- +icon: material/new-box +--- + +# Rule Set + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "type": "", + "tag": "", + "format": "", + + ... // Typed Fields +} +``` + +#### Local Structure + +```json +{ + "type": "local", + + ... + + "path": "" +} +``` + +#### Remote Structure + +!!! info "" + + Remote rule-set will be cached if `experimental.cache_file.enabled`. + +```json +{ + "type": "remote", + + ..., + + "url": "", + "download_detour": "", + "update_interval": "" +} +``` + +### Fields + +#### type + +==Required== + +Type of Rule Set, `local` or `remote`. + +#### tag + +==Required== + +Tag of Rule Set. + +#### format + +==Required== + +Format of Rule Set, `source` or `binary`. + +### Local Fields + +#### path + +==Required== + +File path of Rule Set. + +### Remote Fields + +#### url + +==Required== + +Download URL of Rule Set. + +#### download_detour + +Tag of the outbound to download rule-set. + +Default outbound will be used if empty. + +#### update_interval + +Update interval of Rule Set. + +`1d` will be used if empty. diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md new file mode 100644 index 0000000000..116c1ee66f --- /dev/null +++ b/docs/configuration/rule-set/source-format.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +# Source Format + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "version": 1, + "rules": [] +} +``` + +### Compile + +Use `sing-box rule-set compile [--output .srs] .json` to compile source to binary rule-set. + +### Fields + +#### version + +==Required== + +Version of Rule Set, must be `1`. + +#### rules + +==Required== + +List of [Headless Rule](./headless-rule.md). diff --git a/docs/manual/proxy/client.md b/docs/manual/proxy/client.md index 60db02deb6..11bc40ceb4 100644 --- a/docs/manual/proxy/client.md +++ b/docs/manual/proxy/client.md @@ -343,6 +343,83 @@ flowchart TB } ``` +=== ":material-dns: DNS rules (1.8.0+)" + + !!! info + + DNS rules are optional if FakeIP is used. + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "address": "tls://8.8.8.8" + }, + { + "tag": "local", + "address": "223.5.5.5", + "detour": "direct" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + }, + { + "clash_mode": "Direct", + "server": "local" + }, + { + "clash_mode": "Global", + "server": "google" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": [ + "geosite-cn", + "geosite-category-companies@cn" + ] + } + ], + "server": "local" + } + ] + }, + "route": { + "rule_set": [ + { + "type": "remote", + "tag": "geosite-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-geolocation-!cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" + }, + { + "type": "remote", + "tag": "geosite-category-companies@cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-companies@cn.srs" + } + ] + } + } + ``` + === ":material-router-network: Route rules" ```json @@ -422,4 +499,111 @@ flowchart TB ] } } + ``` + +=== ":material-router-network: Route rules (1.8.0+)" + + ```json + { + "outbounds": [ + { + "type": "direct", + "tag": "direct" + }, + { + "type": "block", + "tag": "block" + } + ], + "route": { + "rules": [ + { + "type": "logical", + "mode": "or", + "rules": [ + { + "protocol": "dns" + }, + { + "port": 53 + } + ], + "outbound": "dns" + }, + { + "ip_is_private": true, + "outbound": "direct" + }, + { + "clash_mode": "Direct", + "outbound": "direct" + }, + { + "clash_mode": "Global", + "outbound": "default" + }, + { + "type": "logical", + "mode": "or", + "rules": [ + { + "port": 853 + }, + { + "network": "udp", + "port": 443 + }, + { + "protocol": "stun" + } + ], + "outbound": "block" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": [ + "geoip-cn", + "geosite-cn", + "geosite-category-companies@cn" + ] + } + ], + "outbound": "direct" + } + ], + "rule_set": [ + { + "type": "remote", + "tag": "geoip-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-geolocation-!cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" + }, + { + "type": "remote", + "tag": "geosite-category-companies@cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-companies@cn.srs" + } + ] + } + } ``` \ No newline at end of file diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000000..aec9b36007 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,195 @@ +--- +icon: material/arrange-bring-forward +--- + +# Migration + +## 1.8.0 + +!!! warning "Unstable" + + This version is still under development, and the following migration guide may be changed in the future. + +### :material-close-box: Migrate cache file from Clash API to independent options + +!!! info "Reference" + + [Clash API](/configuration/experimental/clash-api) / + [Cache File](/configuration/experimental/cache-file) + +=== ":material-card-remove: Deprecated" + + ```json + { + "experimental": { + "clash_api": { + "cache_file": "cache.db", // default value + "cahce_id": "my_profile2", + "store_mode": true, + "store_selected": true, + "store_fakeip": true + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "experimental" : { + "cache_file": { + "enabled": true, + "path": "cache.db", // default value + "cache_id": "my_profile2", + "store_fakeip": true + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate GeoIP to rule sets + +!!! info "Reference" + + [GeoIP](/configuration/route/geoip) / + [Route](/configuration/route) / + [Route Rule](/configuration/route/rule) / + [DNS Rule](/configuration/dns/rule) / + [Rule Set](/configuration/rule-set) + +!!! tip + + `sing-box geoip` commands can help you convert custom GeoIP into rule sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geoip": "private", + "outbound": "direct" + }, + { + "geoip": "cn", + "outbound": "direct" + }, + { + "source_geoip": "cn", + "outbound": "block" + } + ], + "geoip": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "ip_is_private": true, + "outbound": "direct" + }, + { + "rule_set": "geoip-cn", + "outbound": "direct" + }, + { + "rule_set": "geoip-us", + "rule_set_ipcidr_match_source": true, + "outbound": "block" + } + ], + "rule_set": [ + { + "tag": "geoip-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", + "download_detour": "proxy" + }, + { + "tag": "geoip-us", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate Geosite to rule sets + +!!! info "Reference" + + [Geosite](/configuration/route/geosite) / + [Route](/configuration/route) / + [Route Rule](/configuration/route/rule) / + [DNS Rule](/configuration/dns/rule) / + [Rule Set](/configuration/rule-set) + +!!! tip + + `sing-box geosite` commands can help you convert custom Geosite into rule sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geosite": "cn", + "outbound": "direct" + } + ], + "geosite": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geosite-cn", + "outbound": "direct" + } + ], + "rule_set": [ + { + "tag": "geosite-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 1d4b1d8b0e..c5dd7df335 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,12 +32,16 @@ theme: - content.code.copy - content.code.select - content.code.annotate + icon: + admonition: + question: material/new-box nav: - Home: - index.md + - Change Log: changelog.md + - Migration: migration.md - Deprecated: deprecated.md - Support: support.md - - Change Log: changelog.md - Installation: - Package Manager: installation/package-manager.md - Docker: installation/docker.md @@ -56,7 +60,7 @@ nav: - Proxy: - Server: manual/proxy/server.md - Client: manual/proxy/client.md -# - TUN: manual/proxy/tun.md + # - TUN: manual/proxy/tun.md - Proxy Protocol: - Shadowsocks: manual/proxy-protocol/shadowsocks.md - Trojan: manual/proxy-protocol/trojan.md @@ -79,8 +83,15 @@ nav: - Geosite: configuration/route/geosite.md - Route Rule: configuration/route/rule.md - Protocol Sniff: configuration/route/sniff.md + - Rule Set: + - configuration/rule-set/index.md + - Source Format: configuration/rule-set/source-format.md + - Headless Rule: configuration/rule-set/headless-rule.md - Experimental: - configuration/experimental/index.md + - Cache File: configuration/experimental/cache-file.md + - Clash API: configuration/experimental/clash-api.md + - V2Ray API: configuration/experimental/v2ray-api.md - Shared: - Listen Fields: configuration/shared/listen.md - Dial Fields: configuration/shared/dial.md @@ -180,9 +191,10 @@ plugins: name: 简体中文 nav_translations: Home: 开始 + Change Log: 更新日志 + Migration: 迁移指南 Deprecated: 废弃功能列表 Support: 支持 - Change Log: 更新日志 Installation: 安装 Package Manager: 包管理器 @@ -203,6 +215,10 @@ plugins: Route Rule: 路由规则 Protocol Sniff: 协议探测 + Rule Set: 规则集 + Source Format: 源文件格式 + Headless Rule: 无头规则 + Experimental: 实验性 Shared: 通用 @@ -215,10 +231,6 @@ plugins: Inbound: 入站 Outbound: 出站 - FAQ: 常见问题 - Known Issues: 已知问题 - Examples: 示例 - Linux Server Installation: Linux 服务器安装 - DNS Hijack: DNS 劫持 + Manual: 手册 reconfigure_material: true reconfigure_search: true \ No newline at end of file From b78a74e7b7c85e589ca650f625acbbeb7c8b96ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 1 Dec 2023 19:04:21 +0800 Subject: [PATCH 045/103] contextjson: Import json --- common/contextjson/decode.go | 1293 +++++++++++++++++++++++++++++++++ common/contextjson/encode.go | 1283 ++++++++++++++++++++++++++++++++ common/contextjson/fold.go | 48 ++ common/contextjson/indent.go | 174 +++++ common/contextjson/scanner.go | 610 ++++++++++++++++ common/contextjson/stream.go | 513 +++++++++++++ common/contextjson/tables.go | 218 ++++++ common/contextjson/tags.go | 38 + 8 files changed, 4177 insertions(+) create mode 100644 common/contextjson/decode.go create mode 100644 common/contextjson/encode.go create mode 100644 common/contextjson/fold.go create mode 100644 common/contextjson/indent.go create mode 100644 common/contextjson/scanner.go create mode 100644 common/contextjson/stream.go create mode 100644 common/contextjson/tables.go create mode 100644 common/contextjson/tags.go diff --git a/common/contextjson/decode.go b/common/contextjson/decode.go new file mode 100644 index 0000000000..c222bece44 --- /dev/null +++ b/common/contextjson/decode.go @@ -0,0 +1,1293 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Represents JSON data structure using native Go types: booleans, floats, +// strings, arrays, and maps. + +package json + +import ( + "encoding" + "encoding/base64" + "fmt" + "reflect" + "strconv" + "strings" + "unicode" + "unicode/utf16" + "unicode/utf8" +) + +// Unmarshal parses the JSON-encoded data and stores the result +// in the value pointed to by v. If v is nil or not a pointer, +// Unmarshal returns an InvalidUnmarshalError. +// +// Unmarshal uses the inverse of the encodings that +// Marshal uses, allocating maps, slices, and pointers as necessary, +// with the following additional rules: +// +// To unmarshal JSON into a pointer, Unmarshal first handles the case of +// the JSON being the JSON literal null. In that case, Unmarshal sets +// the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into +// the value pointed at by the pointer. If the pointer is nil, Unmarshal +// allocates a new value for it to point to. +// +// To unmarshal JSON into a value implementing the Unmarshaler interface, +// Unmarshal calls that value's UnmarshalJSON method, including +// when the input is a JSON null. +// Otherwise, if the value implements encoding.TextUnmarshaler +// and the input is a JSON quoted string, Unmarshal calls that value's +// UnmarshalText method with the unquoted form of the string. +// +// To unmarshal JSON into a struct, Unmarshal matches incoming object +// keys to the keys used by Marshal (either the struct field name or its tag), +// preferring an exact match but also accepting a case-insensitive match. By +// default, object keys which don't have a corresponding struct field are +// ignored (see Decoder.DisallowUnknownFields for an alternative). +// +// To unmarshal JSON into an interface value, +// Unmarshal stores one of these in the interface value: +// +// bool, for JSON booleans +// float64, for JSON numbers +// string, for JSON strings +// []interface{}, for JSON arrays +// map[string]interface{}, for JSON objects +// nil for JSON null +// +// To unmarshal a JSON array into a slice, Unmarshal resets the slice length +// to zero and then appends each element to the slice. +// As a special case, to unmarshal an empty JSON array into a slice, +// Unmarshal replaces the slice with a new empty slice. +// +// To unmarshal a JSON array into a Go array, Unmarshal decodes +// JSON array elements into corresponding Go array elements. +// If the Go array is smaller than the JSON array, +// the additional JSON array elements are discarded. +// If the JSON array is smaller than the Go array, +// the additional Go array elements are set to zero values. +// +// To unmarshal a JSON object into a map, Unmarshal first establishes a map to +// use. If the map is nil, Unmarshal allocates a new map. Otherwise Unmarshal +// reuses the existing map, keeping existing entries. Unmarshal then stores +// key-value pairs from the JSON object into the map. The map's key type must +// either be any string type, an integer, implement json.Unmarshaler, or +// implement encoding.TextUnmarshaler. +// +// If the JSON-encoded data contain a syntax error, Unmarshal returns a SyntaxError. +// +// If a JSON value is not appropriate for a given target type, +// or if a JSON number overflows the target type, Unmarshal +// skips that field and completes the unmarshaling as best it can. +// If no more serious errors are encountered, Unmarshal returns +// an UnmarshalTypeError describing the earliest such error. In any +// case, it's not guaranteed that all the remaining fields following +// the problematic one will be unmarshaled into the target object. +// +// The JSON null value unmarshals into an interface, map, pointer, or slice +// by setting that Go value to nil. Because null is often used in JSON to mean +// “not present,” unmarshaling a JSON null into any other Go type has no effect +// on the value and produces no error. +// +// When unmarshaling quoted strings, invalid UTF-8 or +// invalid UTF-16 surrogate pairs are not treated as an error. +// Instead, they are replaced by the Unicode replacement +// character U+FFFD. +func Unmarshal(data []byte, v any) error { + // Check for well-formedness. + // Avoids filling out half a data structure + // before discovering a JSON syntax error. + var d decodeState + err := checkValid(data, &d.scan) + if err != nil { + return err + } + + d.init(data) + return d.unmarshal(v) +} + +// Unmarshaler is the interface implemented by types +// that can unmarshal a JSON description of themselves. +// The input can be assumed to be a valid encoding of +// a JSON value. UnmarshalJSON must copy the JSON data +// if it wishes to retain the data after returning. +// +// By convention, to approximate the behavior of Unmarshal itself, +// Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op. +type Unmarshaler interface { + UnmarshalJSON([]byte) error +} + +// An UnmarshalTypeError describes a JSON value that was +// not appropriate for a value of a specific Go type. +type UnmarshalTypeError struct { + Value string // description of JSON value - "bool", "array", "number -5" + Type reflect.Type // type of Go value it could not be assigned to + Offset int64 // error occurred after reading Offset bytes + Struct string // name of the struct type containing the field + Field string // the full path from root node to the field +} + +func (e *UnmarshalTypeError) Error() string { + if e.Struct != "" || e.Field != "" { + return "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String() + } + return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() +} + +// An UnmarshalFieldError describes a JSON object key that +// led to an unexported (and therefore unwritable) struct field. +// +// Deprecated: No longer used; kept for compatibility. +type UnmarshalFieldError struct { + Key string + Type reflect.Type + Field reflect.StructField +} + +func (e *UnmarshalFieldError) Error() string { + return "json: cannot unmarshal object key " + strconv.Quote(e.Key) + " into unexported field " + e.Field.Name + " of type " + e.Type.String() +} + +// An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. +// (The argument to Unmarshal must be a non-nil pointer.) +type InvalidUnmarshalError struct { + Type reflect.Type +} + +func (e *InvalidUnmarshalError) Error() string { + if e.Type == nil { + return "json: Unmarshal(nil)" + } + + if e.Type.Kind() != reflect.Pointer { + return "json: Unmarshal(non-pointer " + e.Type.String() + ")" + } + return "json: Unmarshal(nil " + e.Type.String() + ")" +} + +func (d *decodeState) unmarshal(v any) error { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Pointer || rv.IsNil() { + return &InvalidUnmarshalError{reflect.TypeOf(v)} + } + + d.scan.reset() + d.scanWhile(scanSkipSpace) + // We decode rv not rv.Elem because the Unmarshaler interface + // test must be applied at the top level of the value. + err := d.value(rv) + if err != nil { + return d.addErrorContext(err) + } + return d.savedError +} + +// A Number represents a JSON number literal. +type Number string + +// String returns the literal text of the number. +func (n Number) String() string { return string(n) } + +// Float64 returns the number as a float64. +func (n Number) Float64() (float64, error) { + return strconv.ParseFloat(string(n), 64) +} + +// Int64 returns the number as an int64. +func (n Number) Int64() (int64, error) { + return strconv.ParseInt(string(n), 10, 64) +} + +// An errorContext provides context for type errors during decoding. +type errorContext struct { + Struct reflect.Type + FieldStack []string +} + +// decodeState represents the state while decoding a JSON value. +type decodeState struct { + data []byte + off int // next read offset in data + opcode int // last read result + scan scanner + errorContext *errorContext + savedError error + useNumber bool + disallowUnknownFields bool +} + +// readIndex returns the position of the last byte read. +func (d *decodeState) readIndex() int { + return d.off - 1 +} + +// phasePanicMsg is used as a panic message when we end up with something that +// shouldn't happen. It can indicate a bug in the JSON decoder, or that +// something is editing the data slice while the decoder executes. +const phasePanicMsg = "JSON decoder out of sync - data changing underfoot?" + +func (d *decodeState) init(data []byte) *decodeState { + d.data = data + d.off = 0 + d.savedError = nil + if d.errorContext != nil { + d.errorContext.Struct = nil + // Reuse the allocated space for the FieldStack slice. + d.errorContext.FieldStack = d.errorContext.FieldStack[:0] + } + return d +} + +// saveError saves the first err it is called with, +// for reporting at the end of the unmarshal. +func (d *decodeState) saveError(err error) { + if d.savedError == nil { + d.savedError = d.addErrorContext(err) + } +} + +// addErrorContext returns a new error enhanced with information from d.errorContext +func (d *decodeState) addErrorContext(err error) error { + if d.errorContext != nil && (d.errorContext.Struct != nil || len(d.errorContext.FieldStack) > 0) { + switch err := err.(type) { + case *UnmarshalTypeError: + err.Struct = d.errorContext.Struct.Name() + err.Field = strings.Join(d.errorContext.FieldStack, ".") + } + } + return err +} + +// skip scans to the end of what was started. +func (d *decodeState) skip() { + s, data, i := &d.scan, d.data, d.off + depth := len(s.parseState) + for { + op := s.step(s, data[i]) + i++ + if len(s.parseState) < depth { + d.off = i + d.opcode = op + return + } + } +} + +// scanNext processes the byte at d.data[d.off]. +func (d *decodeState) scanNext() { + if d.off < len(d.data) { + d.opcode = d.scan.step(&d.scan, d.data[d.off]) + d.off++ + } else { + d.opcode = d.scan.eof() + d.off = len(d.data) + 1 // mark processed EOF with len+1 + } +} + +// scanWhile processes bytes in d.data[d.off:] until it +// receives a scan code not equal to op. +func (d *decodeState) scanWhile(op int) { + s, data, i := &d.scan, d.data, d.off + for i < len(data) { + newOp := s.step(s, data[i]) + i++ + if newOp != op { + d.opcode = newOp + d.off = i + return + } + } + + d.off = len(data) + 1 // mark processed EOF with len+1 + d.opcode = d.scan.eof() +} + +// rescanLiteral is similar to scanWhile(scanContinue), but it specialises the +// common case where we're decoding a literal. The decoder scans the input +// twice, once for syntax errors and to check the length of the value, and the +// second to perform the decoding. +// +// Only in the second step do we use decodeState to tokenize literals, so we +// know there aren't any syntax errors. We can take advantage of that knowledge, +// and scan a literal's bytes much more quickly. +func (d *decodeState) rescanLiteral() { + data, i := d.data, d.off +Switch: + switch data[i-1] { + case '"': // string + for ; i < len(data); i++ { + switch data[i] { + case '\\': + i++ // escaped char + case '"': + i++ // tokenize the closing quote too + break Switch + } + } + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-': // number + for ; i < len(data); i++ { + switch data[i] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '.', 'e', 'E', '+', '-': + default: + break Switch + } + } + case 't': // true + i += len("rue") + case 'f': // false + i += len("alse") + case 'n': // null + i += len("ull") + } + if i < len(data) { + d.opcode = stateEndValue(&d.scan, data[i]) + } else { + d.opcode = scanEnd + } + d.off = i + 1 +} + +// value consumes a JSON value from d.data[d.off-1:], decoding into v, and +// reads the following byte ahead. If v is invalid, the value is discarded. +// The first byte of the value has been read already. +func (d *decodeState) value(v reflect.Value) error { + switch d.opcode { + default: + panic(phasePanicMsg) + + case scanBeginArray: + if v.IsValid() { + if err := d.array(v); err != nil { + return err + } + } else { + d.skip() + } + d.scanNext() + + case scanBeginObject: + if v.IsValid() { + if err := d.object(v); err != nil { + return err + } + } else { + d.skip() + } + d.scanNext() + + case scanBeginLiteral: + // All bytes inside literal return scanContinue op code. + start := d.readIndex() + d.rescanLiteral() + + if v.IsValid() { + if err := d.literalStore(d.data[start:d.readIndex()], v, false); err != nil { + return err + } + } + } + return nil +} + +type unquotedValue struct{} + +// valueQuoted is like value but decodes a +// quoted string literal or literal null into an interface value. +// If it finds anything other than a quoted string literal or null, +// valueQuoted returns unquotedValue{}. +func (d *decodeState) valueQuoted() any { + switch d.opcode { + default: + panic(phasePanicMsg) + + case scanBeginArray, scanBeginObject: + d.skip() + d.scanNext() + + case scanBeginLiteral: + v := d.literalInterface() + switch v.(type) { + case nil, string: + return v + } + } + return unquotedValue{} +} + +// indirect walks down v allocating pointers as needed, +// until it gets to a non-pointer. +// If it encounters an Unmarshaler, indirect stops and returns that. +// If decodingNull is true, indirect stops at the first settable pointer so it +// can be set to nil. +func indirect(v reflect.Value, decodingNull bool) (Unmarshaler, encoding.TextUnmarshaler, reflect.Value) { + // Issue #24153 indicates that it is generally not a guaranteed property + // that you may round-trip a reflect.Value by calling Value.Addr().Elem() + // and expect the value to still be settable for values derived from + // unexported embedded struct fields. + // + // The logic below effectively does this when it first addresses the value + // (to satisfy possible pointer methods) and continues to dereference + // subsequent pointers as necessary. + // + // After the first round-trip, we set v back to the original value to + // preserve the original RW flags contained in reflect.Value. + v0 := v + haveAddr := false + + // If v is a named type and is addressable, + // start with its address, so that if the type has pointer methods, + // we find them. + if v.Kind() != reflect.Pointer && v.Type().Name() != "" && v.CanAddr() { + haveAddr = true + v = v.Addr() + } + for { + // Load value from interface, but only if the result will be + // usefully addressable. + if v.Kind() == reflect.Interface && !v.IsNil() { + e := v.Elem() + if e.Kind() == reflect.Pointer && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Pointer) { + haveAddr = false + v = e + continue + } + } + + if v.Kind() != reflect.Pointer { + break + } + + if decodingNull && v.CanSet() { + break + } + + // Prevent infinite loop if v is an interface pointing to its own address: + // var v interface{} + // v = &v + if v.Elem().Kind() == reflect.Interface && v.Elem().Elem() == v { + v = v.Elem() + break + } + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Unmarshaler); ok { + return u, nil, reflect.Value{} + } + if !decodingNull { + if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { + return nil, u, reflect.Value{} + } + } + } + + if haveAddr { + v = v0 // restore original value after round-trip Value.Addr().Elem() + haveAddr = false + } else { + v = v.Elem() + } + } + return nil, nil, v +} + +// array consumes an array from d.data[d.off-1:], decoding into v. +// The first byte of the array ('[') has been read already. +func (d *decodeState) array(v reflect.Value) error { + // Check for unmarshaler. + u, ut, pv := indirect(v, false) + if u != nil { + start := d.readIndex() + d.skip() + return u.UnmarshalJSON(d.data[start:d.off]) + } + if ut != nil { + d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + } + v = pv + + // Check type of target. + switch v.Kind() { + case reflect.Interface: + if v.NumMethod() == 0 { + // Decoding into nil interface? Switch to non-reflect code. + ai := d.arrayInterface() + v.Set(reflect.ValueOf(ai)) + return nil + } + // Otherwise it's invalid. + fallthrough + default: + d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + case reflect.Array, reflect.Slice: + break + } + + i := 0 + for { + // Look ahead for ] - can only happen on first iteration. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndArray { + break + } + + // Expand slice length, growing the slice if necessary. + if v.Kind() == reflect.Slice { + if i >= v.Cap() { + v.Grow(1) + } + if i >= v.Len() { + v.SetLen(i + 1) + } + } + + if i < v.Len() { + // Decode into element. + if err := d.value(v.Index(i)); err != nil { + return err + } + } else { + // Ran out of fixed array: skip. + if err := d.value(reflect.Value{}); err != nil { + return err + } + } + i++ + + // Next token must be , or ]. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndArray { + break + } + if d.opcode != scanArrayValue { + panic(phasePanicMsg) + } + } + + if i < v.Len() { + if v.Kind() == reflect.Array { + for ; i < v.Len(); i++ { + v.Index(i).SetZero() // zero remainder of array + } + } else { + v.SetLen(i) // truncate the slice + } + } + if i == 0 && v.Kind() == reflect.Slice { + v.Set(reflect.MakeSlice(v.Type(), 0, 0)) + } + return nil +} + +var ( + nullLiteral = []byte("null") + textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() +) + +// object consumes an object from d.data[d.off-1:], decoding into v. +// The first byte ('{') of the object has been read already. +func (d *decodeState) object(v reflect.Value) error { + // Check for unmarshaler. + u, ut, pv := indirect(v, false) + if u != nil { + start := d.readIndex() + d.skip() + return u.UnmarshalJSON(d.data[start:d.off]) + } + if ut != nil { + d.saveError(&UnmarshalTypeError{Value: "object", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + } + v = pv + t := v.Type() + + // Decoding into nil interface? Switch to non-reflect code. + if v.Kind() == reflect.Interface && v.NumMethod() == 0 { + oi := d.objectInterface() + v.Set(reflect.ValueOf(oi)) + return nil + } + + var fields structFields + + // Check type of target: + // struct or + // map[T1]T2 where T1 is string, an integer type, + // or an encoding.TextUnmarshaler + switch v.Kind() { + case reflect.Map: + // Map key must either have string kind, have an integer kind, + // or be an encoding.TextUnmarshaler. + switch t.Key().Kind() { + case reflect.String, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + default: + if !reflect.PointerTo(t.Key()).Implements(textUnmarshalerType) { + d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)}) + d.skip() + return nil + } + } + if v.IsNil() { + v.Set(reflect.MakeMap(t)) + } + case reflect.Struct: + fields = cachedTypeFields(t) + // ok + default: + d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)}) + d.skip() + return nil + } + + var mapElem reflect.Value + var origErrorContext errorContext + if d.errorContext != nil { + origErrorContext = *d.errorContext + } + + for { + // Read opening " of string key or closing }. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if d.opcode != scanBeginLiteral { + panic(phasePanicMsg) + } + + // Read key. + start := d.readIndex() + d.rescanLiteral() + item := d.data[start:d.readIndex()] + key, ok := unquoteBytes(item) + if !ok { + panic(phasePanicMsg) + } + + // Figure out field corresponding to key. + var subv reflect.Value + destring := false // whether the value is wrapped in a string to be decoded first + + if v.Kind() == reflect.Map { + elemType := t.Elem() + if !mapElem.IsValid() { + mapElem = reflect.New(elemType).Elem() + } else { + mapElem.SetZero() + } + subv = mapElem + } else { + f := fields.byExactName[string(key)] + if f == nil { + f = fields.byFoldedName[string(foldName(key))] + } + if f != nil { + subv = v + destring = f.quoted + for _, i := range f.index { + if subv.Kind() == reflect.Pointer { + if subv.IsNil() { + // If a struct embeds a pointer to an unexported type, + // it is not possible to set a newly allocated value + // since the field is unexported. + // + // See https://golang.org/issue/21357 + if !subv.CanSet() { + d.saveError(fmt.Errorf("json: cannot set embedded pointer to unexported struct: %v", subv.Type().Elem())) + // Invalidate subv to ensure d.value(subv) skips over + // the JSON value without assigning it to subv. + subv = reflect.Value{} + destring = false + break + } + subv.Set(reflect.New(subv.Type().Elem())) + } + subv = subv.Elem() + } + subv = subv.Field(i) + } + if d.errorContext == nil { + d.errorContext = new(errorContext) + } + d.errorContext.FieldStack = append(d.errorContext.FieldStack, f.name) + d.errorContext.Struct = t + } else if d.disallowUnknownFields { + d.saveError(fmt.Errorf("json: unknown field %q", key)) + } + } + + // Read : before value. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode != scanObjectKey { + panic(phasePanicMsg) + } + d.scanWhile(scanSkipSpace) + + if destring { + switch qv := d.valueQuoted().(type) { + case nil: + if err := d.literalStore(nullLiteral, subv, false); err != nil { + return err + } + case string: + if err := d.literalStore([]byte(qv), subv, true); err != nil { + return err + } + default: + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into %v", subv.Type())) + } + } else { + if err := d.value(subv); err != nil { + return err + } + } + + // Write value back to map; + // if using struct, subv points into struct already. + if v.Kind() == reflect.Map { + kt := t.Key() + var kv reflect.Value + switch { + case reflect.PointerTo(kt).Implements(textUnmarshalerType): + kv = reflect.New(kt) + if err := d.literalStore(item, kv, true); err != nil { + return err + } + kv = kv.Elem() + case kt.Kind() == reflect.String: + kv = reflect.ValueOf(key).Convert(kt) + default: + switch kt.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + s := string(key) + n, err := strconv.ParseInt(s, 10, 64) + if err != nil || reflect.Zero(kt).OverflowInt(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: kt, Offset: int64(start + 1)}) + break + } + kv = reflect.ValueOf(n).Convert(kt) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + s := string(key) + n, err := strconv.ParseUint(s, 10, 64) + if err != nil || reflect.Zero(kt).OverflowUint(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: kt, Offset: int64(start + 1)}) + break + } + kv = reflect.ValueOf(n).Convert(kt) + default: + panic("json: Unexpected key type") // should never occur + } + } + if kv.IsValid() { + v.SetMapIndex(kv, subv) + } + } + + // Next token must be , or }. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.errorContext != nil { + // Reset errorContext to its original state. + // Keep the same underlying array for FieldStack, to reuse the + // space and avoid unnecessary allocs. + d.errorContext.FieldStack = d.errorContext.FieldStack[:len(origErrorContext.FieldStack)] + d.errorContext.Struct = origErrorContext.Struct + } + if d.opcode == scanEndObject { + break + } + if d.opcode != scanObjectValue { + panic(phasePanicMsg) + } + } + return nil +} + +// convertNumber converts the number literal s to a float64 or a Number +// depending on the setting of d.useNumber. +func (d *decodeState) convertNumber(s string) (any, error) { + if d.useNumber { + return Number(s), nil + } + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return nil, &UnmarshalTypeError{Value: "number " + s, Type: reflect.TypeOf(0.0), Offset: int64(d.off)} + } + return f, nil +} + +var numberType = reflect.TypeOf(Number("")) + +// literalStore decodes a literal stored in item into v. +// +// fromQuoted indicates whether this literal came from unwrapping a +// string from the ",string" struct tag option. this is used only to +// produce more helpful error messages. +func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) error { + // Check for unmarshaler. + if len(item) == 0 { + // Empty string given + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + return nil + } + isNull := item[0] == 'n' // null + u, ut, pv := indirect(v, isNull) + if u != nil { + return u.UnmarshalJSON(item) + } + if ut != nil { + if item[0] != '"' { + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + return nil + } + val := "number" + switch item[0] { + case 'n': + val = "null" + case 't', 'f': + val = "bool" + } + d.saveError(&UnmarshalTypeError{Value: val, Type: v.Type(), Offset: int64(d.readIndex())}) + return nil + } + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + return ut.UnmarshalText(s) + } + + v = pv + + switch c := item[0]; c { + case 'n': // null + // The main parser checks that only true and false can reach here, + // but if this was a quoted string input, it could be anything. + if fromQuoted && string(item) != "null" { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + break + } + switch v.Kind() { + case reflect.Interface, reflect.Pointer, reflect.Map, reflect.Slice: + v.SetZero() + // otherwise, ignore null for primitives/string + } + case 't', 'f': // true, false + value := item[0] == 't' + // The main parser checks that only true and false can reach here, + // but if this was a quoted string input, it could be anything. + if fromQuoted && string(item) != "true" && string(item) != "false" { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + break + } + switch v.Kind() { + default: + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.saveError(&UnmarshalTypeError{Value: "bool", Type: v.Type(), Offset: int64(d.readIndex())}) + } + case reflect.Bool: + v.SetBool(value) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(value)) + } else { + d.saveError(&UnmarshalTypeError{Value: "bool", Type: v.Type(), Offset: int64(d.readIndex())}) + } + } + + case '"': // string + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + switch v.Kind() { + default: + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + case reflect.Slice: + if v.Type().Elem().Kind() != reflect.Uint8 { + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) + n, err := base64.StdEncoding.Decode(b, s) + if err != nil { + d.saveError(err) + break + } + v.SetBytes(b[:n]) + case reflect.String: + if v.Type() == numberType && !isValidNumber(string(s)) { + return fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", item) + } + v.SetString(string(s)) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(string(s))) + } else { + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + } + } + + default: // number + if c != '-' && (c < '0' || c > '9') { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + s := string(item) + switch v.Kind() { + default: + if v.Kind() == reflect.String && v.Type() == numberType { + // s must be a valid number, because it's + // already been tokenized. + v.SetString(s) + break + } + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + d.saveError(&UnmarshalTypeError{Value: "number", Type: v.Type(), Offset: int64(d.readIndex())}) + case reflect.Interface: + n, err := d.convertNumber(s) + if err != nil { + d.saveError(err) + break + } + if v.NumMethod() != 0 { + d.saveError(&UnmarshalTypeError{Value: "number", Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.Set(reflect.ValueOf(n)) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n, err := strconv.ParseInt(s, 10, 64) + if err != nil || v.OverflowInt(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetInt(n) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + n, err := strconv.ParseUint(s, 10, 64) + if err != nil || v.OverflowUint(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetUint(n) + + case reflect.Float32, reflect.Float64: + n, err := strconv.ParseFloat(s, v.Type().Bits()) + if err != nil || v.OverflowFloat(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetFloat(n) + } + } + return nil +} + +// The xxxInterface routines build up a value to be stored +// in an empty interface. They are not strictly necessary, +// but they avoid the weight of reflection in this common case. + +// valueInterface is like value but returns interface{} +func (d *decodeState) valueInterface() (val any) { + switch d.opcode { + default: + panic(phasePanicMsg) + case scanBeginArray: + val = d.arrayInterface() + d.scanNext() + case scanBeginObject: + val = d.objectInterface() + d.scanNext() + case scanBeginLiteral: + val = d.literalInterface() + } + return +} + +// arrayInterface is like array but returns []interface{}. +func (d *decodeState) arrayInterface() []any { + v := make([]any, 0) + for { + // Look ahead for ] - can only happen on first iteration. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndArray { + break + } + + v = append(v, d.valueInterface()) + + // Next token must be , or ]. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndArray { + break + } + if d.opcode != scanArrayValue { + panic(phasePanicMsg) + } + } + return v +} + +// objectInterface is like object but returns map[string]interface{}. +func (d *decodeState) objectInterface() map[string]any { + m := make(map[string]any) + for { + // Read opening " of string key or closing }. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if d.opcode != scanBeginLiteral { + panic(phasePanicMsg) + } + + // Read string key. + start := d.readIndex() + d.rescanLiteral() + item := d.data[start:d.readIndex()] + key, ok := unquote(item) + if !ok { + panic(phasePanicMsg) + } + + // Read : before value. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode != scanObjectKey { + panic(phasePanicMsg) + } + d.scanWhile(scanSkipSpace) + + // Read value. + m[key] = d.valueInterface() + + // Next token must be , or }. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndObject { + break + } + if d.opcode != scanObjectValue { + panic(phasePanicMsg) + } + } + return m +} + +// literalInterface consumes and returns a literal from d.data[d.off-1:] and +// it reads the following byte ahead. The first byte of the literal has been +// read already (that's how the caller knows it's a literal). +func (d *decodeState) literalInterface() any { + // All bytes inside literal return scanContinue op code. + start := d.readIndex() + d.rescanLiteral() + + item := d.data[start:d.readIndex()] + + switch c := item[0]; c { + case 'n': // null + return nil + + case 't', 'f': // true, false + return c == 't' + + case '"': // string + s, ok := unquote(item) + if !ok { + panic(phasePanicMsg) + } + return s + + default: // number + if c != '-' && (c < '0' || c > '9') { + panic(phasePanicMsg) + } + n, err := d.convertNumber(string(item)) + if err != nil { + d.saveError(err) + } + return n + } +} + +// getu4 decodes \uXXXX from the beginning of s, returning the hex value, +// or it returns -1. +func getu4(s []byte) rune { + if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { + return -1 + } + var r rune + for _, c := range s[2:6] { + switch { + case '0' <= c && c <= '9': + c = c - '0' + case 'a' <= c && c <= 'f': + c = c - 'a' + 10 + case 'A' <= c && c <= 'F': + c = c - 'A' + 10 + default: + return -1 + } + r = r*16 + rune(c) + } + return r +} + +// unquote converts a quoted JSON string literal s into an actual string t. +// The rules are different than for Go, so cannot use strconv.Unquote. +func unquote(s []byte) (t string, ok bool) { + s, ok = unquoteBytes(s) + t = string(s) + return +} + +func unquoteBytes(s []byte) (t []byte, ok bool) { + if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { + return + } + s = s[1 : len(s)-1] + + // Check for unusual characters. If there are none, + // then no unquoting is needed, so return a slice of the + // original bytes. + r := 0 + for r < len(s) { + c := s[r] + if c == '\\' || c == '"' || c < ' ' { + break + } + if c < utf8.RuneSelf { + r++ + continue + } + rr, size := utf8.DecodeRune(s[r:]) + if rr == utf8.RuneError && size == 1 { + break + } + r += size + } + if r == len(s) { + return s, true + } + + b := make([]byte, len(s)+2*utf8.UTFMax) + w := copy(b, s[0:r]) + for r < len(s) { + // Out of room? Can only happen if s is full of + // malformed UTF-8 and we're replacing each + // byte with RuneError. + if w >= len(b)-2*utf8.UTFMax { + nb := make([]byte, (len(b)+utf8.UTFMax)*2) + copy(nb, b[0:w]) + b = nb + } + switch c := s[r]; { + case c == '\\': + r++ + if r >= len(s) { + return + } + switch s[r] { + default: + return + case '"', '\\', '/', '\'': + b[w] = s[r] + r++ + w++ + case 'b': + b[w] = '\b' + r++ + w++ + case 'f': + b[w] = '\f' + r++ + w++ + case 'n': + b[w] = '\n' + r++ + w++ + case 'r': + b[w] = '\r' + r++ + w++ + case 't': + b[w] = '\t' + r++ + w++ + case 'u': + r-- + rr := getu4(s[r:]) + if rr < 0 { + return + } + r += 6 + if utf16.IsSurrogate(rr) { + rr1 := getu4(s[r:]) + if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar { + // A valid pair; consume. + r += 6 + w += utf8.EncodeRune(b[w:], dec) + break + } + // Invalid surrogate; fall back to replacement rune. + rr = unicode.ReplacementChar + } + w += utf8.EncodeRune(b[w:], rr) + } + + // Quote, control characters are invalid. + case c == '"', c < ' ': + return + + // ASCII + case c < utf8.RuneSelf: + b[w] = c + r++ + w++ + + // Coerce to well-formed UTF-8. + default: + rr, size := utf8.DecodeRune(s[r:]) + r += size + w += utf8.EncodeRune(b[w:], rr) + } + } + return b[0:w], true +} diff --git a/common/contextjson/encode.go b/common/contextjson/encode.go new file mode 100644 index 0000000000..6da0bd9c4a --- /dev/null +++ b/common/contextjson/encode.go @@ -0,0 +1,1283 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package json implements encoding and decoding of JSON as defined in +// RFC 7159. The mapping between JSON and Go values is described +// in the documentation for the Marshal and Unmarshal functions. +// +// See "JSON and Go" for an introduction to this package: +// https://golang.org/doc/articles/json_and_go.html +package json + +import ( + "bytes" + "encoding" + "encoding/base64" + "fmt" + "math" + "reflect" + "sort" + "strconv" + "strings" + "sync" + "unicode" + "unicode/utf8" +) + +// Marshal returns the JSON encoding of v. +// +// Marshal traverses the value v recursively. +// If an encountered value implements the Marshaler interface +// and is not a nil pointer, Marshal calls its MarshalJSON method +// to produce JSON. If no MarshalJSON method is present but the +// value implements encoding.TextMarshaler instead, Marshal calls +// its MarshalText method and encodes the result as a JSON string. +// The nil pointer exception is not strictly necessary +// but mimics a similar, necessary exception in the behavior of +// UnmarshalJSON. +// +// Otherwise, Marshal uses the following type-dependent default encodings: +// +// Boolean values encode as JSON booleans. +// +// Floating point, integer, and Number values encode as JSON numbers. +// NaN and +/-Inf values will return an [UnsupportedValueError]. +// +// String values encode as JSON strings coerced to valid UTF-8, +// replacing invalid bytes with the Unicode replacement rune. +// So that the JSON will be safe to embed inside HTML