From 25065d766a0bf781139b7bfe938bbbe801b8ee0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 11 Jun 2024 21:16:33 +0800 Subject: [PATCH 01/25] platform: Prepare connections list --- box.go | 1 + constant/proxy.go | 8 + experimental/clashapi/connections.go | 5 +- experimental/clashapi/server.go | 48 +--- .../clashapi/trafficontrol/manager.go | 91 ++++-- .../clashapi/trafficontrol/tracker.go | 249 +++++++++------- experimental/libbox/command.go | 2 + experimental/libbox/command_client.go | 8 + .../libbox/command_close_connection.go | 53 ++++ experimental/libbox/command_connections.go | 268 ++++++++++++++++++ experimental/libbox/command_group.go | 60 ++-- experimental/libbox/command_server.go | 6 + experimental/libbox/command_status.go | 2 +- experimental/libbox/iterator.go | 4 + experimental/libbox/service.go | 54 ++-- experimental/libbox/setup.go | 6 + inbound/builder.go | 34 +-- log/format.go | 6 +- 18 files changed, 651 insertions(+), 254 deletions(-) create mode 100644 experimental/libbox/command_close_connection.go create mode 100644 experimental/libbox/command_connections.go diff --git a/box.go b/box.go index 70235fd344..3c514cfee2 100644 --- a/box.go +++ b/box.go @@ -111,6 +111,7 @@ func New(options Options) (*Box, error) { ctx, router, logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")), + tag, inboundOptions, options.PlatformInterface, ) diff --git a/constant/proxy.go b/constant/proxy.go index 1e9baee298..3197de6052 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -32,6 +32,12 @@ const ( func ProxyDisplayName(proxyType string) string { switch proxyType { + case TypeTun: + return "TUN" + case TypeRedirect: + return "Redirect" + case TypeTProxy: + return "TProxy" case TypeDirect: return "Direct" case TypeBlock: @@ -42,6 +48,8 @@ func ProxyDisplayName(proxyType string) string { return "SOCKS" case TypeHTTP: return "HTTP" + case TypeMixed: + return "Mixed" case TypeShadowsocks: return "Shadowsocks" case TypeVMess: diff --git a/experimental/clashapi/connections.go b/experimental/clashapi/connections.go index c9471207e8..999d589828 100644 --- a/experimental/clashapi/connections.go +++ b/experimental/clashapi/connections.go @@ -14,6 +14,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/render" + "github.com/gofrs/uuid/v5" ) func connectionRouter(router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler { @@ -76,10 +77,10 @@ func getConnections(trafficManager *trafficontrol.Manager) func(w http.ResponseW func closeConnection(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") + id := uuid.FromStringOrNil(chi.URLParam(r, "id")) snapshot := trafficManager.Snapshot() for _, c := range snapshot.Connections { - if id == c.ID() { + if id == c.Metadata().ID { c.Close() break } diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 375045ac62..5ace3351e8 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -19,7 +19,6 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" - F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" @@ -218,58 +217,15 @@ func (s *Server) TrafficManager() *trafficontrol.Manager { } func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) (net.Conn, adapter.Tracker) { - tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule) + tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.router, matchedRule) return tracker, tracker } func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule) (N.PacketConn, adapter.Tracker) { - tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule) + tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.router, matchedRule) return tracker, tracker } -func castMetadata(metadata adapter.InboundContext) trafficontrol.Metadata { - var inbound string - if metadata.Inbound != "" { - inbound = metadata.InboundType + "/" + metadata.Inbound - } else { - inbound = metadata.InboundType - } - var domain string - if metadata.Domain != "" { - domain = metadata.Domain - } else { - domain = metadata.Destination.Fqdn - } - var processPath string - if metadata.ProcessInfo != nil { - if metadata.ProcessInfo.ProcessPath != "" { - processPath = metadata.ProcessInfo.ProcessPath - } else if metadata.ProcessInfo.PackageName != "" { - processPath = metadata.ProcessInfo.PackageName - } - if processPath == "" { - if metadata.ProcessInfo.UserId != -1 { - processPath = F.ToString(metadata.ProcessInfo.UserId) - } - } else if metadata.ProcessInfo.User != "" { - processPath = F.ToString(processPath, " (", metadata.ProcessInfo.User, ")") - } else if metadata.ProcessInfo.UserId != -1 { - processPath = F.ToString(processPath, " (", metadata.ProcessInfo.UserId, ")") - } - } - return trafficontrol.Metadata{ - NetWork: metadata.Network, - Type: inbound, - SrcIP: metadata.Source.Addr, - DstIP: metadata.Destination.Addr, - SrcPort: F.ToString(metadata.Source.Port), - DstPort: F.ToString(metadata.Destination.Port), - Host: domain, - DNSMode: "normal", - ProcessPath: processPath, - } -} - func authentication(serverSecret string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { diff --git a/experimental/clashapi/trafficontrol/manager.go b/experimental/clashapi/trafficontrol/manager.go index eac7aee4d8..9b22f1e3d9 100644 --- a/experimental/clashapi/trafficontrol/manager.go +++ b/experimental/clashapi/trafficontrol/manager.go @@ -2,10 +2,17 @@ package trafficontrol import ( "runtime" + "sync" "time" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/clashapi/compatible" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/atomic" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/x/list" + + "github.com/gofrs/uuid/v5" ) type Manager struct { @@ -16,9 +23,11 @@ type Manager struct { uploadTotal atomic.Int64 downloadTotal atomic.Int64 - connections compatible.Map[string, tracker] - ticker *time.Ticker - done chan struct{} + connections compatible.Map[uuid.UUID, Tracker] + closedConnectionsAccess sync.Mutex + closedConnections list.List[TrackerMetadata] + ticker *time.Ticker + done chan struct{} // process *process.Process memory uint64 } @@ -33,12 +42,22 @@ func NewManager() *Manager { return manager } -func (m *Manager) Join(c tracker) { - m.connections.Store(c.ID(), c) +func (m *Manager) Join(c Tracker) { + m.connections.Store(c.Metadata().ID, c) } -func (m *Manager) Leave(c tracker) { - m.connections.Delete(c.ID()) +func (m *Manager) Leave(c Tracker) { + metadata := c.Metadata() + _, loaded := m.connections.LoadAndDelete(metadata.ID) + if loaded { + metadata.ClosedAt = time.Now() + m.closedConnectionsAccess.Lock() + defer m.closedConnectionsAccess.Unlock() + if m.closedConnections.Len() >= 1000 { + m.closedConnections.PopFront() + } + m.closedConnections.PushBack(metadata) + } } func (m *Manager) PushUploaded(size int64) { @@ -59,14 +78,39 @@ func (m *Manager) Total() (up int64, down int64) { return m.uploadTotal.Load(), m.downloadTotal.Load() } -func (m *Manager) Connections() int { +func (m *Manager) ConnectionsLen() int { return m.connections.Len() } +func (m *Manager) Connections() []TrackerMetadata { + var connections []TrackerMetadata + m.connections.Range(func(_ uuid.UUID, value Tracker) bool { + connections = append(connections, value.Metadata()) + return true + }) + return connections +} + +func (m *Manager) ClosedConnections() []TrackerMetadata { + m.closedConnectionsAccess.Lock() + defer m.closedConnectionsAccess.Unlock() + return m.closedConnections.Array() +} + +func (m *Manager) Connection(id uuid.UUID) Tracker { + connection, loaded := m.connections.Load(id) + if !loaded { + return nil + } + return connection +} + func (m *Manager) Snapshot() *Snapshot { - var connections []tracker - m.connections.Range(func(_ string, value tracker) bool { - connections = append(connections, value) + var connections []Tracker + m.connections.Range(func(_ uuid.UUID, value Tracker) bool { + if value.Metadata().OutboundType != C.TypeDNS { + connections = append(connections, value) + } return true }) @@ -75,10 +119,10 @@ func (m *Manager) Snapshot() *Snapshot { m.memory = memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased return &Snapshot{ - UploadTotal: m.uploadTotal.Load(), - DownloadTotal: m.downloadTotal.Load(), - Connections: connections, - Memory: m.memory, + Upload: m.uploadTotal.Load(), + Download: m.downloadTotal.Load(), + Connections: connections, + Memory: m.memory, } } @@ -114,8 +158,17 @@ func (m *Manager) Close() error { } type Snapshot struct { - DownloadTotal int64 `json:"downloadTotal"` - UploadTotal int64 `json:"uploadTotal"` - Connections []tracker `json:"connections"` - Memory uint64 `json:"memory"` + Download int64 + Upload int64 + Connections []Tracker + Memory uint64 +} + +func (s *Snapshot) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]any{ + "downloadTotal": s.Download, + "uploadTotal": s.Upload, + "connections": common.Map(s.Connections, func(t Tracker) TrackerMetadata { return t.Metadata() }), + "memory": s.Memory, + }) } diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go index 4e635d1257..73c28e69bd 100644 --- a/experimental/clashapi/trafficontrol/tracker.go +++ b/experimental/clashapi/trafficontrol/tracker.go @@ -2,97 +2,135 @@ package trafficontrol import ( "net" - "net/netip" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/bufio" + F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" N "github.com/sagernet/sing/common/network" "github.com/gofrs/uuid/v5" ) -type Metadata struct { - NetWork string `json:"network"` - Type string `json:"type"` - SrcIP netip.Addr `json:"sourceIP"` - DstIP netip.Addr `json:"destinationIP"` - SrcPort string `json:"sourcePort"` - DstPort string `json:"destinationPort"` - Host string `json:"host"` - DNSMode string `json:"dnsMode"` - ProcessPath string `json:"processPath"` +type TrackerMetadata struct { + ID uuid.UUID + Metadata adapter.InboundContext + CreatedAt time.Time + ClosedAt time.Time + Upload *atomic.Int64 + Download *atomic.Int64 + Chain []string + Rule adapter.Rule + Outbound string + OutboundType string } -type tracker interface { - ID() string - Close() error - Leave() -} - -type trackerInfo struct { - UUID uuid.UUID `json:"id"` - Metadata Metadata `json:"metadata"` - UploadTotal *atomic.Int64 `json:"upload"` - DownloadTotal *atomic.Int64 `json:"download"` - Start time.Time `json:"start"` - Chain []string `json:"chains"` - Rule string `json:"rule"` - RulePayload string `json:"rulePayload"` -} - -func (t trackerInfo) MarshalJSON() ([]byte, error) { +func (t TrackerMetadata) MarshalJSON() ([]byte, error) { + var inbound string + if t.Metadata.Inbound != "" { + inbound = t.Metadata.InboundType + "/" + t.Metadata.Inbound + } else { + inbound = t.Metadata.InboundType + } + var domain string + if t.Metadata.Domain != "" { + domain = t.Metadata.Domain + } else { + domain = t.Metadata.Destination.Fqdn + } + var processPath string + if t.Metadata.ProcessInfo != nil { + if t.Metadata.ProcessInfo.ProcessPath != "" { + processPath = t.Metadata.ProcessInfo.ProcessPath + } else if t.Metadata.ProcessInfo.PackageName != "" { + processPath = t.Metadata.ProcessInfo.PackageName + } + if processPath == "" { + if t.Metadata.ProcessInfo.UserId != -1 { + processPath = F.ToString(t.Metadata.ProcessInfo.UserId) + } + } else if t.Metadata.ProcessInfo.User != "" { + processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.User, ")") + } else if t.Metadata.ProcessInfo.UserId != -1 { + processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserId, ")") + } + } + var rule string + if t.Rule != nil { + rule = F.ToString(t.Rule, " => ", t.Rule.Outbound()) + } else { + rule = "final" + } return json.Marshal(map[string]any{ - "id": t.UUID.String(), - "metadata": t.Metadata, - "upload": t.UploadTotal.Load(), - "download": t.DownloadTotal.Load(), - "start": t.Start, + "id": t.ID, + "metadata": map[string]any{ + "network": t.Metadata.Network, + "type": inbound, + "sourceIP": t.Metadata.Source.Addr, + "destinationIP": t.Metadata.Destination.Addr, + "sourcePort": F.ToString(t.Metadata.Source.Port), + "destinationPort": F.ToString(t.Metadata.Destination.Port), + "host": domain, + "dnsMode": "normal", + "processPath": processPath, + }, + "upload": t.Upload.Load(), + "download": t.Download.Load(), + "start": t.CreatedAt, "chains": t.Chain, - "rule": t.Rule, - "rulePayload": t.RulePayload, + "rule": rule, + "rulePayload": "", }) } -type tcpTracker struct { - N.ExtendedConn `json:"-"` - *trackerInfo - manager *Manager +type Tracker interface { + adapter.Tracker + Metadata() TrackerMetadata + Close() error +} + +type TCPConn struct { + N.ExtendedConn + metadata TrackerMetadata + manager *Manager } -func (tt *tcpTracker) ID() string { - return tt.UUID.String() +func (tt *TCPConn) Metadata() TrackerMetadata { + return tt.metadata } -func (tt *tcpTracker) Close() error { +func (tt *TCPConn) Close() error { tt.manager.Leave(tt) return tt.ExtendedConn.Close() } -func (tt *tcpTracker) Leave() { +func (tt *TCPConn) Leave() { tt.manager.Leave(tt) } -func (tt *tcpTracker) Upstream() any { +func (tt *TCPConn) Upstream() any { return tt.ExtendedConn } -func (tt *tcpTracker) ReaderReplaceable() bool { +func (tt *TCPConn) ReaderReplaceable() bool { return true } -func (tt *tcpTracker) WriterReplaceable() bool { +func (tt *TCPConn) WriterReplaceable() bool { return true } -func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router adapter.Router, rule adapter.Rule) *tcpTracker { - uuid, _ := uuid.NewV4() - - var chain []string - var next string +func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *TCPConn { + id, _ := uuid.NewV4() + var ( + chain []string + next string + outbound string + outboundType string + ) if rule == nil { if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { next = defaultOutbound.Tag() @@ -106,17 +144,17 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad if !loaded { break } + outbound = detour.Tag() + outboundType = detour.Type() group, isGroup := detour.(adapter.OutboundGroup) if !isGroup { break } next = group.Now() } - upload := new(atomic.Int64) download := new(atomic.Int64) - - t := &tcpTracker{ + tracker := &TCPConn{ ExtendedConn: bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) { upload.Add(n) manager.PushUploaded(n) @@ -124,64 +162,62 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad download.Add(n) manager.PushDownloaded(n) }}), - manager: manager, - trackerInfo: &trackerInfo{ - UUID: uuid, - Start: time.Now(), - Metadata: metadata, - Chain: common.Reverse(chain), - Rule: "", - UploadTotal: upload, - DownloadTotal: download, + metadata: TrackerMetadata{ + ID: id, + Metadata: metadata, + CreatedAt: time.Now(), + Upload: upload, + Download: download, + Chain: common.Reverse(chain), + Rule: rule, + Outbound: outbound, + OutboundType: outboundType, }, + manager: manager, } - - if rule != nil { - t.trackerInfo.Rule = rule.String() + " => " + rule.Outbound() - } else { - t.trackerInfo.Rule = "final" - } - - manager.Join(t) - return t + manager.Join(tracker) + return tracker } -type udpTracker struct { +type UDPConn struct { N.PacketConn `json:"-"` - *trackerInfo - manager *Manager + metadata TrackerMetadata + manager *Manager } -func (ut *udpTracker) ID() string { - return ut.UUID.String() +func (ut *UDPConn) Metadata() TrackerMetadata { + return ut.metadata } -func (ut *udpTracker) Close() error { +func (ut *UDPConn) Close() error { ut.manager.Leave(ut) return ut.PacketConn.Close() } -func (ut *udpTracker) Leave() { +func (ut *UDPConn) Leave() { ut.manager.Leave(ut) } -func (ut *udpTracker) Upstream() any { +func (ut *UDPConn) Upstream() any { return ut.PacketConn } -func (ut *udpTracker) ReaderReplaceable() bool { +func (ut *UDPConn) ReaderReplaceable() bool { return true } -func (ut *udpTracker) WriterReplaceable() bool { +func (ut *UDPConn) WriterReplaceable() bool { return true } -func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, router adapter.Router, rule adapter.Rule) *udpTracker { - uuid, _ := uuid.NewV4() - - var chain []string - var next string +func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *UDPConn { + id, _ := uuid.NewV4() + var ( + chain []string + next string + outbound string + outboundType string + ) if rule == nil { if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil { next = defaultOutbound.Tag() @@ -195,17 +231,17 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route if !loaded { break } + outbound = detour.Tag() + outboundType = detour.Type() group, isGroup := detour.(adapter.OutboundGroup) if !isGroup { break } next = group.Now() } - upload := new(atomic.Int64) download := new(atomic.Int64) - - ut := &udpTracker{ + trackerConn := &UDPConn{ PacketConn: bufio.NewCounterPacketConn(conn, []N.CountFunc{func(n int64) { upload.Add(n) manager.PushUploaded(n) @@ -213,24 +249,19 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route download.Add(n) manager.PushDownloaded(n) }}), - manager: manager, - trackerInfo: &trackerInfo{ - UUID: uuid, - Start: time.Now(), - Metadata: metadata, - Chain: common.Reverse(chain), - Rule: "", - UploadTotal: upload, - DownloadTotal: download, + metadata: TrackerMetadata{ + ID: id, + Metadata: metadata, + CreatedAt: time.Now(), + Upload: upload, + Download: download, + Chain: common.Reverse(chain), + Rule: rule, + Outbound: outbound, + OutboundType: outboundType, }, + manager: manager, } - - if rule != nil { - ut.trackerInfo.Rule = rule.String() + " => " + rule.Outbound() - } else { - ut.trackerInfo.Rule = "final" - } - - manager.Join(ut) - return ut + manager.Join(trackerConn) + return trackerConn } diff --git a/experimental/libbox/command.go b/experimental/libbox/command.go index 7915419d64..f9aca13ff9 100644 --- a/experimental/libbox/command.go +++ b/experimental/libbox/command.go @@ -14,4 +14,6 @@ const ( CommandSetClashMode CommandGetSystemProxyStatus CommandSetSystemProxyEnabled + CommandConnections + CommandCloseConnection ) diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index f3c9ad2a19..199dce0d18 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -31,6 +31,7 @@ type CommandClientHandler interface { WriteGroups(message OutboundGroupIterator) InitializeClashMode(modeList StringIterator, currentMode string) UpdateClashMode(newMode string) + WriteConnections(message *Connections) } func NewStandaloneCommandClient() *CommandClient { @@ -116,6 +117,13 @@ func (c *CommandClient) Connect() error { return nil } go c.handleModeConn(conn) + case CommandConnections: + err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval) + if err != nil { + return E.Cause(err, "write interval") + } + c.handler.Connected() + go c.handleConnectionsConn(conn) } return nil } diff --git a/experimental/libbox/command_close_connection.go b/experimental/libbox/command_close_connection.go new file mode 100644 index 0000000000..62f5dc8419 --- /dev/null +++ b/experimental/libbox/command_close_connection.go @@ -0,0 +1,53 @@ +package libbox + +import ( + "bufio" + "net" + + "github.com/sagernet/sing-box/experimental/clashapi" + "github.com/sagernet/sing/common/binary" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/gofrs/uuid/v5" +) + +func (c *CommandClient) CloseConnection(connId string) error { + conn, err := c.directConnect() + if err != nil { + return err + } + defer conn.Close() + writer := bufio.NewWriter(conn) + err = binary.WriteData(writer, binary.BigEndian, connId) + if err != nil { + return err + } + err = writer.Flush() + if err != nil { + return err + } + return readError(conn) +} + +func (s *CommandServer) handleCloseConnection(conn net.Conn) error { + reader := bufio.NewReader(conn) + var connId string + err := binary.ReadData(reader, binary.BigEndian, &connId) + if err != nil { + return E.Cause(err, "read connection id") + } + service := s.service + if service == nil { + return writeError(conn, E.New("service not ready")) + } + clashServer := service.instance.Router().ClashServer() + if clashServer == nil { + return writeError(conn, E.New("Clash API disabled")) + } + targetConn := clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(connId)) + if targetConn == nil { + return writeError(conn, E.New("connection already closed")) + } + targetConn.Close() + return writeError(conn, nil) +} diff --git a/experimental/libbox/command_connections.go b/experimental/libbox/command_connections.go new file mode 100644 index 0000000000..44d35bbedc --- /dev/null +++ b/experimental/libbox/command_connections.go @@ -0,0 +1,268 @@ +package libbox + +import ( + "bufio" + "net" + "slices" + "strings" + "time" + + "github.com/sagernet/sing-box/experimental/clashapi" + "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" + "github.com/sagernet/sing/common/binary" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + + "github.com/gofrs/uuid/v5" +) + +func (c *CommandClient) handleConnectionsConn(conn net.Conn) { + defer conn.Close() + reader := bufio.NewReader(conn) + var connections Connections + for { + err := binary.ReadData(reader, binary.BigEndian, &connections.connections) + if err != nil { + c.handler.Disconnected(err.Error()) + return + } + c.handler.WriteConnections(&connections) + } +} + +func (s *CommandServer) handleConnectionsConn(conn net.Conn) error { + var interval int64 + err := binary.Read(conn, binary.BigEndian, &interval) + if err != nil { + return E.Cause(err, "read interval") + } + ticker := time.NewTicker(time.Duration(interval)) + defer ticker.Stop() + ctx := connKeepAlive(conn) + var trafficManager *trafficontrol.Manager + for { + service := s.service + if service != nil { + clashServer := service.instance.Router().ClashServer() + if clashServer == nil { + return E.New("Clash API disabled") + } + trafficManager = clashServer.(*clashapi.Server).TrafficManager() + break + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + } + } + var ( + connections = make(map[uuid.UUID]*Connection) + outConnections []Connection + ) + writer := bufio.NewWriter(conn) + for { + outConnections = outConnections[:0] + for _, connection := range trafficManager.Connections() { + outConnections = append(outConnections, newConnection(connections, connection, false)) + } + for _, connection := range trafficManager.ClosedConnections() { + outConnections = append(outConnections, newConnection(connections, connection, true)) + } + err = binary.WriteData(writer, binary.BigEndian, outConnections) + if err != nil { + return err + } + err = writer.Flush() + if err != nil { + return err + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + } + } +} + +const ( + ConnectionStateAll = iota + ConnectionStateActive + ConnectionStateClosed +) + +type Connections struct { + connections []Connection + filteredConnections []Connection + outConnections *[]Connection +} + +func (c *Connections) FilterState(state int32) { + c.filteredConnections = c.filteredConnections[:0] + switch state { + case ConnectionStateAll: + c.filteredConnections = append(c.filteredConnections, c.connections...) + case ConnectionStateActive: + for _, connection := range c.connections { + if connection.ClosedAt == 0 { + c.filteredConnections = append(c.filteredConnections, connection) + } + } + case ConnectionStateClosed: + for _, connection := range c.connections { + if connection.ClosedAt != 0 { + c.filteredConnections = append(c.filteredConnections, connection) + } + } + } +} + +func (c *Connections) SortByDate() { + slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int { + if x.CreatedAt < y.CreatedAt { + return 1 + } else if x.CreatedAt > y.CreatedAt { + return -1 + } else { + return strings.Compare(y.ID, x.ID) + } + }) +} + +func (c *Connections) SortByTraffic() { + slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int { + xTraffic := x.Uplink + x.Downlink + yTraffic := y.Uplink + y.Downlink + if xTraffic < yTraffic { + return 1 + } else if xTraffic > yTraffic { + return -1 + } else { + return strings.Compare(y.ID, x.ID) + } + }) +} + +func (c *Connections) SortByTrafficTotal() { + slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int { + xTraffic := x.UplinkTotal + x.DownlinkTotal + yTraffic := y.UplinkTotal + y.DownlinkTotal + if xTraffic < yTraffic { + return 1 + } else if xTraffic > yTraffic { + return -1 + } else { + return strings.Compare(y.ID, x.ID) + } + }) +} + +func (c *Connections) Iterator() ConnectionIterator { + return newPtrIterator(c.filteredConnections) +} + +type Connection struct { + ID string + Inbound string + InboundType string + IPVersion int32 + Network string + Source string + Destination string + Domain string + Protocol string + User string + FromOutbound string + CreatedAt int64 + ClosedAt int64 + Uplink int64 + Downlink int64 + UplinkTotal int64 + DownlinkTotal int64 + Rule string + Outbound string + OutboundType string + ChainList []string +} + +func (c *Connection) Chain() StringIterator { + return newIterator(c.ChainList) +} + +func (c *Connection) DisplayDestination() string { + destination := M.ParseSocksaddr(c.Destination) + if destination.IsIP() && c.Domain != "" { + destination = M.Socksaddr{ + Fqdn: c.Domain, + Port: destination.Port, + } + return destination.String() + } + return c.Destination +} + +type ConnectionIterator interface { + Next() *Connection + HasNext() bool +} + +func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol.TrackerMetadata, isClosed bool) Connection { + if oldConnection, loaded := connections[metadata.ID]; loaded { + if isClosed { + if oldConnection.ClosedAt == 0 { + oldConnection.Uplink = 0 + oldConnection.Downlink = 0 + oldConnection.ClosedAt = metadata.ClosedAt.UnixMilli() + } + return *oldConnection + } + lastUplink := oldConnection.UplinkTotal + lastDownlink := oldConnection.DownlinkTotal + uplinkTotal := metadata.Upload.Load() + downlinkTotal := metadata.Download.Load() + oldConnection.Uplink = uplinkTotal - lastUplink + oldConnection.Downlink = downlinkTotal - lastDownlink + oldConnection.UplinkTotal = uplinkTotal + oldConnection.DownlinkTotal = downlinkTotal + return *oldConnection + } + var rule string + if metadata.Rule != nil { + rule = metadata.Rule.String() + } + uplinkTotal := metadata.Upload.Load() + downlinkTotal := metadata.Download.Load() + uplink := uplinkTotal + downlink := downlinkTotal + var closedAt int64 + if !metadata.ClosedAt.IsZero() { + closedAt = metadata.ClosedAt.UnixMilli() + uplink = 0 + downlink = 0 + } + connection := Connection{ + ID: metadata.ID.String(), + Inbound: metadata.Metadata.Inbound, + InboundType: metadata.Metadata.InboundType, + IPVersion: int32(metadata.Metadata.IPVersion), + Network: metadata.Metadata.Network, + Source: metadata.Metadata.Source.String(), + Destination: metadata.Metadata.Destination.String(), + Domain: metadata.Metadata.Domain, + Protocol: metadata.Metadata.Protocol, + User: metadata.Metadata.User, + FromOutbound: metadata.Metadata.Outbound, + CreatedAt: metadata.CreatedAt.UnixMilli(), + ClosedAt: closedAt, + Uplink: uplink, + Downlink: downlink, + UplinkTotal: uplinkTotal, + DownlinkTotal: downlinkTotal, + Rule: rule, + Outbound: metadata.Outbound, + OutboundType: metadata.OutboundType, + ChainList: metadata.Chain, + } + connections[metadata.ID] = &connection + return connection +} diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go index 21fd39d29a..a5572ea1f5 100644 --- a/experimental/libbox/command_group.go +++ b/experimental/libbox/command_group.go @@ -14,36 +14,6 @@ import ( "github.com/sagernet/sing/service" ) -type OutboundGroup struct { - Tag string - Type string - Selectable bool - Selected string - IsExpand bool - items []*OutboundGroupItem -} - -func (g *OutboundGroup) GetItems() OutboundGroupItemIterator { - return newIterator(g.items) -} - -type OutboundGroupIterator interface { - Next() *OutboundGroup - HasNext() bool -} - -type OutboundGroupItem struct { - Tag string - Type string - URLTestTime int64 - URLTestDelay int32 -} - -type OutboundGroupItemIterator interface { - Next() *OutboundGroupItem - HasNext() bool -} - func (c *CommandClient) handleGroupConn(conn net.Conn) { defer conn.Close() @@ -92,6 +62,36 @@ func (s *CommandServer) handleGroupConn(conn net.Conn) error { } } +type OutboundGroup struct { + Tag string + Type string + Selectable bool + Selected string + IsExpand bool + items []*OutboundGroupItem +} + +func (g *OutboundGroup) GetItems() OutboundGroupItemIterator { + return newIterator(g.items) +} + +type OutboundGroupIterator interface { + Next() *OutboundGroup + HasNext() bool +} + +type OutboundGroupItem struct { + Tag string + Type string + URLTestTime int64 + URLTestDelay int32 +} + +type OutboundGroupItemIterator interface { + Next() *OutboundGroupItem + HasNext() bool +} + func readGroups(reader io.Reader) (OutboundGroupIterator, error) { var groupLength uint16 err := binary.Read(reader, binary.BigEndian, &groupLength) diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go index da931ef552..8918756dae 100644 --- a/experimental/libbox/command_server.go +++ b/experimental/libbox/command_server.go @@ -33,6 +33,8 @@ type CommandServer struct { urlTestUpdate chan struct{} modeUpdate chan struct{} logReset chan struct{} + + closedConnections []Connection } type CommandServerHandler interface { @@ -176,6 +178,10 @@ func (s *CommandServer) handleConnection(conn net.Conn) error { return s.handleGetSystemProxyStatus(conn) case CommandSetSystemProxyEnabled: return s.handleSetSystemProxyEnabled(conn) + case CommandConnections: + return s.handleConnectionsConn(conn) + case CommandCloseConnection: + return s.handleCloseConnection(conn) default: return E.New("unknown command: ", command) } diff --git a/experimental/libbox/command_status.go b/experimental/libbox/command_status.go index 7f1eca8c03..4ab09d4b28 100644 --- a/experimental/libbox/command_status.go +++ b/experimental/libbox/command_status.go @@ -36,7 +36,7 @@ func (s *CommandServer) readStatus() StatusMessage { trafficManager := clashServer.(*clashapi.Server).TrafficManager() message.Uplink, message.Downlink = trafficManager.Now() message.UplinkTotal, message.DownlinkTotal = trafficManager.Total() - message.ConnectionsIn = int32(trafficManager.Connections()) + message.ConnectionsIn = int32(trafficManager.ConnectionsLen()) } } diff --git a/experimental/libbox/iterator.go b/experimental/libbox/iterator.go index db64a25978..530a7e43bd 100644 --- a/experimental/libbox/iterator.go +++ b/experimental/libbox/iterator.go @@ -17,6 +17,10 @@ func newIterator[T any](values []T) *iterator[T] { return &iterator[T]{values} } +func newPtrIterator[T any](values []T) *iterator[*T] { + return &iterator[*T]{common.Map(values, func(value T) *T { return &value })} +} + func (i *iterator[T]) Next() T { if len(i.values) == 0 { return common.DefaultValue[T]() diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 0a54d7abd8..c65090103c 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -149,33 +149,6 @@ func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions return tun.New(*options) } -func (w *platformInterfaceWrapper) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*process.Info, error) { - var uid int32 - if w.useProcFS { - uid = procfs.ResolveSocketByProcSearch(network, source, destination) - if uid == -1 { - return nil, E.New("procfs: not found") - } - } else { - var ipProtocol int32 - switch N.NetworkName(network) { - case N.NetworkTCP: - ipProtocol = syscall.IPPROTO_TCP - case N.NetworkUDP: - ipProtocol = syscall.IPPROTO_UDP - default: - return nil, E.New("unknown network: ", network) - } - var err error - uid, err = w.iif.FindConnectionOwner(ipProtocol, source.Addr().String(), int32(source.Port()), destination.Addr().String(), int32(destination.Port())) - if err != nil { - return nil, err - } - } - packageName, _ := w.iif.PackageNameByUid(uid) - return &process.Info{UserId: uid, PackageName: packageName}, nil -} - func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool { return w.iif.UsePlatformDefaultInterfaceMonitor() } @@ -229,6 +202,33 @@ func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState { return (adapter.WIFIState)(*wifiState) } +func (w *platformInterfaceWrapper) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*process.Info, error) { + var uid int32 + if w.useProcFS { + uid = procfs.ResolveSocketByProcSearch(network, source, destination) + if uid == -1 { + return nil, E.New("procfs: not found") + } + } else { + var ipProtocol int32 + switch N.NetworkName(network) { + case N.NetworkTCP: + ipProtocol = syscall.IPPROTO_TCP + case N.NetworkUDP: + ipProtocol = syscall.IPPROTO_UDP + default: + return nil, E.New("unknown network: ", network) + } + var err error + uid, err = w.iif.FindConnectionOwner(ipProtocol, source.Addr().String(), int32(source.Port()), destination.Addr().String(), int32(destination.Port())) + if err != nil { + return nil, err + } + } + packageName, _ := w.iif.PackageNameByUid(uid) + return &process.Info{UserId: uid, PackageName: packageName}, nil +} + func (w *platformInterfaceWrapper) DisableColors() bool { return runtime.GOOS != "android" } diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index ea468f391c..31611354bf 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -4,10 +4,12 @@ import ( "os" "os/user" "strconv" + "time" "github.com/sagernet/sing-box/common/humanize" C "github.com/sagernet/sing-box/constant" _ "github.com/sagernet/sing-box/include" + "github.com/sagernet/sing-box/log" ) var ( @@ -59,6 +61,10 @@ func FormatMemoryBytes(length int64) string { return humanize.MemoryBytes(uint64(length)) } +func FormatDuration(duration int64) string { + return log.FormatDuration(time.Duration(duration) * time.Millisecond) +} + func ProxyDisplayType(proxyType string) string { return C.ProxyDisplayName(proxyType) } diff --git a/inbound/builder.go b/inbound/builder.go index 513b016f79..ddfd361dbd 100644 --- a/inbound/builder.go +++ b/inbound/builder.go @@ -11,43 +11,43 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.Inbound, platformInterface platform.Interface) (adapter.Inbound, error) { +func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Inbound, platformInterface platform.Interface) (adapter.Inbound, error) { if options.Type == "" { return nil, E.New("missing inbound type") } switch options.Type { case C.TypeTun: - return NewTun(ctx, router, logger, options.Tag, options.TunOptions, platformInterface) + return NewTun(ctx, router, logger, tag, options.TunOptions, platformInterface) case C.TypeRedirect: - return NewRedirect(ctx, router, logger, options.Tag, options.RedirectOptions), nil + return NewRedirect(ctx, router, logger, tag, options.RedirectOptions), nil case C.TypeTProxy: - return NewTProxy(ctx, router, logger, options.Tag, options.TProxyOptions), nil + return NewTProxy(ctx, router, logger, tag, options.TProxyOptions), nil case C.TypeDirect: - return NewDirect(ctx, router, logger, options.Tag, options.DirectOptions), nil + return NewDirect(ctx, router, logger, tag, options.DirectOptions), nil case C.TypeSOCKS: - return NewSocks(ctx, router, logger, options.Tag, options.SocksOptions), nil + return NewSocks(ctx, router, logger, tag, options.SocksOptions), nil case C.TypeHTTP: - return NewHTTP(ctx, router, logger, options.Tag, options.HTTPOptions) + return NewHTTP(ctx, router, logger, tag, options.HTTPOptions) case C.TypeMixed: - return NewMixed(ctx, router, logger, options.Tag, options.MixedOptions), nil + return NewMixed(ctx, router, logger, tag, options.MixedOptions), nil case C.TypeShadowsocks: - return NewShadowsocks(ctx, router, logger, options.Tag, options.ShadowsocksOptions) + return NewShadowsocks(ctx, router, logger, tag, options.ShadowsocksOptions) case C.TypeVMess: - return NewVMess(ctx, router, logger, options.Tag, options.VMessOptions) + return NewVMess(ctx, router, logger, tag, options.VMessOptions) case C.TypeTrojan: - return NewTrojan(ctx, router, logger, options.Tag, options.TrojanOptions) + return NewTrojan(ctx, router, logger, tag, options.TrojanOptions) case C.TypeNaive: - return NewNaive(ctx, router, logger, options.Tag, options.NaiveOptions) + return NewNaive(ctx, router, logger, tag, options.NaiveOptions) case C.TypeHysteria: - return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions) + return NewHysteria(ctx, router, logger, tag, options.HysteriaOptions) case C.TypeShadowTLS: - return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions) + return NewShadowTLS(ctx, router, logger, tag, options.ShadowTLSOptions) case C.TypeVLESS: - return NewVLESS(ctx, router, logger, options.Tag, options.VLESSOptions) + return NewVLESS(ctx, router, logger, tag, options.VLESSOptions) case C.TypeTUIC: - return NewTUIC(ctx, router, logger, options.Tag, options.TUICOptions) + return NewTUIC(ctx, router, logger, tag, options.TUICOptions) case C.TypeHysteria2: - return NewHysteria2(ctx, router, logger, options.Tag, options.Hysteria2Options) + return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options) default: return nil, E.New("unknown inbound type: ", options.Type) } diff --git a/log/format.go b/log/format.go index 6fb91d3157..6f4347b12a 100644 --- a/log/format.go +++ b/log/format.go @@ -43,7 +43,7 @@ func (f Formatter) Format(ctx context.Context, level Level, tag string, message id, hasId = IDFromContext(ctx) } if hasId { - activeDuration := formatDuration(time.Since(id.CreatedAt)) + activeDuration := FormatDuration(time.Since(id.CreatedAt)) if !f.DisableColors { var color aurora.Color color = aurora.Color(uint8(id.ID)) @@ -113,7 +113,7 @@ func (f Formatter) FormatWithSimple(ctx context.Context, level Level, tag string id, hasId = IDFromContext(ctx) } if hasId { - activeDuration := formatDuration(time.Since(id.CreatedAt)) + activeDuration := FormatDuration(time.Since(id.CreatedAt)) if !f.DisableColors { var color aurora.Color color = aurora.Color(uint8(id.ID)) @@ -163,7 +163,7 @@ func xd(value int, x int) string { return message } -func formatDuration(duration time.Duration) string { +func FormatDuration(duration time.Duration) string { if duration < time.Second { return F.ToString(duration.Milliseconds(), "ms") } else if duration < time.Minute { From 03f5d6e4f2305ddb965125a548254477567b62e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 18 Jun 2024 17:49:06 +0800 Subject: [PATCH 02/25] platform: Add log update interval --- experimental/libbox/command_client.go | 8 ++- experimental/libbox/command_log.go | 95 ++++++++++++++++----------- experimental/libbox/iterator.go | 15 +++-- 3 files changed, 71 insertions(+), 47 deletions(-) diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index 199dce0d18..bd99511582 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -25,8 +25,8 @@ type CommandClientOptions struct { type CommandClientHandler interface { Connected() Disconnected(message string) - ClearLog() - WriteLog(message string) + ClearLogs() + WriteLogs(messageList StringIterator) WriteStatus(message *StatusMessage) WriteGroups(message OutboundGroupIterator) InitializeClashMode(modeList StringIterator, currentMode string) @@ -84,6 +84,10 @@ func (c *CommandClient) Connect() error { } switch c.options.Command { case CommandLog: + err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval) + if err != nil { + return E.Cause(err, "write interval") + } c.handler.Connected() go c.handleLogConn(conn) case CommandStatus: diff --git a/experimental/libbox/command_log.go b/experimental/libbox/command_log.go index ce72010dd0..8a22aa2e74 100644 --- a/experimental/libbox/command_log.go +++ b/experimental/libbox/command_log.go @@ -1,10 +1,14 @@ package libbox import ( + "bufio" "context" - "encoding/binary" "io" "net" + "time" + + "github.com/sagernet/sing/common/binary" + E "github.com/sagernet/sing/common/exceptions" ) func (s *CommandServer) WriteMessage(message string) { @@ -17,43 +21,39 @@ func (s *CommandServer) WriteMessage(message string) { s.access.Unlock() } -func readLog(reader io.Reader) ([]byte, error) { - var messageLength uint16 - err := binary.Read(reader, binary.BigEndian, &messageLength) - if err != nil { - return nil, err - } - if messageLength == 0 { - return nil, nil - } - data := make([]byte, messageLength) - _, err = io.ReadFull(reader, data) - if err != nil { - return nil, err - } - return data, nil -} - -func writeLog(writer io.Writer, message []byte) error { +func writeLog(writer *bufio.Writer, messages []string) error { err := binary.Write(writer, binary.BigEndian, uint8(0)) if err != nil { return err } - err = binary.Write(writer, binary.BigEndian, uint16(len(message))) + err = binary.WriteData(writer, binary.BigEndian, messages) if err != nil { return err } - if len(message) > 0 { - _, err = writer.Write(message) - } - return err + return writer.Flush() } -func writeClearLog(writer io.Writer) error { - return binary.Write(writer, binary.BigEndian, uint8(1)) +func writeClearLog(writer *bufio.Writer) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + return writer.Flush() } func (s *CommandServer) handleLogConn(conn net.Conn) error { + var ( + interval int64 + timer *time.Timer + ) + err := binary.Read(conn, binary.BigEndian, &interval) + if err != nil { + return E.Cause(err, "read interval") + } + timer = time.NewTimer(time.Duration(interval)) + if !timer.Stop() { + <-timer.C + } var savedLines []string s.access.Lock() savedLines = make([]string, 0, s.savedLines.Len()) @@ -66,52 +66,67 @@ func (s *CommandServer) handleLogConn(conn net.Conn) error { return err } defer s.observer.UnSubscribe(subscription) - for _, line := range savedLines { - err = writeLog(conn, []byte(line)) + writer := bufio.NewWriter(conn) + if len(savedLines) > 0 { + err = writeLog(writer, savedLines) if err != nil { return err } } ctx := connKeepAlive(conn) + var logLines []string for { select { case <-ctx.Done(): return ctx.Err() - case message := <-subscription: - err = writeLog(conn, []byte(message)) - if err != nil { - return err - } case <-s.logReset: - err = writeClearLog(conn) + err = writeClearLog(writer) if err != nil { return err } case <-done: return nil + case logLine := <-subscription: + logLines = logLines[:0] + logLines = append(logLines, logLine) + timer.Reset(time.Duration(interval)) + loopLogs: + for { + select { + case logLine = <-subscription: + logLines = append(logLines, logLine) + case <-timer.C: + break loopLogs + } + } + err = writeLog(writer, logLines) + if err != nil { + return err + } } } } func (c *CommandClient) handleLogConn(conn net.Conn) { + reader := bufio.NewReader(conn) for { var messageType uint8 - err := binary.Read(conn, binary.BigEndian, &messageType) + err := binary.Read(reader, binary.BigEndian, &messageType) if err != nil { c.handler.Disconnected(err.Error()) return } - var message []byte + var messages []string switch messageType { case 0: - message, err = readLog(conn) + err = binary.ReadData(reader, binary.BigEndian, &messages) if err != nil { c.handler.Disconnected(err.Error()) return } - c.handler.WriteLog(string(message)) + c.handler.WriteLogs(newIterator(messages)) case 1: - c.handler.ClearLog() + c.handler.ClearLogs() } } } @@ -120,7 +135,7 @@ func connKeepAlive(reader io.Reader) context.Context { ctx, cancel := context.WithCancelCause(context.Background()) go func() { for { - _, err := readLog(reader) + _, err := reader.Read(make([]byte, 1)) if err != nil { cancel(err) return diff --git a/experimental/libbox/iterator.go b/experimental/libbox/iterator.go index 530a7e43bd..50d7385b36 100644 --- a/experimental/libbox/iterator.go +++ b/experimental/libbox/iterator.go @@ -3,8 +3,9 @@ package libbox import "github.com/sagernet/sing/common" type StringIterator interface { - Next() string + Len() int32 HasNext() bool + Next() string } var _ StringIterator = (*iterator[string])(nil) @@ -21,6 +22,14 @@ func newPtrIterator[T any](values []T) *iterator[*T] { return &iterator[*T]{common.Map(values, func(value T) *T { return &value })} } +func (i *iterator[T]) Len() int32 { + return int32(len(i.values)) +} + +func (i *iterator[T]) HasNext() bool { + return len(i.values) > 0 +} + func (i *iterator[T]) Next() T { if len(i.values) == 0 { return common.DefaultValue[T]() @@ -30,10 +39,6 @@ func (i *iterator[T]) Next() T { return nextValue } -func (i *iterator[T]) HasNext() bool { - return len(i.values) > 0 -} - type abstractIterator[T any] interface { Next() T HasNext() bool From 8d0d49c388cb017b3f67697c6cf843631cc85df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 25 Jun 2024 20:26:39 +0800 Subject: [PATCH 03/25] platform: Fix clash server reload on android --- experimental/clashapi/server.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 5ace3351e8..3428bbb771 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -7,7 +7,9 @@ import ( "net" "net/http" "os" + "runtime" "strings" + "syscall" "time" "github.com/sagernet/sing-box/adapter" @@ -143,7 +145,18 @@ func (s *Server) PreStart() error { func (s *Server) Start() error { if s.externalController { s.checkAndDownloadExternalUI() - listener, err := net.Listen("tcp", s.httpServer.Addr) + var ( + listener net.Listener + err error + ) + for i := 0; i < 3; i++ { + listener, err = net.Listen("tcp", s.httpServer.Addr) + if runtime.GOOS == "android" && errors.Is(err, syscall.EADDRINUSE) { + time.Sleep(100 * time.Millisecond) + continue + } + break + } if err != nil { return E.Cause(err, "external controller listen error") } From 388c75a815e97f57f61e0bd604b55b99c432e05c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 24 Jun 2024 09:49:15 +0800 Subject: [PATCH 04/25] WTF is this --- adapter/experimental.go | 17 +-- adapter/router.go | 2 +- box_outbound.go | 4 +- cmd/internal/build_libbox/main.go | 4 +- cmd/internal/build_shared/sdk.go | 10 +- cmd/sing-box/cmd_merge.go | 6 +- cmd/sing-box/cmd_run.go | 2 +- common/geosite/geosite_test.go | 34 +++++ common/geosite/reader.go | 88 +++++++---- common/geosite/writer.go | 21 ++- common/srs/binary.go | 113 +++++--------- common/srs/ip_set.go | 105 +++++-------- constant/path.go | 6 +- experimental/libbox/command_clash_mode.go | 18 +-- .../libbox/command_close_connection.go | 5 +- experimental/libbox/command_connections.go | 36 +++-- experimental/libbox/command_group.go | 140 +++--------------- experimental/libbox/command_log.go | 66 +++++---- experimental/libbox/command_power.go | 10 +- experimental/libbox/command_select.go | 10 +- experimental/libbox/command_server.go | 8 - experimental/libbox/command_shared.go | 6 +- experimental/libbox/command_urltest.go | 6 +- experimental/libbox/profile_import.go | 43 +++--- experimental/libbox/setup.go | 6 + go.mod | 2 +- go.sum | 4 +- inbound/mixed.go | 14 +- inbound/vless.go | 11 +- inbound/vmess.go | 11 +- option/outbound.go | 2 +- option/route.go | 2 +- outbound/proxy.go | 18 +-- outbound/tor.go | 12 +- route/router.go | 4 +- route/router_geo_resources.go | 8 +- route/rule_abstract.go | 19 ++- transport/trojan/mux.go | 27 +++- transport/trojan/service.go | 4 +- transport/v2raygrpc/conn.go | 4 +- transport/v2raygrpclite/conn.go | 8 +- 41 files changed, 415 insertions(+), 501 deletions(-) create mode 100644 common/geosite/geosite_test.go diff --git a/adapter/experimental.go b/adapter/experimental.go index 5e1cbd9d9d..0cab5ed5a8 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -4,14 +4,13 @@ import ( "bytes" "context" "encoding/binary" - "io" "net" "time" "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-dns" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) type ClashServer interface { @@ -56,16 +55,15 @@ func (s *SavedRuleSet) MarshalBinary() ([]byte, error) { if err != nil { return nil, err } - err = rw.WriteUVariant(&buffer, uint64(len(s.Content))) + err = varbin.Write(&buffer, binary.BigEndian, 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) + err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag) if err != nil { return nil, err } @@ -79,12 +77,7 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error { 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) + err = varbin.Read(reader, binary.BigEndian, &s.Content) if err != nil { return err } @@ -94,7 +87,7 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error { return err } s.LastUpdated = time.Unix(lastUpdated, 0) - s.LastEtag, err = rw.ReadVString(reader) + err = varbin.Read(reader, binary.BigEndian, &s.LastEtag) if err != nil { return err } diff --git a/adapter/router.go b/adapter/router.go index 54dc3396dc..c481f0c8c9 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -45,7 +45,7 @@ type Router interface { DefaultInterface() string AutoDetectInterface() bool AutoDetectInterfaceFunc() control.Func - DefaultMark() int + DefaultMark() uint32 NetworkMonitor() tun.NetworkUpdateMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor PackageManager() tun.PackageManager diff --git a/box_outbound.go b/box_outbound.go index 6e3f0617fc..f03f3b7d41 100644 --- a/box_outbound.go +++ b/box_outbound.go @@ -45,7 +45,9 @@ func (s *Box) startOutbounds() error { } started[outboundTag] = true canContinue = true - if starter, isStarter := outboundToStart.(common.Starter); isStarter { + if starter, isStarter := outboundToStart.(interface { + Start() error + }); isStarter { monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]") err := starter.Start() monitor.Finish() diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go index ae0fe34a6b..fc9308ff40 100644 --- a/cmd/internal/build_libbox/main.go +++ b/cmd/internal/build_libbox/main.go @@ -93,7 +93,7 @@ func buildAndroid() { const name = "libbox.aar" copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs") - if rw.FileExists(copyPath) { + if rw.IsDir(copyPath) { copyPath, _ = filepath.Abs(copyPath) err = rw.CopyFile(name, filepath.Join(copyPath, name)) if err != nil { @@ -134,7 +134,7 @@ func buildiOS() { } copyPath := filepath.Join("..", "sing-box-for-apple") - if rw.FileExists(copyPath) { + if rw.IsDir(copyPath) { targetDir := filepath.Join(copyPath, "Libbox.xcframework") targetDir, _ = filepath.Abs(targetDir) os.RemoveAll(targetDir) diff --git a/cmd/internal/build_shared/sdk.go b/cmd/internal/build_shared/sdk.go index ce7f0c86d0..b6c1ec9deb 100644 --- a/cmd/internal/build_shared/sdk.go +++ b/cmd/internal/build_shared/sdk.go @@ -30,7 +30,7 @@ func FindSDK() { } for _, path := range searchPath { path = os.ExpandEnv(path) - if rw.FileExists(filepath.Join(path, "licenses", "android-sdk-license")) { + if rw.IsFile(filepath.Join(path, "licenses", "android-sdk-license")) { androidSDKPath = path break } @@ -60,7 +60,7 @@ func FindSDK() { func findNDK() bool { const fixedVersion = "26.2.11394342" const versionFile = "source.properties" - if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.FileExists(filepath.Join(fixedPath, versionFile)) { + if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) { androidNDKPath = fixedPath return true } @@ -86,7 +86,7 @@ func findNDK() bool { }) for _, versionName := range versionNames { currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName) - if rw.FileExists(filepath.Join(androidSDKPath, versionFile)) { + if rw.IsFile(filepath.Join(androidSDKPath, versionFile)) { androidNDKPath = currentNDKPath log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion) return true @@ -100,11 +100,11 @@ var GoBinPath string func FindMobile() { goBin := filepath.Join(build.Default.GOPATH, "bin") if runtime.GOOS == "windows" { - if !rw.FileExists(filepath.Join(goBin, "gobind.exe")) { + if !rw.IsFile(filepath.Join(goBin, "gobind.exe")) { log.Fatal("missing gomobile installation") } } else { - if !rw.FileExists(filepath.Join(goBin, "gobind")) { + if !rw.IsFile(filepath.Join(goBin, "gobind")) { log.Fatal("missing gomobile installation") } } diff --git a/cmd/sing-box/cmd_merge.go b/cmd/sing-box/cmd_merge.go index 1d19ff17e1..10dd38a1db 100644 --- a/cmd/sing-box/cmd_merge.go +++ b/cmd/sing-box/cmd_merge.go @@ -54,7 +54,11 @@ func merge(outputPath string) error { return nil } } - err = rw.WriteFile(outputPath, buffer.Bytes()) + err = rw.MkdirParent(outputPath) + if err != nil { + return err + } + err = os.WriteFile(outputPath, buffer.Bytes(), 0o644) if err != nil { return err } diff --git a/cmd/sing-box/cmd_run.go b/cmd/sing-box/cmd_run.go index 3c4dd0d910..e717c5945d 100644 --- a/cmd/sing-box/cmd_run.go +++ b/cmd/sing-box/cmd_run.go @@ -109,7 +109,7 @@ func readConfigAndMerge() (option.Options, error) { } var mergedMessage json.RawMessage for _, options := range optionsList { - mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage) + mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage, false) if err != nil { return option.Options{}, E.Cause(err, "merge config at ", options.path) } diff --git a/common/geosite/geosite_test.go b/common/geosite/geosite_test.go new file mode 100644 index 0000000000..bdcb7a7afd --- /dev/null +++ b/common/geosite/geosite_test.go @@ -0,0 +1,34 @@ +package geosite_test + +import ( + "bytes" + "testing" + + "github.com/sagernet/sing-box/common/geosite" + + "github.com/stretchr/testify/require" +) + +func TestGeosite(t *testing.T) { + t.Parallel() + + var buffer bytes.Buffer + err := geosite.Write(&buffer, map[string][]geosite.Item{ + "test": { + { + Type: geosite.RuleTypeDomain, + Value: "example.org", + }, + }, + }) + require.NoError(t, err) + reader, codes, err := geosite.NewReader(bytes.NewReader(buffer.Bytes())) + require.NoError(t, err) + require.Equal(t, []string{"test"}, codes) + items, err := reader.Read("test") + require.NoError(t, err) + require.Equal(t, []geosite.Item{{ + Type: geosite.RuleTypeDomain, + Value: "example.org", + }}, items) +} diff --git a/common/geosite/reader.go b/common/geosite/reader.go index a1b39f28b4..3b3f7fec29 100644 --- a/common/geosite/reader.go +++ b/common/geosite/reader.go @@ -1,17 +1,24 @@ package geosite import ( + "bufio" + "encoding/binary" "io" "os" + "sync" + "sync/atomic" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) type Reader struct { - reader io.ReadSeeker - domainIndex map[string]int - domainLength map[string]int + access sync.Mutex + reader io.ReadSeeker + bufferedReader *bufio.Reader + metadataIndex int64 + domainIndex map[string]int + domainLength map[string]int } func Open(path string) (*Reader, []string, error) { @@ -19,12 +26,20 @@ func Open(path string) (*Reader, []string, error) { if err != nil { return nil, nil, err } + reader, codes, err := NewReader(content) + if err != nil { + content.Close() + return nil, nil, err + } + return reader, codes, nil +} + +func NewReader(readSeeker io.ReadSeeker) (*Reader, []string, error) { reader := &Reader{ - reader: content, + reader: readSeeker, } - err = reader.readMetadata() + err := reader.readMetadata() if err != nil { - content.Close() return nil, nil, err } codes := make([]string, 0, len(reader.domainIndex)) @@ -34,15 +49,23 @@ func Open(path string) (*Reader, []string, error) { return reader, codes, nil } +type geositeMetadata struct { + Code string + Index uint64 + Length uint64 +} + func (r *Reader) readMetadata() error { - version, err := rw.ReadByte(r.reader) + counter := &readCounter{Reader: r.reader} + reader := bufio.NewReader(counter) + version, err := reader.ReadByte() if err != nil { return err } if version != 0 { return E.New("unknown version") } - entryLength, err := rw.ReadUVariant(r.reader) + entryLength, err := binary.ReadUvarint(reader) if err != nil { return err } @@ -55,16 +78,16 @@ func (r *Reader) readMetadata() error { codeIndex uint64 codeLength uint64 ) - code, err = rw.ReadVString(r.reader) + code, err = varbin.ReadValue[string](reader, binary.BigEndian) if err != nil { return err } keys[i] = code - codeIndex, err = rw.ReadUVariant(r.reader) + codeIndex, err = binary.ReadUvarint(reader) if err != nil { return err } - codeLength, err = rw.ReadUVariant(r.reader) + codeLength, err = binary.ReadUvarint(reader) if err != nil { return err } @@ -73,6 +96,8 @@ func (r *Reader) readMetadata() error { } r.domainIndex = domainIndex r.domainLength = domainLength + r.metadataIndex = counter.count - int64(reader.Buffered()) + r.bufferedReader = reader return nil } @@ -81,31 +106,32 @@ func (r *Reader) Read(code string) ([]Item, error) { if !exists { return nil, E.New("code ", code, " not exists!") } - _, err := r.reader.Seek(int64(index), io.SeekCurrent) + _, err := r.reader.Seek(r.metadataIndex+int64(index), io.SeekStart) if err != nil { return nil, err } - counter := &rw.ReadCounter{Reader: r.reader} - domain := make([]Item, r.domainLength[code]) - for i := range domain { - var ( - item Item - err error - ) - item.Type, err = rw.ReadByte(counter) - if err != nil { - return nil, err - } - item.Value, err = rw.ReadVString(counter) - if err != nil { - return nil, err - } - domain[i] = item + r.bufferedReader.Reset(r.reader) + itemList := make([]Item, r.domainLength[code]) + err = varbin.Read(r.bufferedReader, binary.BigEndian, &itemList) + if err != nil { + return nil, err } - _, err = r.reader.Seek(int64(-index)-counter.Count(), io.SeekCurrent) - return domain, err + return itemList, nil } func (r *Reader) Upstream() any { return r.reader } + +type readCounter struct { + io.Reader + count int64 +} + +func (r *readCounter) Read(p []byte) (n int, err error) { + n, err = r.Reader.Read(p) + if n > 0 { + atomic.AddInt64(&r.count, int64(n)) + } + return +} diff --git a/common/geosite/writer.go b/common/geosite/writer.go index 4e7ec514b1..1615fa3480 100644 --- a/common/geosite/writer.go +++ b/common/geosite/writer.go @@ -2,13 +2,13 @@ package geosite import ( "bytes" - "io" + "encoding/binary" "sort" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) -func Write(writer io.Writer, domains map[string][]Item) error { +func Write(writer varbin.Writer, domains map[string][]Item) error { keys := make([]string, 0, len(domains)) for code := range domains { keys = append(keys, code) @@ -19,35 +19,34 @@ func Write(writer io.Writer, domains map[string][]Item) error { index := make(map[string]int) for _, code := range keys { index[code] = content.Len() - for _, domain := range domains[code] { - content.WriteByte(domain.Type) - err := rw.WriteVString(content, domain.Value) + for _, item := range domains[code] { + err := varbin.Write(content, binary.BigEndian, item) if err != nil { return err } } } - err := rw.WriteByte(writer, 0) + err := writer.WriteByte(0) if err != nil { return err } - err = rw.WriteUVariant(writer, uint64(len(keys))) + _, err = varbin.WriteUvarint(writer, uint64(len(keys))) if err != nil { return err } for _, code := range keys { - err = rw.WriteVString(writer, code) + err = varbin.Write(writer, binary.BigEndian, code) if err != nil { return err } - err = rw.WriteUVariant(writer, uint64(index[code])) + _, err = varbin.WriteUvarint(writer, uint64(index[code])) if err != nil { return err } - err = rw.WriteUVariant(writer, uint64(len(domains[code]))) + _, err = varbin.WriteUvarint(writer, uint64(len(domains[code]))) if err != nil { return err } diff --git a/common/srs/binary.go b/common/srs/binary.go index faf4cd17b2..c7c55e0838 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -1,6 +1,7 @@ package srs import ( + "bufio" "compress/zlib" "encoding/binary" "io" @@ -11,7 +12,7 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/domain" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" "go4.org/netipx" ) @@ -38,7 +39,7 @@ const ( ruleItemFinal uint8 = 0xFF ) -func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) { +func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err error) { var magicBytes [3]byte _, err = io.ReadFull(reader, magicBytes[:]) if err != nil { @@ -60,13 +61,14 @@ func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err err if err != nil { return } - length, err := rw.ReadUVariant(zReader) + bReader := bufio.NewReader(zReader) + length, err := binary.ReadUvarint(bReader) if err != nil { return } ruleSet.Rules = make([]option.HeadlessRule, length) for i := uint64(0); i < length; i++ { - ruleSet.Rules[i], err = readRule(zReader, recovery) + ruleSet.Rules[i], err = readRule(bReader, recover) if err != nil { err = E.Cause(err, "read rule[", i, "]") return @@ -88,20 +90,25 @@ func Write(writer io.Writer, ruleSet option.PlainRuleSet) error { if err != nil { return err } - err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules))) + bWriter := bufio.NewWriter(zWriter) + _, err = varbin.WriteUvarint(bWriter, uint64(len(ruleSet.Rules))) if err != nil { return err } for _, rule := range ruleSet.Rules { - err = writeRule(zWriter, rule) + err = writeRule(bWriter, rule) if err != nil { return err } } + err = bWriter.Flush() + if err != nil { + return err + } return zWriter.Close() } -func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) { +func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err error) { var ruleType uint8 err = binary.Read(reader, binary.BigEndian, &ruleType) if err != nil { @@ -110,17 +117,17 @@ func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err er switch ruleType { case 0: rule.Type = C.RuleTypeDefault - rule.DefaultOptions, err = readDefaultRule(reader, recovery) + rule.DefaultOptions, err = readDefaultRule(reader, recover) case 1: rule.Type = C.RuleTypeLogical - rule.LogicalOptions, err = readLogicalRule(reader, recovery) + rule.LogicalOptions, err = readLogicalRule(reader, recover) default: err = E.New("unknown rule type: ", ruleType) } return } -func writeRule(writer io.Writer, rule option.HeadlessRule) error { +func writeRule(writer varbin.Writer, rule option.HeadlessRule) error { switch rule.Type { case C.RuleTypeDefault: return writeDefaultRule(writer, rule.DefaultOptions) @@ -131,7 +138,7 @@ func writeRule(writer io.Writer, rule option.HeadlessRule) error { } } -func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) { +func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHeadlessRule, err error) { var lastItemType uint8 for { var itemType uint8 @@ -158,6 +165,9 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle return } rule.DomainMatcher = matcher + if recover { + rule.Domain, rule.DomainSuffix = matcher.Dump() + } case ruleItemDomainKeyword: rule.DomainKeyword, err = readRuleItemString(reader) case ruleItemDomainRegex: @@ -167,7 +177,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle if err != nil { return } - if recovery { + if recover { rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String) } case ruleItemIPCIDR: @@ -175,7 +185,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle if err != nil { return } - if recovery { + if recover { rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String) } case ruleItemSourcePort: @@ -209,7 +219,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle } } -func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error { +func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule) error { err := binary.Write(writer, binary.BigEndian, uint8(0)) if err != nil { return err @@ -327,73 +337,31 @@ func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error { 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 readRuleItemString(reader varbin.Reader) ([]string, error) { + return varbin.ReadValue[[]string](reader, binary.BigEndian) } -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))) +func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error { + err := writer.WriteByte(itemType) if err != nil { return err } - for _, item := range value { - err = rw.WriteVString(writer, item) - if err != nil { - return err - } - } - return nil + return varbin.Write(writer, binary.BigEndian, value) } -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 readRuleItemUint16(reader varbin.Reader) ([]uint16, error) { + return varbin.ReadValue[[]uint16](reader, binary.BigEndian) } -func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error { - err := binary.Write(writer, binary.BigEndian, itemType) +func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) error { + err := writer.WriteByte(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 + return varbin.Write(writer, binary.BigEndian, value) } -func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error { +func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) error { var builder netipx.IPSetBuilder for i, prefixString := range value { prefix, err := netip.ParsePrefix(prefixString) @@ -419,9 +387,8 @@ func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error { 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) +func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { + mode, err := reader.ReadByte() if err != nil { return } @@ -434,7 +401,7 @@ func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.Logica err = E.New("unknown logical mode: ", mode) return } - length, err := rw.ReadUVariant(reader) + length, err := binary.ReadUvarint(reader) if err != nil { return } @@ -453,7 +420,7 @@ func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.Logica return } -func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error { +func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule) error { err := binary.Write(writer, binary.BigEndian, uint8(1)) if err != nil { return err @@ -469,7 +436,7 @@ func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) if err != nil { return err } - err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules))) + _, err = varbin.WriteUvarint(writer, uint64(len(logicalRule.Rules))) if err != nil { return err } diff --git a/common/srs/ip_set.go b/common/srs/ip_set.go index b346da26f6..044dc823b3 100644 --- a/common/srs/ip_set.go +++ b/common/srs/ip_set.go @@ -2,11 +2,13 @@ package srs import ( "encoding/binary" - "io" "net/netip" + "os" "unsafe" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/varbin" "go4.org/netipx" ) @@ -20,94 +22,57 @@ type myIPRange struct { to netip.Addr } -func readIPSet(reader io.Reader) (*netipx.IPSet, error) { - var version uint8 - err := binary.Read(reader, binary.BigEndian, &version) +type myIPRangeData struct { + From []byte + To []byte +} + +func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) { + version, err := reader.ReadByte() if err != nil { return nil, err } + if version != 1 { + return nil, os.ErrInvalid + } + // WTF why using uint64 here var length uint64 err = binary.Read(reader, binary.BigEndian, &length) if err != nil { return nil, err } + ranges := make([]myIPRangeData, length) + err = varbin.Read(reader, binary.BigEndian, &ranges) + if err != nil { + return nil, err + } mySet := &myIPSet{ - rr: make([]myIPRange, length), + rr: make([]myIPRange, len(ranges)), } - 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} + for i, rangeData := range ranges { + mySet.rr[i].from = M.AddrFromIP(rangeData.From) + mySet.rr[i].to = M.AddrFromIP(rangeData.To) } return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil } -func writeIPSet(writer io.Writer, set *netipx.IPSet) error { - err := binary.Write(writer, binary.BigEndian, uint8(1)) +func writeIPSet(writer varbin.Writer, set *netipx.IPSet) error { + err := writer.WriteByte(1) if err != nil { return err } - mySet := (*myIPSet)(unsafe.Pointer(set)) - err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr))) + dataList := common.Map((*myIPSet)(unsafe.Pointer(set)).rr, func(rr myIPRange) myIPRangeData { + return myIPRangeData{ + From: rr.from.AsSlice(), + To: rr.to.AsSlice(), + } + }) + err = binary.Write(writer, binary.BigEndian, uint64(len(dataList))) 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) + for _, data := range dataList { + err = varbin.Write(writer, binary.BigEndian, data) if err != nil { return err } diff --git a/constant/path.go b/constant/path.go index 98acacdc37..ea2aad3e23 100644 --- a/constant/path.go +++ b/constant/path.go @@ -13,14 +13,14 @@ var resourcePaths []string func FindPath(name string) (string, bool) { name = os.ExpandEnv(name) - if rw.FileExists(name) { + if rw.IsFile(name) { return name, true } for _, dir := range resourcePaths { - if path := filepath.Join(dir, dirName, name); rw.FileExists(path) { + if path := filepath.Join(dir, dirName, name); rw.IsFile(path) { return path, true } - if path := filepath.Join(dir, name); rw.FileExists(path) { + if path := filepath.Join(dir, name); rw.IsFile(path) { return path, true } } diff --git a/experimental/libbox/command_clash_mode.go b/experimental/libbox/command_clash_mode.go index 3377ae3afd..1b6eb47085 100644 --- a/experimental/libbox/command_clash_mode.go +++ b/experimental/libbox/command_clash_mode.go @@ -9,7 +9,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/experimental/clashapi" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) func (c *CommandClient) SetClashMode(newMode string) error { @@ -22,7 +22,7 @@ func (c *CommandClient) SetClashMode(newMode string) error { if err != nil { return err } - err = rw.WriteVString(conn, newMode) + err = varbin.Write(conn, binary.BigEndian, newMode) if err != nil { return err } @@ -30,7 +30,7 @@ func (c *CommandClient) SetClashMode(newMode string) error { } func (s *CommandServer) handleSetClashMode(conn net.Conn) error { - newMode, err := rw.ReadVString(conn) + newMode, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { return err } @@ -50,7 +50,7 @@ func (c *CommandClient) handleModeConn(conn net.Conn) { defer conn.Close() for { - newMode, err := rw.ReadVString(conn) + newMode, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { c.handler.Disconnected(err.Error()) return @@ -80,7 +80,7 @@ func (s *CommandServer) handleModeConn(conn net.Conn) error { for { select { case <-s.modeUpdate: - err = rw.WriteVString(conn, clashServer.Mode()) + err = varbin.Write(conn, binary.BigEndian, clashServer.Mode()) if err != nil { return err } @@ -101,12 +101,12 @@ func readClashModeList(reader io.Reader) (modeList []string, currentMode string, } modeList = make([]string, modeListLength) for i := 0; i < int(modeListLength); i++ { - modeList[i], err = rw.ReadVString(reader) + modeList[i], err = varbin.ReadValue[string](reader, binary.BigEndian) if err != nil { return } } - currentMode, err = rw.ReadVString(reader) + currentMode, err = varbin.ReadValue[string](reader, binary.BigEndian) return } @@ -118,12 +118,12 @@ func writeClashModeList(writer io.Writer, clashServer adapter.ClashServer) error } if len(modeList) > 0 { for _, mode := range modeList { - err = rw.WriteVString(writer, mode) + err = varbin.Write(writer, binary.BigEndian, mode) if err != nil { return err } } - err = rw.WriteVString(writer, clashServer.Mode()) + err = varbin.Write(writer, binary.BigEndian, clashServer.Mode()) if err != nil { return err } diff --git a/experimental/libbox/command_close_connection.go b/experimental/libbox/command_close_connection.go index 62f5dc8419..1edd591122 100644 --- a/experimental/libbox/command_close_connection.go +++ b/experimental/libbox/command_close_connection.go @@ -7,6 +7,7 @@ import ( "github.com/sagernet/sing-box/experimental/clashapi" "github.com/sagernet/sing/common/binary" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/varbin" "github.com/gofrs/uuid/v5" ) @@ -18,7 +19,7 @@ func (c *CommandClient) CloseConnection(connId string) error { } defer conn.Close() writer := bufio.NewWriter(conn) - err = binary.WriteData(writer, binary.BigEndian, connId) + err = varbin.Write(writer, binary.BigEndian, connId) if err != nil { return err } @@ -32,7 +33,7 @@ func (c *CommandClient) CloseConnection(connId string) error { func (s *CommandServer) handleCloseConnection(conn net.Conn) error { reader := bufio.NewReader(conn) var connId string - err := binary.ReadData(reader, binary.BigEndian, &connId) + err := varbin.Read(reader, binary.BigEndian, &connId) if err != nil { return E.Cause(err, "read connection id") } diff --git a/experimental/libbox/command_connections.go b/experimental/libbox/command_connections.go index 44d35bbedc..de90c95259 100644 --- a/experimental/libbox/command_connections.go +++ b/experimental/libbox/command_connections.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing/common/binary" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/varbin" "github.com/gofrs/uuid/v5" ) @@ -19,13 +20,17 @@ import ( func (c *CommandClient) handleConnectionsConn(conn net.Conn) { defer conn.Close() reader := bufio.NewReader(conn) - var connections Connections + var ( + rawConnections []Connection + connections Connections + ) for { - err := binary.ReadData(reader, binary.BigEndian, &connections.connections) + err := varbin.Read(reader, binary.BigEndian, &rawConnections) if err != nil { c.handler.Disconnected(err.Error()) return } + connections.input = rawConnections c.handler.WriteConnections(&connections) } } @@ -69,7 +74,7 @@ func (s *CommandServer) handleConnectionsConn(conn net.Conn) error { for _, connection := range trafficManager.ClosedConnections() { outConnections = append(outConnections, newConnection(connections, connection, true)) } - err = binary.WriteData(writer, binary.BigEndian, outConnections) + err = varbin.Write(writer, binary.BigEndian, outConnections) if err != nil { return err } @@ -92,33 +97,32 @@ const ( ) type Connections struct { - connections []Connection - filteredConnections []Connection - outConnections *[]Connection + input []Connection + filtered []Connection } func (c *Connections) FilterState(state int32) { - c.filteredConnections = c.filteredConnections[:0] + c.filtered = c.filtered[:0] switch state { case ConnectionStateAll: - c.filteredConnections = append(c.filteredConnections, c.connections...) + c.filtered = append(c.filtered, c.input...) case ConnectionStateActive: - for _, connection := range c.connections { + for _, connection := range c.input { if connection.ClosedAt == 0 { - c.filteredConnections = append(c.filteredConnections, connection) + c.filtered = append(c.filtered, connection) } } case ConnectionStateClosed: - for _, connection := range c.connections { + for _, connection := range c.input { if connection.ClosedAt != 0 { - c.filteredConnections = append(c.filteredConnections, connection) + c.filtered = append(c.filtered, connection) } } } } func (c *Connections) SortByDate() { - slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int { + slices.SortStableFunc(c.filtered, func(x, y Connection) int { if x.CreatedAt < y.CreatedAt { return 1 } else if x.CreatedAt > y.CreatedAt { @@ -130,7 +134,7 @@ func (c *Connections) SortByDate() { } func (c *Connections) SortByTraffic() { - slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int { + slices.SortStableFunc(c.filtered, func(x, y Connection) int { xTraffic := x.Uplink + x.Downlink yTraffic := y.Uplink + y.Downlink if xTraffic < yTraffic { @@ -144,7 +148,7 @@ func (c *Connections) SortByTraffic() { } func (c *Connections) SortByTrafficTotal() { - slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int { + slices.SortStableFunc(c.filtered, func(x, y Connection) int { xTraffic := x.UplinkTotal + x.DownlinkTotal yTraffic := y.UplinkTotal + y.DownlinkTotal if xTraffic < yTraffic { @@ -158,7 +162,7 @@ func (c *Connections) SortByTrafficTotal() { } func (c *Connections) Iterator() ConnectionIterator { - return newPtrIterator(c.filteredConnections) + return newPtrIterator(c.filtered) } type Connection struct { diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go index a5572ea1f5..3a8d2a0749 100644 --- a/experimental/libbox/command_group.go +++ b/experimental/libbox/command_group.go @@ -1,6 +1,7 @@ package libbox import ( + "bufio" "encoding/binary" "io" "net" @@ -10,7 +11,7 @@ import ( "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/outbound" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/service" ) @@ -36,19 +37,24 @@ func (s *CommandServer) handleGroupConn(conn net.Conn) error { ticker := time.NewTicker(time.Duration(interval)) defer ticker.Stop() ctx := connKeepAlive(conn) + writer := bufio.NewWriter(conn) for { service := s.service if service != nil { - err := writeGroups(conn, service) + err = writeGroups(writer, service) if err != nil { return err } } else { - err := binary.Write(conn, binary.BigEndian, uint16(0)) + err = binary.Write(writer, binary.BigEndian, uint16(0)) if err != nil { return err } } + err = writer.Flush() + if err != nil { + return err + } select { case <-ctx.Done(): return ctx.Err() @@ -68,11 +74,11 @@ type OutboundGroup struct { Selectable bool Selected string IsExpand bool - items []*OutboundGroupItem + ItemList []*OutboundGroupItem } func (g *OutboundGroup) GetItems() OutboundGroupItemIterator { - return newIterator(g.items) + return newIterator(g.ItemList) } type OutboundGroupIterator interface { @@ -93,73 +99,10 @@ type OutboundGroupItemIterator interface { } func readGroups(reader io.Reader) (OutboundGroupIterator, error) { - var groupLength uint16 - err := binary.Read(reader, binary.BigEndian, &groupLength) + groups, err := varbin.ReadValue[[]*OutboundGroup](reader, binary.BigEndian) if err != nil { return nil, err } - - groups := make([]*OutboundGroup, 0, groupLength) - for i := 0; i < int(groupLength); i++ { - var group OutboundGroup - group.Tag, err = rw.ReadVString(reader) - if err != nil { - return nil, err - } - - group.Type, err = rw.ReadVString(reader) - if err != nil { - return nil, err - } - - err = binary.Read(reader, binary.BigEndian, &group.Selectable) - if err != nil { - return nil, err - } - - group.Selected, err = rw.ReadVString(reader) - if err != nil { - return nil, err - } - - err = binary.Read(reader, binary.BigEndian, &group.IsExpand) - if err != nil { - return nil, err - } - - var itemLength uint16 - err = binary.Read(reader, binary.BigEndian, &itemLength) - if err != nil { - return nil, err - } - - group.items = make([]*OutboundGroupItem, itemLength) - for j := 0; j < int(itemLength); j++ { - var item OutboundGroupItem - item.Tag, err = rw.ReadVString(reader) - if err != nil { - return nil, err - } - - item.Type, err = rw.ReadVString(reader) - if err != nil { - return nil, err - } - - err = binary.Read(reader, binary.BigEndian, &item.URLTestTime) - if err != nil { - return nil, err - } - - err = binary.Read(reader, binary.BigEndian, &item.URLTestDelay) - if err != nil { - return nil, err - } - - group.items[j] = &item - } - groups = append(groups, &group) - } return newIterator(groups), nil } @@ -199,63 +142,14 @@ func writeGroups(writer io.Writer, boxService *BoxService) error { item.URLTestTime = history.Time.Unix() item.URLTestDelay = int32(history.Delay) } - group.items = append(group.items, &item) + group.ItemList = append(group.ItemList, &item) } - if len(group.items) < 2 { + if len(group.ItemList) < 2 { continue } groups = append(groups, group) } - - err := binary.Write(writer, binary.BigEndian, uint16(len(groups))) - if err != nil { - return err - } - for _, group := range groups { - err = rw.WriteVString(writer, group.Tag) - if err != nil { - return err - } - err = rw.WriteVString(writer, group.Type) - if err != nil { - return err - } - err = binary.Write(writer, binary.BigEndian, group.Selectable) - if err != nil { - return err - } - err = rw.WriteVString(writer, group.Selected) - if err != nil { - return err - } - err = binary.Write(writer, binary.BigEndian, group.IsExpand) - if err != nil { - return err - } - err = binary.Write(writer, binary.BigEndian, uint16(len(group.items))) - if err != nil { - return err - } - for _, item := range group.items { - err = rw.WriteVString(writer, item.Tag) - if err != nil { - return err - } - err = rw.WriteVString(writer, item.Type) - if err != nil { - return err - } - err = binary.Write(writer, binary.BigEndian, item.URLTestTime) - if err != nil { - return err - } - err = binary.Write(writer, binary.BigEndian, item.URLTestDelay) - if err != nil { - return err - } - } - } - return nil + return varbin.Write(writer, binary.BigEndian, groups) } func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error { @@ -268,7 +162,7 @@ func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error { if err != nil { return err } - err = rw.WriteVString(conn, groupTag) + err = varbin.Write(conn, binary.BigEndian, groupTag) if err != nil { return err } @@ -280,7 +174,7 @@ func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error { } func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error { - groupTag, err := rw.ReadVString(conn) + groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { return err } diff --git a/experimental/libbox/command_log.go b/experimental/libbox/command_log.go index 8a22aa2e74..07f6e83919 100644 --- a/experimental/libbox/command_log.go +++ b/experimental/libbox/command_log.go @@ -9,8 +9,19 @@ import ( "github.com/sagernet/sing/common/binary" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/varbin" ) +func (s *CommandServer) ResetLog() { + s.access.Lock() + defer s.access.Unlock() + s.savedLines.Init() + select { + case s.logReset <- struct{}{}: + default: + } +} + func (s *CommandServer) WriteMessage(message string) { s.subscriber.Emit(message) s.access.Lock() @@ -21,26 +32,6 @@ func (s *CommandServer) WriteMessage(message string) { s.access.Unlock() } -func writeLog(writer *bufio.Writer, messages []string) error { - err := binary.Write(writer, binary.BigEndian, uint8(0)) - if err != nil { - return err - } - err = binary.WriteData(writer, binary.BigEndian, messages) - if err != nil { - return err - } - return writer.Flush() -} - -func writeClearLog(writer *bufio.Writer) error { - err := binary.Write(writer, binary.BigEndian, uint8(1)) - if err != nil { - return err - } - return writer.Flush() -} - func (s *CommandServer) handleLogConn(conn net.Conn) error { var ( interval int64 @@ -67,8 +58,24 @@ func (s *CommandServer) handleLogConn(conn net.Conn) error { } defer s.observer.UnSubscribe(subscription) writer := bufio.NewWriter(conn) + select { + case <-s.logReset: + err = writer.WriteByte(1) + if err != nil { + return err + } + err = writer.Flush() + if err != nil { + return err + } + default: + } if len(savedLines) > 0 { - err = writeLog(writer, savedLines) + err = writer.WriteByte(0) + if err != nil { + return err + } + err = varbin.Write(writer, binary.BigEndian, savedLines) if err != nil { return err } @@ -76,11 +83,15 @@ func (s *CommandServer) handleLogConn(conn net.Conn) error { ctx := connKeepAlive(conn) var logLines []string for { + err = writer.Flush() + if err != nil { + return err + } select { case <-ctx.Done(): return ctx.Err() case <-s.logReset: - err = writeClearLog(writer) + err = writer.WriteByte(1) if err != nil { return err } @@ -99,7 +110,11 @@ func (s *CommandServer) handleLogConn(conn net.Conn) error { break loopLogs } } - err = writeLog(writer, logLines) + err = writer.WriteByte(0) + if err != nil { + return err + } + err = varbin.Write(writer, binary.BigEndian, logLines) if err != nil { return err } @@ -110,8 +125,7 @@ func (s *CommandServer) handleLogConn(conn net.Conn) error { func (c *CommandClient) handleLogConn(conn net.Conn) { reader := bufio.NewReader(conn) for { - var messageType uint8 - err := binary.Read(reader, binary.BigEndian, &messageType) + messageType, err := reader.ReadByte() if err != nil { c.handler.Disconnected(err.Error()) return @@ -119,7 +133,7 @@ func (c *CommandClient) handleLogConn(conn net.Conn) { var messages []string switch messageType { case 0: - err = binary.ReadData(reader, binary.BigEndian, &messages) + err = varbin.Read(reader, binary.BigEndian, &messages) if err != nil { c.handler.Disconnected(err.Error()) return diff --git a/experimental/libbox/command_power.go b/experimental/libbox/command_power.go index 619cb57b50..5ed7b01428 100644 --- a/experimental/libbox/command_power.go +++ b/experimental/libbox/command_power.go @@ -5,7 +5,7 @@ import ( "net" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) func (c *CommandClient) ServiceReload() error { @@ -24,7 +24,7 @@ func (c *CommandClient) ServiceReload() error { return err } if hasError { - errorMessage, err := rw.ReadVString(conn) + errorMessage, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { return err } @@ -40,7 +40,7 @@ func (s *CommandServer) handleServiceReload(conn net.Conn) error { return err } if rErr != nil { - return rw.WriteVString(conn, rErr.Error()) + return varbin.Write(conn, binary.BigEndian, rErr.Error()) } return nil } @@ -61,7 +61,7 @@ func (c *CommandClient) ServiceClose() error { return nil } if hasError { - errorMessage, err := rw.ReadVString(conn) + errorMessage, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { return nil } @@ -78,7 +78,7 @@ func (s *CommandServer) handleServiceClose(conn net.Conn) error { return err } if rErr != nil { - return rw.WriteVString(conn, rErr.Error()) + return varbin.Write(conn, binary.BigEndian, rErr.Error()) } return nil } diff --git a/experimental/libbox/command_select.go b/experimental/libbox/command_select.go index e7d5b08f86..e1e67e6068 100644 --- a/experimental/libbox/command_select.go +++ b/experimental/libbox/command_select.go @@ -6,7 +6,7 @@ import ( "github.com/sagernet/sing-box/outbound" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error { @@ -19,11 +19,11 @@ func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) erro if err != nil { return err } - err = rw.WriteVString(conn, groupTag) + err = varbin.Write(conn, binary.BigEndian, groupTag) if err != nil { return err } - err = rw.WriteVString(conn, outboundTag) + err = varbin.Write(conn, binary.BigEndian, outboundTag) if err != nil { return err } @@ -31,11 +31,11 @@ func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) erro } func (s *CommandServer) handleSelectOutbound(conn net.Conn) error { - groupTag, err := rw.ReadVString(conn) + groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { return err } - outboundTag, err := rw.ReadVString(conn) + outboundTag, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { return err } diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go index 8918756dae..f913191d6a 100644 --- a/experimental/libbox/command_server.go +++ b/experimental/libbox/command_server.go @@ -66,14 +66,6 @@ func (s *CommandServer) SetService(newService *BoxService) { s.notifyURLTestUpdate() } -func (s *CommandServer) ResetLog() { - s.savedLines.Init() - select { - case s.logReset <- struct{}{}: - default: - } -} - func (s *CommandServer) notifyURLTestUpdate() { select { case s.urlTestUpdate <- struct{}{}: diff --git a/experimental/libbox/command_shared.go b/experimental/libbox/command_shared.go index ecad78dd8a..b98c2e5d3c 100644 --- a/experimental/libbox/command_shared.go +++ b/experimental/libbox/command_shared.go @@ -5,7 +5,7 @@ import ( "io" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) func readError(reader io.Reader) error { @@ -15,7 +15,7 @@ func readError(reader io.Reader) error { return err } if hasError { - errorMessage, err := rw.ReadVString(reader) + errorMessage, err := varbin.ReadValue[string](reader, binary.BigEndian) if err != nil { return err } @@ -30,7 +30,7 @@ func writeError(writer io.Writer, wErr error) error { return err } if wErr != nil { - err = rw.WriteVString(writer, wErr.Error()) + err = varbin.Write(writer, binary.BigEndian, wErr.Error()) if err != nil { return err } diff --git a/experimental/libbox/command_urltest.go b/experimental/libbox/command_urltest.go index 19ddf3da3c..6feda3f8ef 100644 --- a/experimental/libbox/command_urltest.go +++ b/experimental/libbox/command_urltest.go @@ -11,7 +11,7 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/service" ) @@ -25,7 +25,7 @@ func (c *CommandClient) URLTest(groupTag string) error { if err != nil { return err } - err = rw.WriteVString(conn, groupTag) + err = varbin.Write(conn, binary.BigEndian, groupTag) if err != nil { return err } @@ -33,7 +33,7 @@ func (c *CommandClient) URLTest(groupTag string) error { } func (s *CommandServer) handleURLTest(conn net.Conn) error { - groupTag, err := rw.ReadVString(conn) + groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian) if err != nil { return err } diff --git a/experimental/libbox/profile_import.go b/experimental/libbox/profile_import.go index 75ddb06dcf..258c175a9f 100644 --- a/experimental/libbox/profile_import.go +++ b/experimental/libbox/profile_import.go @@ -1,13 +1,13 @@ package libbox import ( + "bufio" "bytes" "compress/gzip" "encoding/binary" - "io" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) func EncodeChunkedMessage(data []byte) []byte { @@ -35,13 +35,13 @@ type ErrorMessage struct { func (e *ErrorMessage) Encode() []byte { var buffer bytes.Buffer buffer.WriteByte(MessageTypeError) - rw.WriteVString(&buffer, e.Message) + varbin.Write(&buffer, binary.BigEndian, e.Message) return buffer.Bytes() } func DecodeErrorMessage(data []byte) (*ErrorMessage, error) { reader := bytes.NewReader(data) - messageType, err := rw.ReadByte(reader) + messageType, err := reader.ReadByte() if err != nil { return nil, err } @@ -49,7 +49,7 @@ func DecodeErrorMessage(data []byte) (*ErrorMessage, error) { return nil, E.New("invalid message") } var message ErrorMessage - message.Message, err = rw.ReadVString(reader) + message.Message, err = varbin.ReadValue[string](reader, binary.BigEndian) if err != nil { return nil, err } @@ -87,7 +87,7 @@ func (e *ProfileEncoder) Encode() []byte { binary.Write(&buffer, binary.BigEndian, uint16(len(e.profiles))) for _, preview := range e.profiles { binary.Write(&buffer, binary.BigEndian, preview.ProfileID) - rw.WriteVString(&buffer, preview.Name) + varbin.Write(&buffer, binary.BigEndian, preview.Name) binary.Write(&buffer, binary.BigEndian, preview.Type) } return buffer.Bytes() @@ -117,7 +117,7 @@ func (d *ProfileDecoder) Decode(data []byte) error { if err != nil { return err } - profile.Name, err = rw.ReadVString(reader) + profile.Name, err = varbin.ReadValue[string](reader, binary.BigEndian) if err != nil { return err } @@ -147,7 +147,7 @@ func (r *ProfileContentRequest) Encode() []byte { func DecodeProfileContentRequest(data []byte) (*ProfileContentRequest, error) { reader := bytes.NewReader(data) - messageType, err := rw.ReadByte(reader) + messageType, err := reader.ReadByte() if err != nil { return nil, err } @@ -176,12 +176,13 @@ func (c *ProfileContent) Encode() []byte { buffer := new(bytes.Buffer) buffer.WriteByte(MessageTypeProfileContent) buffer.WriteByte(1) - writer := gzip.NewWriter(buffer) - rw.WriteVString(writer, c.Name) + gWriter := gzip.NewWriter(buffer) + writer := bufio.NewWriter(gWriter) + varbin.Write(writer, binary.BigEndian, c.Name) binary.Write(writer, binary.BigEndian, c.Type) - rw.WriteVString(writer, c.Config) + varbin.Write(writer, binary.BigEndian, c.Config) if c.Type != ProfileTypeLocal { - rw.WriteVString(writer, c.RemotePath) + varbin.Write(writer, binary.BigEndian, c.RemotePath) } if c.Type == ProfileTypeRemote { binary.Write(writer, binary.BigEndian, c.AutoUpdate) @@ -189,29 +190,31 @@ func (c *ProfileContent) Encode() []byte { binary.Write(writer, binary.BigEndian, c.LastUpdated) } writer.Flush() - writer.Close() + gWriter.Flush() + gWriter.Close() return buffer.Bytes() } func DecodeProfileContent(data []byte) (*ProfileContent, error) { - var reader io.Reader = bytes.NewReader(data) - messageType, err := rw.ReadByte(reader) + reader := bytes.NewReader(data) + messageType, err := reader.ReadByte() if err != nil { return nil, err } if messageType != MessageTypeProfileContent { return nil, E.New("invalid message") } - version, err := rw.ReadByte(reader) + version, err := reader.ReadByte() if err != nil { return nil, err } - reader, err = gzip.NewReader(reader) + gReader, err := gzip.NewReader(reader) if err != nil { return nil, E.Cause(err, "unsupported profile") } + bReader := varbin.StubReader(gReader) var content ProfileContent - content.Name, err = rw.ReadVString(reader) + content.Name, err = varbin.ReadValue[string](bReader, binary.BigEndian) if err != nil { return nil, err } @@ -219,12 +222,12 @@ func DecodeProfileContent(data []byte) (*ProfileContent, error) { if err != nil { return nil, err } - content.Config, err = rw.ReadVString(reader) + content.Config, err = varbin.ReadValue[string](bReader, binary.BigEndian) if err != nil { return nil, err } if content.Type != ProfileTypeLocal { - content.RemotePath, err = rw.ReadVString(reader) + content.RemotePath, err = varbin.ReadValue[string](bReader, binary.BigEndian) if err != nil { return nil, err } diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index 31611354bf..ac67db38f2 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -3,6 +3,7 @@ package libbox import ( "os" "os/user" + "runtime/debug" "strconv" "time" @@ -21,6 +22,11 @@ var ( sTVOS bool ) +func init() { + debug.SetPanicOnFault(true) + debug.SetTraceback("all") +} + func Setup(basePath string, workingPath string, tempPath string, isTVOS bool) { sBasePath = basePath sWorkingPath = workingPath diff --git a/go.mod b/go.mod index d471b4433b..6346e38ab2 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f github.com/sagernet/quic-go v0.47.0-beta.2 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.4.3 + github.com/sagernet/sing v0.5.0-beta.2 github.com/sagernet/sing-dns v0.2.3 github.com/sagernet/sing-mux v0.2.0 github.com/sagernet/sing-quic v0.2.2 diff --git a/go.sum b/go.sum index e6147304a0..1301c07286 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/sagernet/quic-go v0.47.0-beta.2/go.mod h1:bLVKvElSEMNv7pu7SZHscW02TYi 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.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= -github.com/sagernet/sing v0.4.3 h1:Ty/NAiNnVd6844k7ujlL5lkzydhcTH5Psc432jXA4Y8= -github.com/sagernet/sing v0.4.3/go.mod h1:ieZHA/+Y9YZfXs2I3WtuwgyCZ6GPsIR7HdKb1SdEnls= +github.com/sagernet/sing v0.5.0-beta.2 h1:V12EpwtsgYo5OLGjAiGoJobDJZeUsKv0b5y+yGAM6W0= +github.com/sagernet/sing v0.5.0-beta.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-dns v0.2.3 h1:YzeBUn2tR38F7HtvGEQ0kLRLmZWMEgi/+7wqa4Twb1k= github.com/sagernet/sing-dns v0.2.3/go.mod h1:BJpJv6XLnrUbSyIntOT6DG9FW0f4fETmPAHvNjOprLg= github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo= diff --git a/inbound/mixed.go b/inbound/mixed.go index 982842efeb..3933f7af05 100644 --- a/inbound/mixed.go +++ b/inbound/mixed.go @@ -12,10 +12,7 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/auth" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/protocol/http" "github.com/sagernet/sing/protocol/socks" "github.com/sagernet/sing/protocol/socks/socks4" @@ -51,16 +48,17 @@ func NewMixed(ctx context.Context, router adapter.Router, logger log.ContextLogg } func (h *Mixed) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - headerType, err := rw.ReadByte(conn) + reader := std_bufio.NewReader(conn) + headerBytes, err := reader.Peek(1) if err != nil { return err } - switch headerType { + switch headerBytes[0] { case socks4.Version, socks5.Version: - return socks.HandleConnection0(ctx, conn, headerType, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata)) + return socks.HandleConnection0(ctx, conn, reader, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata)) + default: + return http.HandleConnection(ctx, conn, reader, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata)) } - reader := std_bufio.NewReader(bufio.NewCachedReader(conn, buf.As([]byte{headerType}))) - return http.HandleConnection(ctx, conn, reader, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata)) } func (h *Mixed) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { diff --git a/inbound/vless.go b/inbound/vless.go index 69ed042bc3..567475647a 100644 --- a/inbound/vless.go +++ b/inbound/vless.go @@ -83,12 +83,11 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg } func (h *VLESS) Start() error { - err := common.Start( - h.service, - h.tlsConfig, - ) - if err != nil { - return err + if h.tlsConfig != nil { + err := h.tlsConfig.Start() + if err != nil { + return err + } } if h.transport == nil { return h.myInboundAdapter.Start() diff --git a/inbound/vmess.go b/inbound/vmess.go index 70676bbd8c..154512751c 100644 --- a/inbound/vmess.go +++ b/inbound/vmess.go @@ -93,13 +93,16 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg } func (h *VMess) Start() error { - err := common.Start( - h.service, - h.tlsConfig, - ) + err := h.service.Start() if err != nil { return err } + if h.tlsConfig != nil { + err = h.tlsConfig.Start() + if err != nil { + return err + } + } if h.transport == nil { return h.myInboundAdapter.Start() } diff --git a/option/outbound.go b/option/outbound.go index 59ee85ab69..6c943cd9af 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -113,7 +113,7 @@ type DialerOptions struct { Inet4BindAddress *ListenAddress `json:"inet4_bind_address,omitempty"` Inet6BindAddress *ListenAddress `json:"inet6_bind_address,omitempty"` ProtectPath string `json:"protect_path,omitempty"` - RoutingMark int `json:"routing_mark,omitempty"` + RoutingMark uint32 `json:"routing_mark,omitempty"` ReuseAddr bool `json:"reuse_addr,omitempty"` ConnectTimeout Duration `json:"connect_timeout,omitempty"` TCPFastOpen bool `json:"tcp_fast_open,omitempty"` diff --git a/option/route.go b/option/route.go index e313fcf242..dfd72986ff 100644 --- a/option/route.go +++ b/option/route.go @@ -10,7 +10,7 @@ type RouteOptions struct { AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"` DefaultInterface string `json:"default_interface,omitempty"` - DefaultMark int `json:"default_mark,omitempty"` + DefaultMark uint32 `json:"default_mark,omitempty"` } type GeoIPOptions struct { diff --git a/outbound/proxy.go b/outbound/proxy.go index 6127f0f215..fbc4848180 100644 --- a/outbound/proxy.go +++ b/outbound/proxy.go @@ -1,7 +1,6 @@ package outbound import ( - std_bufio "bufio" "context" "crypto/rand" "encoding/hex" @@ -11,16 +10,10 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" - "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" - "github.com/sagernet/sing/common/rw" - "github.com/sagernet/sing/protocol/http" "github.com/sagernet/sing/protocol/socks" - "github.com/sagernet/sing/protocol/socks/socks4" - "github.com/sagernet/sing/protocol/socks/socks5" ) type ProxyListener struct { @@ -102,16 +95,7 @@ func (l *ProxyListener) acceptLoop() { } func (l *ProxyListener) accept(ctx context.Context, conn *net.TCPConn) error { - headerType, err := rw.ReadByte(conn) - if err != nil { - return err - } - switch headerType { - case socks4.Version, socks5.Version: - return socks.HandleConnection0(ctx, conn, headerType, l.authenticator, l, M.Metadata{}) - } - reader := std_bufio.NewReader(bufio.NewCachedReader(conn, buf.As([]byte{headerType}))) - return http.HandleConnection(ctx, conn, reader, l.authenticator, l, M.Metadata{}) + return socks.HandleConnection(ctx, conn, l.authenticator, l, M.Metadata{}) } func (l *ProxyListener) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata M.Metadata) error { diff --git a/outbound/tor.go b/outbound/tor.go index 76c7955da7..8ae73a66fa 100644 --- a/outbound/tor.go +++ b/outbound/tor.go @@ -44,10 +44,10 @@ func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger startConf.ExtraArgs = options.ExtraArgs if options.DataDirectory != "" { dataDirAbs, _ := filepath.Abs(startConf.DataDir) - if geoIPPath := filepath.Join(dataDirAbs, "geoip"); rw.FileExists(geoIPPath) && !common.Contains(options.ExtraArgs, "--GeoIPFile") { + if geoIPPath := filepath.Join(dataDirAbs, "geoip"); rw.IsFile(geoIPPath) && !common.Contains(options.ExtraArgs, "--GeoIPFile") { options.ExtraArgs = append(options.ExtraArgs, "--GeoIPFile", geoIPPath) } - if geoIP6Path := filepath.Join(dataDirAbs, "geoip6"); rw.FileExists(geoIP6Path) && !common.Contains(options.ExtraArgs, "--GeoIPv6File") { + if geoIP6Path := filepath.Join(dataDirAbs, "geoip6"); rw.IsFile(geoIP6Path) && !common.Contains(options.ExtraArgs, "--GeoIPv6File") { options.ExtraArgs = append(options.ExtraArgs, "--GeoIPv6File", geoIP6Path) } } @@ -58,8 +58,12 @@ func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger } if startConf.DataDir != "" { torrcFile := filepath.Join(startConf.DataDir, "torrc") - if !rw.FileExists(torrcFile) { - err := rw.WriteFile(torrcFile, []byte("")) + err := rw.MkdirParent(torrcFile) + if err != nil { + return nil, err + } + if !rw.IsFile(torrcFile) { + err := os.WriteFile(torrcFile, []byte(""), 0o600) if err != nil { return nil, err } diff --git a/route/router.go b/route/router.go index a8698264cb..3a89568518 100644 --- a/route/router.go +++ b/route/router.go @@ -82,7 +82,7 @@ type Router struct { interfaceFinder *control.DefaultInterfaceFinder autoDetectInterface bool defaultInterface string - defaultMark int + defaultMark uint32 networkMonitor tun.NetworkUpdateMonitor interfaceMonitor tun.DefaultInterfaceMonitor packageManager tun.PackageManager @@ -1171,7 +1171,7 @@ func (r *Router) DefaultInterface() string { return r.defaultInterface } -func (r *Router) DefaultMark() int { +func (r *Router) DefaultMark() uint32 { return r.defaultMark } diff --git a/route/router_geo_resources.go b/route/router_geo_resources.go index e0a572c92f..14364d210d 100644 --- a/route/router_geo_resources.go +++ b/route/router_geo_resources.go @@ -50,7 +50,7 @@ func (r *Router) prepareGeoIPDatabase() error { geoPath = foundPath } } - if !rw.FileExists(geoPath) { + if !rw.IsFile(geoPath) { geoPath = filemanager.BasePath(r.ctx, geoPath) } if stat, err := os.Stat(geoPath); err == nil { @@ -61,7 +61,7 @@ func (r *Router) prepareGeoIPDatabase() error { os.Remove(geoPath) } } - if !rw.FileExists(geoPath) { + if !rw.IsFile(geoPath) { r.logger.Warn("geoip database not exists: ", geoPath) var err error for attempts := 0; attempts < 3; attempts++ { @@ -96,7 +96,7 @@ func (r *Router) prepareGeositeDatabase() error { geoPath = foundPath } } - if !rw.FileExists(geoPath) { + if !rw.IsFile(geoPath) { geoPath = filemanager.BasePath(r.ctx, geoPath) } if stat, err := os.Stat(geoPath); err == nil { @@ -107,7 +107,7 @@ func (r *Router) prepareGeositeDatabase() error { os.Remove(geoPath) } } - if !rw.FileExists(geoPath) { + if !rw.IsFile(geoPath) { r.logger.Warn("geosite database not exists: ", geoPath) var err error for attempts := 0; attempts < 3; attempts++ { diff --git a/route/rule_abstract.go b/route/rule_abstract.go index c13bdd8d96..9ef2e93277 100644 --- a/route/rule_abstract.go +++ b/route/rule_abstract.go @@ -29,9 +29,13 @@ func (r *abstractDefaultRule) Type() string { func (r *abstractDefaultRule) Start() error { for _, item := range r.allItems { - err := common.Start(item) - if err != nil { - return err + if starter, isStarter := item.(interface { + Start() error + }); isStarter { + err := starter.Start() + if err != nil { + return err + } } } return nil @@ -183,8 +187,13 @@ func (r *abstractLogicalRule) UpdateGeosite() error { } func (r *abstractLogicalRule) Start() error { - for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (common.Starter, bool) { - rule, loaded := it.(common.Starter) + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (interface { + Start() error + }, bool, + ) { + rule, loaded := it.(interface { + Start() error + }) return rule, loaded }) { err := rule.Start() diff --git a/transport/trojan/mux.go b/transport/trojan/mux.go index 13ac1e83a9..b1cc9985c9 100644 --- a/transport/trojan/mux.go +++ b/transport/trojan/mux.go @@ -1,12 +1,14 @@ package trojan import ( + std_bufio "bufio" "context" "net" + "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" - "github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/task" "github.com/sagernet/smux" ) @@ -33,27 +35,36 @@ func HandleMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata return group.Run(ctx) } -func newMuxConnection(ctx context.Context, stream net.Conn, metadata M.Metadata, handler Handler) { - err := newMuxConnection0(ctx, stream, metadata, handler) +func newMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler) { + err := newMuxConnection0(ctx, conn, metadata, handler) if err != nil { handler.NewError(ctx, E.Cause(err, "process trojan-go multiplex connection")) } } -func newMuxConnection0(ctx context.Context, stream net.Conn, metadata M.Metadata, handler Handler) error { - command, err := rw.ReadByte(stream) +func newMuxConnection0(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler) error { + reader := std_bufio.NewReader(conn) + command, err := reader.ReadByte() if err != nil { return E.Cause(err, "read command") } - metadata.Destination, err = M.SocksaddrSerializer.ReadAddrPort(stream) + metadata.Destination, err = M.SocksaddrSerializer.ReadAddrPort(reader) if err != nil { return E.Cause(err, "read destination") } + if reader.Buffered() > 0 { + buffer := buf.NewSize(reader.Buffered()) + _, err = buffer.ReadFullFrom(reader, buffer.Len()) + if err != nil { + return err + } + conn = bufio.NewCachedConn(conn, buffer) + } switch command { case CommandTCP: - return handler.NewConnection(ctx, stream, metadata) + return handler.NewConnection(ctx, conn, metadata) case CommandUDP: - return handler.NewPacketConnection(ctx, &PacketConn{Conn: stream}, metadata) + return handler.NewPacketConnection(ctx, &PacketConn{Conn: conn}, metadata) default: return E.New("unknown command ", command) } diff --git a/transport/trojan/service.go b/transport/trojan/service.go index 9078276c73..97f674ab93 100644 --- a/transport/trojan/service.go +++ b/transport/trojan/service.go @@ -2,6 +2,7 @@ package trojan import ( "context" + "encoding/binary" "net" "github.com/sagernet/sing/common/auth" @@ -76,7 +77,8 @@ func (s *Service[K]) NewConnection(ctx context.Context, conn net.Conn, metadata return E.Cause(err, "skip crlf") } - command, err := rw.ReadByte(conn) + var command byte + err = binary.Read(conn, binary.BigEndian, &command) if err != nil { return E.Cause(err, "read command") } diff --git a/transport/v2raygrpc/conn.go b/transport/v2raygrpc/conn.go index 0fecbf33d3..bc78f91e3c 100644 --- a/transport/v2raygrpc/conn.go +++ b/transport/v2raygrpc/conn.go @@ -8,7 +8,7 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/baderror" M "github.com/sagernet/sing/common/metadata" - "github.com/sagernet/sing/common/rw" + N "github.com/sagernet/sing/common/network" ) var _ net.Conn = (*GRPCConn)(nil) @@ -90,7 +90,7 @@ func (c *GRPCConn) Upstream() any { return c.GunService } -var _ rw.WriteCloser = (*clientConnWrapper)(nil) +var _ N.WriteCloser = (*clientConnWrapper)(nil) type clientConnWrapper struct { GunService_TunClient diff --git a/transport/v2raygrpclite/conn.go b/transport/v2raygrpclite/conn.go index f5a71939d3..5ab02569fd 100644 --- a/transport/v2raygrpclite/conn.go +++ b/transport/v2raygrpclite/conn.go @@ -13,7 +13,7 @@ import ( "github.com/sagernet/sing/common/baderror" "github.com/sagernet/sing/common/buf" M "github.com/sagernet/sing/common/metadata" - "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/common/varbin" ) // kanged from: https://github.com/Qv2ray/gun-lite @@ -96,7 +96,7 @@ func (c *GunConn) read(b []byte) (n int, err error) { } func (c *GunConn) Write(b []byte) (n int, err error) { - varLen := rw.UVariantLen(uint64(len(b))) + varLen := varbin.UvarintLen(uint64(len(b))) buffer := buf.NewSize(6 + varLen + len(b)) header := buffer.Extend(6 + varLen) header[0] = 0x00 @@ -117,13 +117,13 @@ func (c *GunConn) Write(b []byte) (n int, err error) { func (c *GunConn) WriteBuffer(buffer *buf.Buffer) error { defer buffer.Release() dataLen := buffer.Len() - varLen := rw.UVariantLen(uint64(dataLen)) + varLen := varbin.UvarintLen(uint64(dataLen)) header := buffer.ExtendHeader(6 + varLen) header[0] = 0x00 binary.BigEndian.PutUint32(header[1:5], uint32(1+varLen+dataLen)) header[5] = 0x0A binary.PutUvarint(header[6:], uint64(dataLen)) - err := rw.WriteBytes(c.writer, buffer.Bytes()) + err := common.Error(c.writer.Write(buffer.Bytes())) if err != nil { return baderror.WrapH2(err) } From a18179ad24df7b5ca717f787a2f7ba7ad29e87ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 19 Aug 2024 07:34:11 +0800 Subject: [PATCH 05/25] Implement read deadline for QUIC based UDP inbounds --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6346e38ab2..7f3610d48d 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/sagernet/sing v0.5.0-beta.2 github.com/sagernet/sing-dns v0.2.3 github.com/sagernet/sing-mux v0.2.0 - github.com/sagernet/sing-quic v0.2.2 + github.com/sagernet/sing-quic v0.3.0-beta.3 github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/sagernet/sing-shadowtls v0.1.4 diff --git a/go.sum b/go.sum index 1301c07286..40318265fe 100644 --- a/go.sum +++ b/go.sum @@ -114,8 +114,8 @@ github.com/sagernet/sing-dns v0.2.3 h1:YzeBUn2tR38F7HtvGEQ0kLRLmZWMEgi/+7wqa4Twb github.com/sagernet/sing-dns v0.2.3/go.mod h1:BJpJv6XLnrUbSyIntOT6DG9FW0f4fETmPAHvNjOprLg= github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo= github.com/sagernet/sing-mux v0.2.0/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ= -github.com/sagernet/sing-quic v0.2.2 h1:Ryp02zMhHh/ZDrG7MdLsmhuBU8+BEpOdJonFQiqIopo= -github.com/sagernet/sing-quic v0.2.2/go.mod h1:YLV1dUDv8Eyp/8e55O/EvfsrwxOgEDVgDCIoPqmDREE= +github.com/sagernet/sing-quic v0.3.0-beta.3 h1:8S98VXZxtSiOqVCFbCNbMEvKDPhOF/VNBYMjVC3xMhw= +github.com/sagernet/sing-quic v0.3.0-beta.3/go.mod h1:rFPUlYnSj1Bx9gFSghjCqrCzfGvpjhkisOiTKpjq5vQ= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg= From bd122478fc4f3a9ccd02f807a16fbbfb3c72fc5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 17 Jul 2024 17:57:35 +0800 Subject: [PATCH 06/25] Bump rule-set version --- cmd/sing-box/cmd_geoip_export.go | 2 +- cmd/sing-box/cmd_geosite_export.go | 2 +- cmd/sing-box/cmd_rule_set_compile.go | 6 +- cmd/sing-box/cmd_rule_set_upgrade.go | 91 ++++++++++++++++++++ common/srs/binary.go | 38 ++++---- constant/rule.go | 6 +- docs/configuration/rule-set/source-format.md | 21 ++++- option/rule_set.go | 6 +- route/rule_item_domain.go | 2 +- 9 files changed, 145 insertions(+), 29 deletions(-) create mode 100644 cmd/sing-box/cmd_rule_set_upgrade.go diff --git a/cmd/sing-box/cmd_geoip_export.go b/cmd/sing-box/cmd_geoip_export.go index 5787d2e5ad..b80e5cd3d0 100644 --- a/cmd/sing-box/cmd_geoip_export.go +++ b/cmd/sing-box/cmd_geoip_export.go @@ -87,7 +87,7 @@ func geoipExport(countryCode string) error { headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String()) } var plainRuleSet option.PlainRuleSetCompat - plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Version = C.RuleSetVersion2 plainRuleSet.Options.Rules = []option.HeadlessRule{ { Type: C.RuleTypeDefault, diff --git a/cmd/sing-box/cmd_geosite_export.go b/cmd/sing-box/cmd_geosite_export.go index 2a6c27a0ed..90a7955b99 100644 --- a/cmd/sing-box/cmd_geosite_export.go +++ b/cmd/sing-box/cmd_geosite_export.go @@ -70,7 +70,7 @@ func geositeExport(category string) error { headlessRule.DomainKeyword = defaultRule.DomainKeyword headlessRule.DomainRegex = defaultRule.DomainRegex var plainRuleSet option.PlainRuleSetCompat - plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Version = C.RuleSetVersion2 plainRuleSet.Options.Rules = []option.HeadlessRule{ { Type: C.RuleTypeDefault, diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go index 6e065101a8..7e3753c928 100644 --- a/cmd/sing-box/cmd_rule_set_compile.go +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/json" @@ -55,9 +56,6 @@ func compileRuleSet(sourcePath string) error { if err != nil { return err } - if err != nil { - return err - } ruleSet := plainRuleSet.Upgrade() var outputPath string if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { @@ -73,7 +71,7 @@ func compileRuleSet(sourcePath string) error { if err != nil { return err } - err = srs.Write(outputFile, ruleSet) + err = srs.Write(outputFile, ruleSet, plainRuleSet.Version == C.RuleSetVersion2) if err != nil { outputFile.Close() os.Remove(outputPath) diff --git a/cmd/sing-box/cmd_rule_set_upgrade.go b/cmd/sing-box/cmd_rule_set_upgrade.go new file mode 100644 index 0000000000..0ec039fd71 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_upgrade.go @@ -0,0 +1,91 @@ +package main + +import ( + "bytes" + "io" + "os" + "path/filepath" + + 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/sagernet/sing/common/json" + + "github.com/spf13/cobra" +) + +var commandRuleSetUpgradeFlagWrite bool + +var commandRuleSetUpgrade = &cobra.Command{ + Use: "upgrade ", + Short: "Upgrade rule-set json", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := upgradeRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSetUpgrade.Flags().BoolVarP(&commandRuleSetUpgradeFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") + commandRuleSet.AddCommand(commandRuleSetUpgrade) +} + +func upgradeRuleSet(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 + } + plainRuleSetCompat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return err + } + switch plainRuleSetCompat.Version { + case C.RuleSetVersion1: + default: + log.Info("already up-to-date") + return nil + } + plainRuleSet := plainRuleSetCompat.Upgrade() + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(plainRuleSet) + if err != nil { + return E.Cause(err, "encode config") + } + outputPath, _ := filepath.Abs(sourcePath) + if !commandRuleSetUpgradeFlagWrite || 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/common/srs/binary.go b/common/srs/binary.go index c7c55e0838..0bd5a9099b 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -54,14 +54,14 @@ func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err erro if err != nil { return ruleSet, err } - if version != 1 { + if version > C.RuleSetVersion2 { return ruleSet, E.New("unsupported version: ", version) } - zReader, err := zlib.NewReader(reader) + compressReader, err := zlib.NewReader(reader) if err != nil { return } - bReader := bufio.NewReader(zReader) + bReader := bufio.NewReader(compressReader) length, err := binary.ReadUvarint(bReader) if err != nil { return @@ -77,26 +77,32 @@ func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err erro return } -func Write(writer io.Writer, ruleSet option.PlainRuleSet) error { +func Write(writer io.Writer, ruleSet option.PlainRuleSet, generateUnstable bool) error { _, err := writer.Write(MagicBytes[:]) if err != nil { return err } - err = binary.Write(writer, binary.BigEndian, uint8(1)) + var version uint8 + if generateUnstable { + version = C.RuleSetVersion2 + } else { + version = C.RuleSetVersion1 + } + err = binary.Write(writer, binary.BigEndian, version) if err != nil { return err } - zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) + compressWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) if err != nil { return err } - bWriter := bufio.NewWriter(zWriter) + bWriter := bufio.NewWriter(compressWriter) _, err = varbin.WriteUvarint(bWriter, uint64(len(ruleSet.Rules))) if err != nil { return err } for _, rule := range ruleSet.Rules { - err = writeRule(bWriter, rule) + err = writeRule(bWriter, rule, generateUnstable) if err != nil { return err } @@ -105,7 +111,7 @@ func Write(writer io.Writer, ruleSet option.PlainRuleSet) error { if err != nil { return err } - return zWriter.Close() + return compressWriter.Close() } func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err error) { @@ -127,12 +133,12 @@ func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err return } -func writeRule(writer varbin.Writer, rule option.HeadlessRule) error { +func writeRule(writer varbin.Writer, rule option.HeadlessRule, generateUnstable bool) error { switch rule.Type { case C.RuleTypeDefault: - return writeDefaultRule(writer, rule.DefaultOptions) + return writeDefaultRule(writer, rule.DefaultOptions, generateUnstable) case C.RuleTypeLogical: - return writeLogicalRule(writer, rule.LogicalOptions) + return writeLogicalRule(writer, rule.LogicalOptions, generateUnstable) default: panic("unknown rule type: " + rule.Type) } @@ -219,7 +225,7 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea } } -func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule) error { +func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, generateUnstable bool) error { err := binary.Write(writer, binary.BigEndian, uint8(0)) if err != nil { return err @@ -243,7 +249,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule) err if err != nil { return err } - err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer) + err = domain.NewMatcher(rule.Domain, rule.DomainSuffix, !generateUnstable).Write(writer) if err != nil { return err } @@ -420,7 +426,7 @@ func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.Lo return } -func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule) error { +func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule, generateUnstable bool) error { err := binary.Write(writer, binary.BigEndian, uint8(1)) if err != nil { return err @@ -441,7 +447,7 @@ func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRu return err } for _, rule := range logicalRule.Rules { - err = writeRule(writer, rule) + err = writeRule(writer, rule, generateUnstable) if err != nil { return err } diff --git a/constant/rule.go b/constant/rule.go index 5a8eaf127f..fb0f39e22e 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -13,7 +13,11 @@ const ( const ( RuleSetTypeLocal = "local" RuleSetTypeRemote = "remote" - RuleSetVersion1 = 1 RuleSetFormatSource = "source" RuleSetFormatBinary = "binary" ) + +const ( + RuleSetVersion1 = 1 + iota + RuleSetVersion2 +) diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md index ee5e48e04c..8c46cbf02b 100644 --- a/docs/configuration/rule-set/source-format.md +++ b/docs/configuration/rule-set/source-format.md @@ -1,12 +1,20 @@ +--- +icon: material/new-box +--- + # Source Format +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: version `2` + !!! question "Since sing-box 1.8.0" ### Structure ```json { - "version": 1, + "version": 2, "rules": [] } ``` @@ -21,7 +29,16 @@ Use `sing-box rule-set compile [--output .srs] .json` to c ==Required== -Version of Rule Set, must be `1`. +Version of rule-set, one of `1` or `2`. + +* 1: Initial rule-set version, since sing-box 1.8.0. +* 2: Optimized memory usages of `domain_suffix` rules. + +The new rule-set version `2` does not make any changes to the format, only affecting `binary` rule-sets compiled by command `rule-set compile` + +Since 1.10.0, the optimization is always applied to `source` rule-sets even if version is set to `1`. + +It is recommended to upgrade to `2` after sing-box 1.10.0 becomes a stable version. #### rules diff --git a/option/rule_set.go b/option/rule_set.go index 8e367e69a5..5498400f2f 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -185,7 +185,7 @@ type PlainRuleSetCompat _PlainRuleSetCompat func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { var v any switch r.Version { - case C.RuleSetVersion1: + case C.RuleSetVersion1, C.RuleSetVersion2: v = r.Options default: return nil, E.New("unknown rule set version: ", r.Version) @@ -200,7 +200,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { } var v any switch r.Version { - case C.RuleSetVersion1: + case C.RuleSetVersion1, C.RuleSetVersion2: v = &r.Options case 0: return E.New("missing rule set version") @@ -217,7 +217,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { func (r PlainRuleSetCompat) Upgrade() PlainRuleSet { var result PlainRuleSet switch r.Version { - case C.RuleSetVersion1: + case C.RuleSetVersion1, C.RuleSetVersion2: result = r.Options default: panic("unknown rule set version: " + F.ToString(r.Version)) diff --git a/route/rule_item_domain.go b/route/rule_item_domain.go index 36839a5556..c77890df24 100644 --- a/route/rule_item_domain.go +++ b/route/rule_item_domain.go @@ -38,7 +38,7 @@ func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem { } } return &DomainItem{ - domain.NewMatcher(domains, domainSuffixes), + domain.NewMatcher(domains, domainSuffixes, false), description, } } From 744cc985ba69fb793f87811007885c4a465ad5e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 22 Jul 2024 12:10:22 +0800 Subject: [PATCH 07/25] Improve usages of `json.Unmarshal` --- option/json.go | 6 +----- option/types.go | 4 ++-- option/udp_over_tcp.go | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/option/json.go b/option/json.go index 07580e9f7b..775141d5aa 100644 --- a/option/json.go +++ b/option/json.go @@ -1,8 +1,6 @@ package option import ( - "bytes" - "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" @@ -69,7 +67,5 @@ func UnmarshallExcluded(inputContent []byte, parentObject any, object any) error if err != nil { return err } - decoder := json.NewDecoder(bytes.NewReader(inputContent)) - decoder.DisallowUnknownFields() - return decoder.Decode(object) + return json.UnmarshalDisallowUnknownFields(inputContent, object) } diff --git a/option/types.go b/option/types.go index 83fee8f05a..b17f222248 100644 --- a/option/types.go +++ b/option/types.go @@ -128,12 +128,12 @@ func (l Listable[T]) MarshalJSON() ([]byte, error) { } func (l *Listable[T]) UnmarshalJSON(content []byte) error { - err := json.Unmarshal(content, (*[]T)(l)) + err := json.UnmarshalDisallowUnknownFields(content, (*[]T)(l)) if err == nil { return nil } var singleItem T - newError := json.Unmarshal(content, &singleItem) + newError := json.UnmarshalDisallowUnknownFields(content, &singleItem) if newError != nil { return E.Errors(err, newError) } diff --git a/option/udp_over_tcp.go b/option/udp_over_tcp.go index e8a7a9726e..b496017a41 100644 --- a/option/udp_over_tcp.go +++ b/option/udp_over_tcp.go @@ -26,5 +26,5 @@ func (o *UDPOverTCPOptions) UnmarshalJSON(bytes []byte) error { if err == nil { return nil } - return json.Unmarshal(bytes, (*_UDPOverTCPOptions)(o)) + return json.UnmarshalDisallowUnknownFields(bytes, (*_UDPOverTCPOptions)(o)) } From 2dbeb63b4eaf8bae460e7644427656945277329f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 3 Jul 2024 19:42:06 +0800 Subject: [PATCH 08/25] Add IP address support for `rule-set match` match --- cmd/sing-box/cmd_rule_set_match.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/cmd/sing-box/cmd_rule_set_match.go b/cmd/sing-box/cmd_rule_set_match.go index 08784caf11..fb2560afb1 100644 --- a/cmd/sing-box/cmd_rule_set_match.go +++ b/cmd/sing-box/cmd_rule_set_match.go @@ -14,6 +14,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" + M "github.com/sagernet/sing/common/metadata" "github.com/spf13/cobra" ) @@ -21,8 +22,8 @@ import ( var flagRuleSetMatchFormat string var commandRuleSetMatch = &cobra.Command{ - Use: "match ", - Short: "Check if a domain matches the rule set", + Use: "match ", + Short: "Check if an IP address or a domain matches the rule set", Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { err := ruleSetMatch(args[0], args[1]) @@ -71,15 +72,20 @@ func ruleSetMatch(sourcePath string, domain string) error { default: return E.New("unknown rule set format: ", flagRuleSetMatchFormat) } + ipAddress := M.ParseAddr(domain) + var metadata adapter.InboundContext + if ipAddress.IsValid() { + metadata.Destination = M.SocksaddrFrom(ipAddress, 0) + } else { + metadata.Domain = domain + } for i, ruleOptions := range plainRuleSet.Rules { var currentRule adapter.HeadlessRule currentRule, err = route.NewHeadlessRule(nil, ruleOptions) if err != nil { return E.Cause(err, "parse rule_set.rules.[", i, "]") } - if currentRule.Match(&adapter.InboundContext{ - Domain: domain, - }) { + if currentRule.Match(&metadata) { println(F.ToString("match rules.[", i, "]: ", currentRule)) } } From 00e621092827e633d3a6c84f3284171ea0e2c0a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 3 Jul 2024 19:42:21 +0800 Subject: [PATCH 09/25] Add `rule-set decompile` command --- cmd/sing-box/cmd_rule_set_decompile.go | 83 ++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 cmd/sing-box/cmd_rule_set_decompile.go diff --git a/cmd/sing-box/cmd_rule_set_decompile.go b/cmd/sing-box/cmd_rule_set_decompile.go new file mode 100644 index 0000000000..02af03dd67 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_decompile.go @@ -0,0 +1,83 @@ +package main + +import ( + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/json" + + "github.com/spf13/cobra" +) + +var flagRuleSetDecompileOutput string + +const flagRuleSetDecompileDefaultOutput = ".json" + +var commandRuleSetDecompile = &cobra.Command{ + Use: "decompile [binary-path]", + Short: "Decompile rule-set binary to json", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := decompileRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSet.AddCommand(commandRuleSetDecompile) + commandRuleSetDecompile.Flags().StringVarP(&flagRuleSetDecompileOutput, "output", "o", flagRuleSetDecompileDefaultOutput, "Output file") +} + +func decompileRuleSet(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 + } + } + plainRuleSet, err := srs.Read(reader, true) + if err != nil { + return err + } + ruleSet := option.PlainRuleSetCompat{ + Version: C.RuleSetVersion1, + Options: plainRuleSet, + } + var outputPath string + if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput { + if strings.HasSuffix(sourcePath, ".srs") { + outputPath = sourcePath[:len(sourcePath)-4] + ".json" + } else { + outputPath = sourcePath + ".json" + } + } else { + outputPath = flagRuleSetDecompileOutput + } + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + encoder := json.NewEncoder(outputFile) + encoder.SetIndent("", " ") + err = encoder.Encode(ruleSet) + if err != nil { + outputFile.Close() + os.Remove(outputPath) + return err + } + outputFile.Close() + return nil +} From a50b0bc1a97dd16355879e8a6610db0952efdbfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 7 Jun 2024 15:55:21 +0800 Subject: [PATCH 10/25] Add auto-redirect & Improve auto-route --- Dockerfile | 2 +- adapter/router.go | 15 ++ box.go | 28 ++- cmd/sing-box/cmd_tools_fetch.go | 32 ++- cmd/sing-box/cmd_tools_fetch_http3.go | 36 +++ cmd/sing-box/cmd_tools_fetch_http3_stub.go | 18 ++ common/dialer/default.go | 18 +- docs/configuration/inbound/tun.md | 144 +++++++++++- docs/configuration/inbound/tun.zh.md | 147 ++++++++++++- docs/deprecated.md | 8 + docs/deprecated.zh.md | 8 + docs/migration.md | 68 ++++++ docs/migration.zh.md | 68 ++++++ go.mod | 19 +- go.sum | 40 ++-- inbound/tun.go | 243 ++++++++++++++++++--- option/tun.go | 63 ++++-- route/router.go | 52 ++++- route/rule_item_rule_set.go | 1 + route/rule_set.go | 21 ++ route/rule_set_local.go | 59 ++++- route/rule_set_remote.go | 74 ++++++- 22 files changed, 1045 insertions(+), 119 deletions(-) create mode 100644 cmd/sing-box/cmd_tools_fetch_http3.go create mode 100644 cmd/sing-box/cmd_tools_fetch_http3_stub.go diff --git a/Dockerfile b/Dockerfile index 22f2dfb289..e82b81cee9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ FROM --platform=$TARGETPLATFORM alpine AS dist LABEL maintainer="nekohasekai " RUN set -ex \ && apk upgrade \ - && apk add bash tzdata ca-certificates \ + && apk add bash tzdata ca-certificates nftables \ && rm -rf /var/cache/apk/* COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box ENTRYPOINT ["sing-box"] diff --git a/adapter/router.go b/adapter/router.go index c481f0c8c9..619c1110cb 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -10,15 +10,18 @@ import ( "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" mdns "github.com/miekg/dns" + "go4.org/netipx" ) type Router interface { Service PreStarter PostStarter + Cleanup() error Outbounds() []Outbound Outbound(tag string) (Outbound, bool) @@ -46,6 +49,8 @@ type Router interface { AutoDetectInterface() bool AutoDetectInterfaceFunc() control.Func DefaultMark() uint32 + RegisterAutoRedirectOutputMark(mark uint32) error + AutoRedirectOutputMark() uint32 NetworkMonitor() tun.NetworkUpdateMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor PackageManager() tun.PackageManager @@ -92,12 +97,22 @@ type DNSRule interface { } type RuleSet interface { + Name() string StartContext(ctx context.Context, startContext RuleSetStartContext) error + PostStart() error Metadata() RuleSetMetadata + ExtractIPSet() []*netipx.IPSet + IncRef() + DecRef() + Cleanup() + RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback] + UnregisterCallback(element *list.Element[RuleSetUpdateCallback]) Close() error HeadlessRule } +type RuleSetUpdateCallback func(it RuleSet) + type RuleSetMetadata struct { ContainsProcessRule bool ContainsWIFIRule bool diff --git a/box.go b/box.go index 3c514cfee2..716b1b093c 100644 --- a/box.go +++ b/box.go @@ -303,7 +303,11 @@ func (s *Box) start() error { return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]") } } - return s.postStart() + err = s.postStart() + if err != nil { + return err + } + return s.router.Cleanup() } func (s *Box) postStart() error { @@ -313,16 +317,28 @@ func (s *Box) postStart() error { return E.Cause(err, "start ", serviceName) } } - for _, outbound := range s.outbounds { - if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound { + // TODO: reorganize ALL start order + for _, out := range s.outbounds { + if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound { err := lateOutbound.PostStart() if err != nil { - return E.Cause(err, "post-start outbound/", outbound.Tag()) + return E.Cause(err, "post-start outbound/", out.Tag()) } } } - - return s.router.PostStart() + err := s.router.PostStart() + if err != nil { + return err + } + for _, in := range s.inbounds { + if lateInbound, isLateInbound := in.(adapter.PostStarter); isLateInbound { + err = lateInbound.PostStart() + if err != nil { + return E.Cause(err, "post-start inbound/", in.Tag()) + } + } + } + return nil } func (s *Box) Close() error { diff --git a/cmd/sing-box/cmd_tools_fetch.go b/cmd/sing-box/cmd_tools_fetch.go index 256c3f42b6..3f62424a49 100644 --- a/cmd/sing-box/cmd_tools_fetch.go +++ b/cmd/sing-box/cmd_tools_fetch.go @@ -9,8 +9,10 @@ import ( "net/url" "os" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/spf13/cobra" @@ -32,7 +34,10 @@ func init() { commandTools.AddCommand(commandFetch) } -var httpClient *http.Client +var ( + httpClient *http.Client + http3Client *http.Client +) func fetch(args []string) error { instance, err := createPreStartedClient() @@ -53,8 +58,16 @@ func fetch(args []string) error { }, } defer httpClient.CloseIdleConnections() + if C.WithQUIC { + err = initializeHTTP3Client(instance) + if err != nil { + return err + } + defer http3Client.CloseIdleConnections() + } for _, urlString := range args { - parsedURL, err := url.Parse(urlString) + var parsedURL *url.URL + parsedURL, err = url.Parse(urlString) if err != nil { return err } @@ -63,16 +76,27 @@ func fetch(args []string) error { parsedURL.Scheme = "http" fallthrough case "http", "https": - err = fetchHTTP(parsedURL) + err = fetchHTTP(httpClient, parsedURL) + if err != nil { + return err + } + case "http3": + if !C.WithQUIC { + return C.ErrQUICNotIncluded + } + parsedURL.Scheme = "https" + err = fetchHTTP(http3Client, parsedURL) if err != nil { return err } + default: + return E.New("unsupported scheme: ", parsedURL.Scheme) } } return nil } -func fetchHTTP(parsedURL *url.URL) error { +func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error { request, err := http.NewRequest("GET", parsedURL.String(), nil) if err != nil { return err diff --git a/cmd/sing-box/cmd_tools_fetch_http3.go b/cmd/sing-box/cmd_tools_fetch_http3.go new file mode 100644 index 0000000000..5dc3d9157f --- /dev/null +++ b/cmd/sing-box/cmd_tools_fetch_http3.go @@ -0,0 +1,36 @@ +//go:build with_quic + +package main + +import ( + "context" + "crypto/tls" + "net/http" + + "github.com/sagernet/quic-go" + "github.com/sagernet/quic-go/http3" + box "github.com/sagernet/sing-box" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func initializeHTTP3Client(instance *box.Box) error { + dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound) + if err != nil { + return err + } + http3Client = &http.Client{ + Transport: &http3.RoundTripper{ + Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + destination := M.ParseSocksaddr(addr) + udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination) + if dErr != nil { + return nil, dErr + } + return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg) + }, + }, + } + return nil +} diff --git a/cmd/sing-box/cmd_tools_fetch_http3_stub.go b/cmd/sing-box/cmd_tools_fetch_http3_stub.go new file mode 100644 index 0000000000..ae13f54c42 --- /dev/null +++ b/cmd/sing-box/cmd_tools_fetch_http3_stub.go @@ -0,0 +1,18 @@ +//go:build !with_quic + +package main + +import ( + "net/url" + "os" + + box "github.com/sagernet/sing-box" +) + +func initializeHTTP3Client(instance *box.Box) error { + return os.ErrInvalid +} + +func fetchHTTP3(parsedURL *url.URL) error { + return os.ErrInvalid +} diff --git a/common/dialer/default.go b/common/dialer/default.go index 4fbad07deb..488e8000eb 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -50,12 +50,26 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) } - if options.RoutingMark != 0 { + var autoRedirectOutputMark uint32 + if router != nil { + autoRedirectOutputMark = router.AutoRedirectOutputMark() + } + if autoRedirectOutputMark > 0 { + dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark)) + listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark)) + } + if options.RoutingMark > 0 { dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark)) listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark)) - } else if router != nil && router.DefaultMark() != 0 { + if autoRedirectOutputMark > 0 { + return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `routing_mark`") + } + } else if router != nil && router.DefaultMark() > 0 { dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark())) listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark())) + if autoRedirectOutputMark > 0 { + return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `default_mark`") + } } if options.ReuseAddr { listener.Control = control.Append(listener.Control, control.ReuseAddr()) diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 1d5d8d0f65..1e2bf40058 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -2,6 +2,21 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: [address](#address) + :material-delete-clock: [inet4_address](#inet4_address) + :material-delete-clock: [inet6_address](#inet6_address) + :material-plus: [route_address](#route_address) + :material-delete-clock: [inet4_route_address](#inet4_route_address) + :material-delete-clock: [inet6_route_address](#inet6_route_address) + :material-plus: [route_exclude_address](#route_address) + :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address) + :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) + :material-plus: [auto_redirect](#auto_redirect) + :material-plus: [route_address_set](#route_address_set) + :material-plus: [route_exclude_address_set](#route_address_set) + !!! quote "Changes in sing-box 1.9.0" :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) @@ -23,26 +38,57 @@ icon: material/new-box "type": "tun", "tag": "tun-in", "interface_name": "tun0", - "inet4_address": "172.19.0.1/30", - "inet6_address": "fdfe:dcba:9876::1/126", + "address": [ + "172.18.0.1/30", + "fdfe:dcba:9876::1/126" + ], + // deprecated + "inet4_address": [ + "172.19.0.1/30" + ], + // deprecated + "inet6_address": [ + "fdfe:dcba:9876::1/126" + ], "mtu": 9000, "gso": false, "auto_route": true, "strict_route": true, + "auto_redirect": false, + "route_address": [ + "0.0.0.0/1", + "128.0.0.0/1", + "::/1", + "8000::/1" + ], + // deprecated "inet4_route_address": [ "0.0.0.0/1", "128.0.0.0/1" ], + // deprecated "inet6_route_address": [ "::/1", "8000::/1" ], + "route_exclude_address": [ + "192.168.0.0/16", + "fc00::/7" + ], + // deprecated "inet4_route_exclude_address": [ "192.168.0.0/16" ], + // deprecated "inet6_route_exclude_address": [ "fc00::/7" ], + "route_address_set": [ + "geoip-cloudflare" + ], + "route_exclude_address_set": [ + "geoip-cn" + ], "endpoint_independent_nat": false, "udp_timeout": "5m", "stack": "system", @@ -102,14 +148,26 @@ icon: material/new-box Virtual device name, automatically selected if empty. +#### address + +!!! question "Since sing-box 1.10.0" + +IPv4 and IPv6 prefix for the tun interface. + #### inet4_address -==Required== +!!! failure "Deprecated in sing-box 1.10.0" + + `inet4_address` is merged to `address` and will be removed in sing-box 1.11.0. IPv4 prefix for the tun interface. #### inet6_address +!!! failure "Deprecated in sing-box 1.10.0" + + `inet6_address` is merged to `address` and will be removed in sing-box 1.11.0. + IPv6 prefix for the tun interface. #### mtu @@ -145,9 +203,10 @@ Enforce strict routing rules when `auto_route` is enabled: *In Linux*: * Let unsupported network unreachable +* Make ICMP traffic route to tun instead of upstream interfaces * Route all connections to tun -It prevents address leaks and makes DNS hijacking work on Android. +It prevents IP address leaks and makes DNS hijacking work on Android. *In Windows*: @@ -156,22 +215,95 @@ It prevents address leaks and makes DNS hijacking work on Android. It may prevent some applications (such as VirtualBox) from working properly in certain situations. +#### auto_redirect + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux with `auto_route` enabled. + +Automatically configure iptables/nftables to redirect connections. + +*In Android*: + +Only local connections are forwarded. To share your VPN connection over hotspot or repeater, +use [VPNHotspot](https://github.com/Mygod/VPNHotspot). + +*In Linux*: + +`auto_route` with `auto_redirect` now works as expected on routers **without intervention**. + +#### route_address + +!!! question "Since sing-box 1.10.0" + +Use custom routes instead of default when `auto_route` is enabled. + #### inet4_route_address +!!! failure "Deprecated in sing-box 1.10.0" + + `inet4_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) instead. + Use custom routes instead of default when `auto_route` is enabled. #### inet6_route_address +!!! failure "Deprecated in sing-box 1.10.0" + + `inet6_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) instead. + Use custom routes instead of default when `auto_route` is enabled. +#### route_exclude_address + +!!! question "Since sing-box 1.10.0" + +Exclude custom routes when `auto_route` is enabled. + #### inet4_route_exclude_address +!!! failure "Deprecated in sing-box 1.10.0" + + `inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_exclude_address](#route_exclude_address) instead. + Exclude custom routes when `auto_route` is enabled. #### inet6_route_exclude_address +!!! failure "Deprecated in sing-box 1.10.0" + + `inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_exclude_address](#route_exclude_address) instead. + Exclude custom routes when `auto_route` is enabled. +#### route_address_set + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled. + +Add the destination IP CIDR rules in the specified rule-sets to the firewall. +Unmatched traffic will bypass the sing-box routes. + +Conflict with `route.default_mark` and `[dialOptions].routing_mark`. + +#### route_exclude_address_set + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled. + +Add the destination IP CIDR rules in the specified rule-sets to the firewall. +Matched traffic will bypass the sing-box routes. + +Conflict with `route.default_mark` and `[dialOptions].routing_mark`. + #### endpoint_independent_nat !!! info "" @@ -214,6 +346,10 @@ Conflict with `exclude_interface`. #### exclude_interface +!!! warning "" + + When `strict_route` enabled, return traffic to excluded interfaces will not be automatically excluded, so add them as well (example: `br-lan` and `pppoe-wan`). + Exclude interfaces in route. Conflict with `include_interface`. diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index 73d31d6497..5b1d35afb5 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -2,6 +2,21 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: [address](#address) + :material-delete-clock: [inet4_address](#inet4_address) + :material-delete-clock: [inet6_address](#inet6_address) + :material-plus: [route_address](#route_address) + :material-delete-clock: [inet4_route_address](#inet4_route_address) + :material-delete-clock: [inet6_route_address](#inet6_route_address) + :material-plus: [route_exclude_address](#route_address) + :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address) + :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) + :material-plus: [auto_redirect](#auto_redirect) + :material-plus: [route_address_set](#route_address_set) + :material-plus: [route_exclude_address_set](#route_address_set) + !!! quote "sing-box 1.9.0 中的更改" :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) @@ -23,26 +38,57 @@ icon: material/new-box "type": "tun", "tag": "tun-in", "interface_name": "tun0", - "inet4_address": "172.19.0.1/30", - "inet6_address": "fdfe:dcba:9876::1/126", + "address": [ + "172.18.0.1/30", + "fdfe:dcba:9876::1/126" + ], + // 已弃用 + "inet4_address": [ + "172.19.0.1/30" + ], + // 已弃用 + "inet6_address": [ + "fdfe:dcba:9876::1/126" + ], "mtu": 9000, "gso": false, "auto_route": true, "strict_route": true, + "auto_redirect": false, + "route_address": [ + "0.0.0.0/1", + "128.0.0.0/1", + "::/1", + "8000::/1" + ], + // 已弃用 "inet4_route_address": [ "0.0.0.0/1", "128.0.0.0/1" ], + // 已弃用 "inet6_route_address": [ "::/1", "8000::/1" ], + "route_exclude_address": [ + "192.168.0.0/16", + "fc00::/7" + ], + // 已弃用 "inet4_route_exclude_address": [ "192.168.0.0/16" ], + // 已弃用 "inet6_route_exclude_address": [ "fc00::/7" ], + "route_address_set": [ + "geoip-cloudflare" + ], + "route_exclude_address_set": [ + "geoip-cn" + ], "endpoint_independent_nat": false, "udp_timeout": "5m", "stack": "system", @@ -102,14 +148,30 @@ icon: material/new-box 虚拟设备名称,默认自动选择。 +#### address + +!!! question "自 sing-box 1.10.0 起" + +==必填== + +tun 接口的 IPv4 和 IPv6 前缀。 + #### inet4_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet4_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除. + ==必填== tun 接口的 IPv4 前缀。 #### inet6_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet6_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除. + tun 接口的 IPv6 前缀。 #### mtu @@ -145,9 +207,10 @@ tun 接口的 IPv6 前缀。 *在 Linux 中*: * 让不支持的网络无法到达 +* 使 ICMP 流量路由到 tun 而不是上游接口 * 将所有连接路由到 tun -它可以防止地址泄漏,并使 DNS 劫持在 Android 上工作。 +它可以防止 IP 地址泄漏,并使 DNS 劫持在 Android 上工作。 *在 Windows 中*: @@ -157,22 +220,94 @@ tun 接口的 IPv6 前缀。 它可能会使某些应用程序(如 VirtualBox)在某些情况下无法正常工作。 +#### auto_redirect + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux。 + +自动配置 iptables 以重定向 TCP 连接。 + +*在 Android 中*: + +仅转发本地 IPv4 连接。 要通过热点或中继共享您的 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。 + +*在 Linux 中*: + +带有 `auto_redirect `的 `auto_route` 现在可以在路由器上按预期工作,**无需干预**。 + +#### route_address + +!!! question "自 sing-box 1.10.0 起" + +设置到 Tun 的自定义路由。 + #### inet4_route_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除. + 启用 `auto_route` 时使用自定义路由而不是默认路由。 #### inet6_route_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除. + 启用 `auto_route` 时使用自定义路由而不是默认路由。 +#### route_exclude_address + +!!! question "自 sing-box 1.10.0 起" + +设置到 Tun 的排除自定义路由。 + #### inet4_route_exclude_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除. + 启用 `auto_route` 时排除自定义路由。 #### inet6_route_exclude_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除. + 启用 `auto_route` 时排除自定义路由。 +#### route_address_set + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 + +将指定规则集中的目标 IP CIDR 规则添加到防火墙。 +不匹配的流量将绕过 sing-box 路由。 + +与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。 + +#### route_exclude_address_set + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 + +将指定规则集中的目标 IP CIDR 规则添加到防火墙。 +匹配的流量将绕过 sing-box 路由。 + +与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。 + #### endpoint_independent_nat 启用独立于端点的 NAT。 @@ -211,6 +346,10 @@ TCP/IP 栈。 #### exclude_interface +!!! warning "" + + 当 `strict_route` 启用,到被排除接口的回程流量将不会被自动排除,因此也要添加它们(例:`br-lan` 与 `pppoe-wan`)。 + 排除路由的接口。 与 `include_interface` 冲突。 @@ -284,7 +423,7 @@ TCP/IP 栈。 !!! note "" - 在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**. + 在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**. 绕过代理的主机名列表。 diff --git a/docs/deprecated.md b/docs/deprecated.md index 439bf7e8f5..249bc492a2 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -6,6 +6,14 @@ icon: material/delete-alert ## 1.10.0 +#### TUN address fields are merged + +`inet4_address` and `inet6_address` are merged into `address`, +`inet4_route_address` and `inet6_route_address` are merged into `route_address`, +`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`. + +Old fields are deprecated and will be removed in sing-box 1.11.0. + #### Drop support for go1.18 and go1.19 Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile. diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index 76e7e7684c..6815e9fc8b 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -6,6 +6,14 @@ icon: material/delete-alert ## 1.10.0 +#### TUN 地址字段已合并 + +`inet4_address` 和 `inet6_address` 已合并为 `address`, +`inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`, +`inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。 + +旧字段已废弃,且将在 sing-box 1.11.0 中移除。 + #### 移除对 go1.18 和 go1.19 的支持 由于维护困难,sing-box 1.10.0 要求至少 Go 1.20 才能编译。 diff --git a/docs/migration.md b/docs/migration.md index 6ea0de4927..017ed2302e 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -2,6 +2,74 @@ icon: material/arrange-bring-forward --- +## 1.10.0 + +### TUN address fields are merged + +`inet4_address` and `inet6_address` are merged into `address`, +`inet4_route_address` and `inet6_route_address` are merged into `route_address`, +`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`. + +Old fields are deprecated and will be removed in sing-box 1.11.0. + +!!! info "References" + + [TUN](/configuration/inbound/tun/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "inbounds": [ + { + "type": "tun", + "inet4_address": "172.19.0.1/30", + "inet6_address": "fdfe:dcba:9876::1/126", + "inet4_route_address": [ + "0.0.0.0/1", + "128.0.0.0/1" + ], + "inet6_route_address": [ + "::/1", + "8000::/1" + ], + "inet4_route_exclude_address": [ + "192.168.0.0/16" + ], + "inet6_route_exclude_address": [ + "fc00::/7" + ] + } + ] + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "inbounds": [ + { + "type": "tun", + "address": [ + "172.19.0.1/30", + "fdfe:dcba:9876::1/126" + ], + "route_address": [ + "0.0.0.0/1", + "128.0.0.0/1", + "::/1", + "8000::/1" + ], + "route_exclude_address": [ + "192.168.0.0/16", + "fc00::/7" + ] + } + ] + } + ``` + ## 1.9.5 ### Bundle Identifier updates in Apple platform clients diff --git a/docs/migration.zh.md b/docs/migration.zh.md index 2cbbba6f4b..a8a0774045 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -2,6 +2,74 @@ icon: material/arrange-bring-forward --- +## 1.10.0 + +### TUN 地址字段已合并 + +`inet4_address` 和 `inet6_address` 已合并为 `address`, +`inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`, +`inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。 + +旧字段已废弃,且将在 sing-box 1.11.0 中移除。 + +!!! info "参考" + + [TUN](/zh/configuration/inbound/tun/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "inbounds": [ + { + "type": "tun", + "inet4_address": "172.19.0.1/30", + "inet6_address": "fdfe:dcba:9876::1/126", + "inet4_route_address": [ + "0.0.0.0/1", + "128.0.0.0/1" + ], + "inet6_route_address": [ + "::/1", + "8000::/1" + ], + "inet4_route_exclude_address": [ + "192.168.0.0/16" + ], + "inet6_route_exclude_address": [ + "fc00::/7" + ] + } + ] + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "inbounds": [ + { + "type": "tun", + "address": [ + "172.19.0.1/30", + "fdfe:dcba:9876::1/126" + ], + "route_address": [ + "0.0.0.0/1", + "128.0.0.0/1", + "::/1", + "8000::/1" + ], + "route_exclude_address": [ + "192.168.0.0/16", + "fc00::/7" + ] + } + ] + } + ``` + ## 1.9.5 ### Apple 平台客户端的 Bundle Identifier 更新 diff --git a/go.mod b/go.mod index 7f3610d48d..8f219f8df7 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/sagernet/sing-shadowtls v0.1.4 - github.com/sagernet/sing-tun v0.3.3 + github.com/sagernet/sing-tun v0.4.0-beta.16 github.com/sagernet/sing-vmess v0.1.12 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 github.com/sagernet/utls v1.5.4 @@ -44,8 +44,8 @@ require ( github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.23.0 - golang.org/x/net v0.25.0 + golang.org/x/crypto v0.24.0 + golang.org/x/net v0.26.0 golang.org/x/sys v0.25.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 google.golang.org/grpc v1.63.2 @@ -65,6 +65,7 @@ require ( github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/google/btree v1.1.2 // indirect + github.com/google/go-cmp v0.6.0 // 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 @@ -72,24 +73,28 @@ require ( github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/libdns/libdns v0.2.2 // indirect + github.com/mdlayher/netlink v1.7.2 // indirect + github.com/mdlayher/socket v0.4.1 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // 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.4.1 // indirect - github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba // indirect + github.com/sagernet/fswatch v0.1.1 // indirect + github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect + github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect - github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect + github.com/vishvananda/netns v0.0.4 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.18.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 40318265fe..b3d96a4594 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,7 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek 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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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= @@ -69,6 +70,10 @@ github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d h1:j9LtzkYstLFoNvXW824QQeN7Y26uPL5249kzWKbzO9U= github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts= github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= @@ -97,12 +102,16 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 h1:YbmpqPQEMdlk9oFSKYWRqVuu9qzNiOayIonKmv1gCXY= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1/go.mod h1:J2yAxTFPDjrDPhuAi9aWFz2L3ox9it4qAluBBbN0H5k= +github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= +github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= github.com/sagernet/gomobile v0.1.4 h1:WzX9ka+iHdupMgy2Vdich+OAt7TM8C2cZbIbzNjBrJY= github.com/sagernet/gomobile v0.1.4/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E= github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f h1:NkhuupzH5ch7b/Y/6ZHJWrnNLoiNnSJaow6DPb8VW2I= github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f/go.mod h1:KXmw+ouSJNOsuRpg4wgwwCQuunrGz4yoAqQjsLjc6N0= -github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba h1:EY5AS7CCtfmARNv2zXUOrsEMPFDGYxaw65JzA2p51Vk= -github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= +github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.47.0-beta.2 h1:1tCGWFOSaXIeuQaHrwOMJIYvlupjTcaVInGQw5ArULU= github.com/sagernet/quic-go v0.47.0-beta.2/go.mod h1:bLVKvElSEMNv7pu7SZHscW02TYigzQ5lQu3Nh4wNh8Q= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= @@ -122,8 +131,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wK github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= 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.3.3 h1:LZnQNmfGcNG2KPTPkLgc+Lo7k606QJVkPp2DnjriwUk= -github.com/sagernet/sing-tun v0.3.3/go.mod h1:DxLIyhjWU/HwGYoX0vNGg2c5QgTQIakphU1MuERR5tQ= +github.com/sagernet/sing-tun v0.4.0-beta.16 h1:05VdL5BZiKLQsDNrpdXMseSO1NwPfl9Y4o76PqAd9sY= +github.com/sagernet/sing-tun v0.4.0-beta.16/go.mod h1:81JwnnYw8X9W9XvmZetSTTiPgIE3SbAbnc+EHKwPJ5U= github.com/sagernet/sing-vmess v0.1.12 h1:2gFD8JJb+eTFMoa8FIVMnknEi+vCSfaiTXTfEYAYAPg= github.com/sagernet/sing-vmess v0.1.12/go.mod h1:luTSsfyBGAc9VhtCqwjR+dt1QgqBhuYBCONB/POhF8I= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= @@ -146,8 +155,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= @@ -163,20 +172,19 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/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= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -187,7 +195,7 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= 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.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= @@ -195,8 +203,8 @@ golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.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.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= diff --git a/inbound/tun.go b/inbound/tun.go index 7bd700d348..cb6a02c301 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -3,6 +3,9 @@ package inbound import ( "context" "net" + "net/netip" + "os" + "runtime" "strconv" "strings" "time" @@ -19,27 +22,91 @@ import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ranges" + "github.com/sagernet/sing/common/x/list" + + "go4.org/netipx" ) var _ adapter.Inbound = (*Tun)(nil) type Tun struct { - tag string - ctx context.Context - router adapter.Router - logger log.ContextLogger - inboundOptions option.InboundOptions - tunOptions tun.Options - endpointIndependentNat bool - udpTimeout int64 - stack string - tunIf tun.Tun - tunStack tun.Stack - platformInterface platform.Interface - platformOptions option.TunPlatformOptions + tag string + ctx context.Context + router adapter.Router + logger log.ContextLogger + inboundOptions option.InboundOptions + tunOptions tun.Options + endpointIndependentNat bool + udpTimeout int64 + stack string + tunIf tun.Tun + tunStack tun.Stack + platformInterface platform.Interface + platformOptions option.TunPlatformOptions + autoRedirect tun.AutoRedirect + routeRuleSet []adapter.RuleSet + routeRuleSetCallback []*list.Element[adapter.RuleSetUpdateCallback] + routeExcludeRuleSet []adapter.RuleSet + routeExcludeRuleSetCallback []*list.Element[adapter.RuleSetUpdateCallback] + routeAddressSet []*netipx.IPSet + routeExcludeAddressSet []*netipx.IPSet } func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*Tun, error) { + address := options.Address + //nolint:staticcheck + //goland:noinspection GoDeprecation + if len(options.Inet4Address) > 0 { + address = append(address, options.Inet4Address...) + } + //nolint:staticcheck + //goland:noinspection GoDeprecation + if len(options.Inet6Address) > 0 { + address = append(address, options.Inet6Address...) + } + inet4Address := common.Filter(address, func(it netip.Prefix) bool { + return it.Addr().Is4() + }) + inet6Address := common.Filter(address, func(it netip.Prefix) bool { + return it.Addr().Is6() + }) + + routeAddress := options.RouteAddress + //nolint:staticcheck + //goland:noinspection GoDeprecation + if len(options.Inet4RouteAddress) > 0 { + routeAddress = append(routeAddress, options.Inet4RouteAddress...) + } + //nolint:staticcheck + //goland:noinspection GoDeprecation + if len(options.Inet6RouteAddress) > 0 { + routeAddress = append(routeAddress, options.Inet6RouteAddress...) + } + inet4RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool { + return it.Addr().Is4() + }) + inet6RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool { + return it.Addr().Is6() + }) + + routeExcludeAddress := options.RouteExcludeAddress + //nolint:staticcheck + //goland:noinspection GoDeprecation + if len(options.Inet4RouteExcludeAddress) > 0 { + routeExcludeAddress = append(routeExcludeAddress, options.Inet4RouteExcludeAddress...) + } + //nolint:staticcheck + //goland:noinspection GoDeprecation + if len(options.Inet6RouteExcludeAddress) > 0 { + routeExcludeAddress = append(routeExcludeAddress, options.Inet6RouteExcludeAddress...) + } + inet4RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool { + return it.Addr().Is4() + }) + inet6RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool { + return it.Addr().Is6() + }) + tunMTU := options.MTU if tunMTU == 0 { tunMTU = 9000 @@ -50,9 +117,9 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger } else { udpTimeout = C.UDPTimeout } + var err error includeUID := uidToRange(options.IncludeUID) if len(options.IncludeUIDRange) > 0 { - var err error includeUID, err = parseRange(includeUID, options.IncludeUIDRange) if err != nil { return nil, E.Cause(err, "parse include_uid_range") @@ -60,13 +127,30 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger } excludeUID := uidToRange(options.ExcludeUID) if len(options.ExcludeUIDRange) > 0 { - var err error excludeUID, err = parseRange(excludeUID, options.ExcludeUIDRange) if err != nil { return nil, E.Cause(err, "parse exclude_uid_range") } } - return &Tun{ + + tableIndex := options.IPRoute2TableIndex + if tableIndex == 0 { + tableIndex = tun.DefaultIPRoute2TableIndex + } + ruleIndex := options.IPRoute2RuleIndex + if ruleIndex == 0 { + ruleIndex = tun.DefaultIPRoute2RuleIndex + } + inputMark := options.AutoRedirectInputMark + if inputMark == 0 { + inputMark = tun.DefaultAutoRedirectInputMark + } + outputMark := options.AutoRedirectOutputMark + if outputMark == 0 { + outputMark = tun.DefaultAutoRedirectOutputMark + } + + inbound := &Tun{ tag: tag, ctx: ctx, router: router, @@ -76,30 +160,83 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger Name: options.InterfaceName, MTU: tunMTU, GSO: options.GSO, - Inet4Address: options.Inet4Address, - Inet6Address: options.Inet6Address, + Inet4Address: inet4Address, + Inet6Address: inet6Address, AutoRoute: options.AutoRoute, + IPRoute2TableIndex: tableIndex, + IPRoute2RuleIndex: ruleIndex, + AutoRedirectInputMark: inputMark, + AutoRedirectOutputMark: outputMark, StrictRoute: options.StrictRoute, IncludeInterface: options.IncludeInterface, ExcludeInterface: options.ExcludeInterface, - Inet4RouteAddress: options.Inet4RouteAddress, - Inet6RouteAddress: options.Inet6RouteAddress, - Inet4RouteExcludeAddress: options.Inet4RouteExcludeAddress, - Inet6RouteExcludeAddress: options.Inet6RouteExcludeAddress, + Inet4RouteAddress: inet4RouteAddress, + Inet6RouteAddress: inet6RouteAddress, + Inet4RouteExcludeAddress: inet4RouteExcludeAddress, + Inet6RouteExcludeAddress: inet6RouteExcludeAddress, IncludeUID: includeUID, ExcludeUID: excludeUID, IncludeAndroidUser: options.IncludeAndroidUser, IncludePackage: options.IncludePackage, ExcludePackage: options.ExcludePackage, InterfaceMonitor: router.InterfaceMonitor(), - TableIndex: 2022, }, endpointIndependentNat: options.EndpointIndependentNat, udpTimeout: int64(udpTimeout.Seconds()), stack: options.Stack, platformInterface: platformInterface, platformOptions: common.PtrValueOrDefault(options.Platform), - }, nil + } + if options.AutoRedirect { + if !options.AutoRoute { + return nil, E.New("`auto_route` is required by `auto_redirect`") + } + disableNFTables, dErr := strconv.ParseBool(os.Getenv("DISABLE_NFTABLES")) + inbound.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{ + TunOptions: &inbound.tunOptions, + Context: ctx, + Handler: inbound, + Logger: logger, + NetworkMonitor: router.NetworkMonitor(), + InterfaceFinder: router.InterfaceFinder(), + TableName: "sing-box", + DisableNFTables: dErr == nil && disableNFTables, + RouteAddressSet: &inbound.routeAddressSet, + RouteExcludeAddressSet: &inbound.routeExcludeAddressSet, + }) + if err != nil { + return nil, E.Cause(err, "initialize auto-redirect") + } + if runtime.GOOS != "android" { + var markMode bool + for _, routeAddressSet := range options.RouteAddressSet { + ruleSet, loaded := router.RuleSet(routeAddressSet) + if !loaded { + return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet) + } + ruleSet.IncRef() + inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet) + markMode = true + } + for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet { + ruleSet, loaded := router.RuleSet(routeExcludeAddressSet) + if !loaded { + return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet) + } + ruleSet.IncRef() + inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet) + markMode = true + } + if markMode { + inbound.tunOptions.AutoRedirectMarkMode = true + err = router.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark) + if err != nil { + return nil, err + } + } + } + } + return inbound, nil } func uidToRange(uidList option.Listable[uint32]) []ranges.Range[uint32] { @@ -121,11 +258,11 @@ func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges. } var start, end uint64 var err error - start, err = strconv.ParseUint(uidRange[:subIndex], 10, 32) + start, err = strconv.ParseUint(uidRange[:subIndex], 0, 32) if err != nil { return nil, E.Cause(err, "parse range start") } - end, err = strconv.ParseUint(uidRange[subIndex+1:], 10, 32) + end, err = strconv.ParseUint(uidRange[subIndex+1:], 0, 32) if err != nil { return nil, E.Cause(err, "parse range end") } @@ -200,10 +337,58 @@ func (t *Tun) Start() error { return nil } +func (t *Tun) PostStart() error { + monitor := taskmonitor.New(t.logger, C.StartTimeout) + if t.autoRedirect != nil { + t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) + for _, routeRuleSet := range t.routeRuleSet { + ipSets := routeRuleSet.ExtractIPSet() + if len(ipSets) == 0 { + t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name()) + } + t.routeAddressSet = append(t.routeAddressSet, ipSets...) + } + t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) + for _, routeExcludeRuleSet := range t.routeExcludeRuleSet { + ipSets := routeExcludeRuleSet.ExtractIPSet() + if len(ipSets) == 0 { + t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name()) + } + t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...) + } + monitor.Start("initialize auto-redirect") + err := t.autoRedirect.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "auto-redirect") + } + for _, routeRuleSet := range t.routeRuleSet { + t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet)) + routeRuleSet.DecRef() + } + for _, routeExcludeRuleSet := range t.routeExcludeRuleSet { + t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet)) + routeExcludeRuleSet.DecRef() + } + t.routeAddressSet = nil + t.routeExcludeAddressSet = nil + } + return nil +} + +func (t *Tun) updateRouteAddressSet(it adapter.RuleSet) { + t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) + t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) + t.autoRedirect.UpdateRouteAddressSet() + t.routeAddressSet = nil + t.routeExcludeAddressSet = nil +} + func (t *Tun) Close() error { return common.Close( t.tunStack, t.tunIf, + t.autoRedirect, ) } @@ -215,7 +400,11 @@ func (t *Tun) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata metadata.Source = upstreamMetadata.Source metadata.Destination = upstreamMetadata.Destination metadata.InboundOptions = t.inboundOptions - t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + if upstreamMetadata.Protocol != "" { + t.logger.InfoContext(ctx, "inbound ", upstreamMetadata.Protocol, " connection from ", metadata.Source) + } else { + t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + } t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) err := t.router.RouteConnection(ctx, conn, metadata) if err != nil { diff --git a/option/tun.go b/option/tun.go index ac66a8061c..cbc73e7d4b 100644 --- a/option/tun.go +++ b/option/tun.go @@ -3,29 +3,46 @@ package option import "net/netip" type TunInboundOptions struct { - InterfaceName string `json:"interface_name,omitempty"` - MTU uint32 `json:"mtu,omitempty"` - GSO bool `json:"gso,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"` + InterfaceName string `json:"interface_name,omitempty"` + MTU uint32 `json:"mtu,omitempty"` + GSO bool `json:"gso,omitempty"` + Address Listable[netip.Prefix] `json:"address,omitempty"` + AutoRoute bool `json:"auto_route,omitempty"` + IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"` + IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"` + AutoRedirect bool `json:"auto_redirect,omitempty"` + AutoRedirectInputMark uint32 `json:"auto_redirect_input_mark,omitempty"` + AutoRedirectOutputMark uint32 `json:"auto_redirect_output_mark,omitempty"` + StrictRoute bool `json:"strict_route,omitempty"` + RouteAddress Listable[netip.Prefix] `json:"route_address,omitempty"` + RouteAddressSet Listable[string] `json:"route_address_set,omitempty"` + RouteExcludeAddress Listable[netip.Prefix] `json:"route_exclude_address,omitempty"` + RouteExcludeAddressSet Listable[string] `json:"route_exclude_address_set,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 UDPTimeoutCompat `json:"udp_timeout,omitempty"` + Stack string `json:"stack,omitempty"` + Platform *TunPlatformOptions `json:"platform,omitempty"` + InboundOptions + + // Deprecated: merged to Address + Inet4Address Listable[netip.Prefix] `json:"inet4_address,omitempty"` + // Deprecated: merged to Address + Inet6Address Listable[netip.Prefix] `json:"inet6_address,omitempty"` + // Deprecated: merged to RouteAddress + Inet4RouteAddress Listable[netip.Prefix] `json:"inet4_route_address,omitempty"` + // Deprecated: merged to RouteAddress + Inet6RouteAddress Listable[netip.Prefix] `json:"inet6_route_address,omitempty"` + // Deprecated: merged to RouteExcludeAddress Inet4RouteExcludeAddress Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"` + // Deprecated: merged to RouteExcludeAddress 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 UDPTimeoutCompat `json:"udp_timeout,omitempty"` - Stack string `json:"stack,omitempty"` - Platform *TunPlatformOptions `json:"platform,omitempty"` - InboundOptions } diff --git a/route/router.go b/route/router.go index 3a89568518..3e0e90db88 100644 --- a/route/router.go +++ b/route/router.go @@ -83,6 +83,7 @@ type Router struct { autoDetectInterface bool defaultInterface string defaultMark uint32 + autoRedirectOutputMark uint32 networkMonitor tun.NetworkUpdateMonitor interfaceMonitor tun.DefaultInterfaceMonitor packageManager tun.PackageManager @@ -533,7 +534,10 @@ func (r *Router) Start() error { if C.IsAndroid && r.platformInterface == nil { monitor.Start("initialize package manager") - packageManager, err := tun.NewPackageManager(r) + packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{ + Callback: r, + Logger: r.logger, + }) monitor.Finish() if err != nil { return E.Cause(err, "create package manager") @@ -736,10 +740,26 @@ func (r *Router) PostStart() error { return E.Cause(err, "initialize rule[", i, "]") } } + for _, ruleSet := range r.ruleSets { + monitor.Start("post start rule_set[", ruleSet.Name(), "]") + err := ruleSet.PostStart() + monitor.Finish() + if err != nil { + return E.Cause(err, "post start rule_set[", ruleSet.Name(), "]") + } + } r.started = true return nil } +func (r *Router) Cleanup() error { + for _, ruleSet := range r.ruleSetMap { + ruleSet.Cleanup() + } + runtime.GC() + return nil +} + func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { outbound, loaded := r.outboundByTag[tag] return outbound, loaded @@ -993,15 +1013,15 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m } if metadata.InboundOptions.SniffEnabled { sniffMetadata, _ := sniff.PeekPacket( - ctx, - buffer.Bytes(), - sniff.DomainNameQuery, - sniff.QUICClientHello, - sniff.STUNMessage, - sniff.UTP, - sniff.UDPTracker, - sniff.DTLSRecord, - ) + ctx, + buffer.Bytes(), + sniff.DomainNameQuery, + sniff.QUICClientHello, + sniff.STUNMessage, + sniff.UTP, + sniff.UDPTracker, + sniff.DTLSRecord, + ) if sniffMetadata != nil { metadata.Protocol = sniffMetadata.Protocol metadata.Domain = sniffMetadata.Domain @@ -1167,6 +1187,18 @@ func (r *Router) AutoDetectInterfaceFunc() control.Func { } } +func (r *Router) RegisterAutoRedirectOutputMark(mark uint32) error { + if r.autoRedirectOutputMark > 0 { + return E.New("only one auto-redirect can be configured") + } + r.autoRedirectOutputMark = mark + return nil +} + +func (r *Router) AutoRedirectOutputMark() uint32 { + return r.autoRedirectOutputMark +} + func (r *Router) DefaultInterface() string { return r.defaultInterface } diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go index 482a9c7b45..4ecf8c18f0 100644 --- a/route/rule_item_rule_set.go +++ b/route/rule_item_rule_set.go @@ -32,6 +32,7 @@ func (r *RuleSetItem) Start() error { if !loaded { return E.New("rule-set not found: ", tag) } + ruleSet.IncRef() r.setList = append(r.setList, ruleSet) } return nil diff --git a/route/rule_set.go b/route/rule_set.go index fda3d1056c..7c02aa7a82 100644 --- a/route/rule_set.go +++ b/route/rule_set.go @@ -9,10 +9,13 @@ import ( "github.com/sagernet/sing-box/adapter" 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" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + + "go4.org/netipx" ) func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { @@ -26,6 +29,24 @@ func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.Contex } } +func extractIPSetFromRule(rawRule adapter.HeadlessRule) []*netipx.IPSet { + switch rule := rawRule.(type) { + case *DefaultHeadlessRule: + return common.FlatMap(rule.destinationIPCIDRItems, func(rawItem RuleItem) []*netipx.IPSet { + switch item := rawItem.(type) { + case *IPCIDRItem: + return []*netipx.IPSet{item.ipSet} + default: + return nil + } + }) + case *LogicalHeadlessRule: + return common.FlatMap(rule.rules, extractIPSetFromRule) + default: + panic("unexpected rule type") + } +} + var _ adapter.RuleSetStartContext = (*RuleSetStartContext)(nil) type RuleSetStartContext struct { diff --git a/route/rule_set_local.go b/route/rule_set_local.go index 07839c65b3..87a393c27d 100644 --- a/route/rule_set_local.go +++ b/route/rule_set_local.go @@ -9,17 +9,24 @@ import ( "github.com/sagernet/sing-box/common/srs" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/atomic" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service/filemanager" + + "go4.org/netipx" ) var _ adapter.RuleSet = (*LocalRuleSet)(nil) type LocalRuleSet struct { + tag string rules []adapter.HeadlessRule metadata adapter.RuleSetMetadata + refs atomic.Int32 } func NewLocalRuleSet(ctx context.Context, router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { @@ -59,16 +66,11 @@ func NewLocalRuleSet(ctx context.Context, router adapter.Router, options option. metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) - return &LocalRuleSet{rules, metadata}, nil + return &LocalRuleSet{tag: options.Tag, rules: rules, metadata: metadata}, 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) Name() string { + return s.tag } func (s *LocalRuleSet) String() string { @@ -79,10 +81,51 @@ func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.Ru return nil } +func (s *LocalRuleSet) PostStart() error { + return nil +} + func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata { return s.metadata } +func (s *LocalRuleSet) ExtractIPSet() []*netipx.IPSet { + return common.FlatMap(s.rules, extractIPSetFromRule) +} + +func (s *LocalRuleSet) IncRef() { + s.refs.Add(1) +} + +func (s *LocalRuleSet) DecRef() { + if s.refs.Add(-1) < 0 { + panic("rule-set: negative refs") + } +} + +func (s *LocalRuleSet) Cleanup() { + if s.refs.Load() == 0 { + s.rules = nil + } +} + +func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { + return nil +} + +func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { +} + func (s *LocalRuleSet) Close() error { + s.rules = nil return nil } + +func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go index 8389c2f46b..bf0cfe204c 100644 --- a/route/rule_set_remote.go +++ b/route/rule_set_remote.go @@ -8,20 +8,26 @@ import ( "net/http" "runtime" "strings" + "sync" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/srs" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/atomic" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" "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/x/list" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" + + "go4.org/netipx" ) var _ adapter.RuleSet = (*RemoteRuleSet)(nil) @@ -40,6 +46,9 @@ type RemoteRuleSet struct { lastEtag string updateTicker *time.Ticker pauseManager pause.Manager + callbackAccess sync.Mutex + callbacks list.List[adapter.RuleSetUpdateCallback] + refs atomic.Int32 } func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { @@ -61,13 +70,8 @@ func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger. } } -func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { - for _, rule := range s.rules { - if rule.Match(metadata) { - return true - } - } - return false +func (s *RemoteRuleSet) Name() string { + return s.options.Tag } func (s *RemoteRuleSet) String() string { @@ -108,6 +112,10 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.R } } s.updateTicker = time.NewTicker(s.updateInterval) + return nil +} + +func (s *RemoteRuleSet) PostStart() error { go s.loopUpdate() return nil } @@ -116,6 +124,38 @@ func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata { return s.metadata } +func (s *RemoteRuleSet) ExtractIPSet() []*netipx.IPSet { + return common.FlatMap(s.rules, extractIPSetFromRule) +} + +func (s *RemoteRuleSet) IncRef() { + s.refs.Add(1) +} + +func (s *RemoteRuleSet) DecRef() { + if s.refs.Add(-1) < 0 { + panic("rule-set: negative refs") + } +} + +func (s *RemoteRuleSet) Cleanup() { + if s.refs.Load() == 0 { + s.rules = nil + } +} + +func (s *RemoteRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { + s.callbackAccess.Lock() + defer s.callbackAccess.Unlock() + return s.callbacks.PushBack(callback) +} + +func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { + s.callbackAccess.Lock() + defer s.callbackAccess.Unlock() + s.callbacks.Remove(element) +} + func (s *RemoteRuleSet) loadBytes(content []byte) error { var ( plainRuleSet option.PlainRuleSet @@ -148,6 +188,12 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) s.rules = rules + s.callbackAccess.Lock() + callbacks := s.callbacks.Array() + s.callbackAccess.Unlock() + for _, callback := range callbacks { + callback(s) + } return nil } @@ -156,6 +202,8 @@ func (s *RemoteRuleSet) loopUpdate() { err := s.fetchOnce(s.ctx, nil) if err != nil { s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } else if s.refs.Load() == 0 { + s.rules = nil } } for { @@ -168,6 +216,8 @@ func (s *RemoteRuleSet) loopUpdate() { err := s.fetchOnce(s.ctx, nil) if err != nil { s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } else if s.refs.Load() == 0 { + s.rules = nil } } } @@ -253,7 +303,17 @@ func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext adapter.Rule } func (s *RemoteRuleSet) Close() error { + s.rules = nil s.updateTicker.Stop() s.cancel() return nil } + +func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} From 1cdaea49b6bfc182d500703321aa9dfb783abf2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 7 Jun 2024 15:59:56 +0800 Subject: [PATCH 11/25] Improve base DNS transports & Minor fixes --- go.mod | 14 +++++++------- go.sum | 30 +++++++++++++++--------------- route/router_dns.go | 7 ------- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index 8f219f8df7..ffcd0d796d 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/logrusorgru/aurora v2.0.3+incompatible github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d github.com/mholt/acmez v1.2.0 - github.com/miekg/dns v1.1.59 + github.com/miekg/dns v1.1.61 github.com/ooni/go-libtor v1.1.8 github.com/oschwald/maxminddb-golang v1.12.0 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a @@ -28,7 +28,7 @@ require ( github.com/sagernet/quic-go v0.47.0-beta.2 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 github.com/sagernet/sing v0.5.0-beta.2 - github.com/sagernet/sing-dns v0.2.3 + github.com/sagernet/sing-dns v0.3.0-beta.14 github.com/sagernet/sing-mux v0.2.0 github.com/sagernet/sing-quic v0.3.0-beta.3 github.com/sagernet/sing-shadowsocks v0.2.7 @@ -44,8 +44,8 @@ require ( github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.24.0 - golang.org/x/net v0.26.0 + golang.org/x/crypto v0.25.0 + golang.org/x/net v0.27.0 golang.org/x/sys v0.25.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 google.golang.org/grpc v1.63.2 @@ -89,12 +89,12 @@ require ( github.com/vishvananda/netns v0.0.4 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect - golang.org/x/mod v0.18.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.19.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.22.0 // indirect + golang.org/x/tools v0.23.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index b3d96a4594..6deefbe260 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d h1:j9LtzkYstLFoNv github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts= github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE= -github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= -github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= +github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= +github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= 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.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= @@ -119,8 +119,8 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4Wk github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing v0.5.0-beta.2 h1:V12EpwtsgYo5OLGjAiGoJobDJZeUsKv0b5y+yGAM6W0= github.com/sagernet/sing v0.5.0-beta.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= -github.com/sagernet/sing-dns v0.2.3 h1:YzeBUn2tR38F7HtvGEQ0kLRLmZWMEgi/+7wqa4Twb1k= -github.com/sagernet/sing-dns v0.2.3/go.mod h1:BJpJv6XLnrUbSyIntOT6DG9FW0f4fETmPAHvNjOprLg= +github.com/sagernet/sing-dns v0.3.0-beta.14 h1:/s+fJzYKsvLaNDt/2rjpsrDcN8wmCO2JbX6OFrl8Nww= +github.com/sagernet/sing-dns v0.3.0-beta.14/go.mod h1:rscgSr5ixOPk8XM9ZMLuMXCyldEQ1nLvdl0nfv+lp00= github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo= github.com/sagernet/sing-mux v0.2.0/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ= github.com/sagernet/sing-quic v0.3.0-beta.3 h1:8S98VXZxtSiOqVCFbCNbMEvKDPhOF/VNBYMjVC3xMhw= @@ -172,16 +172,16 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= -golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -195,7 +195,7 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= 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.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= @@ -203,8 +203,8 @@ golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.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.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= diff --git a/route/router_dns.go b/route/router_dns.go index e0055009b0..51994072ab 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -8,7 +8,6 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-dns" "github.com/sagernet/sing/common/cache" E "github.com/sagernet/sing/common/exceptions" @@ -125,12 +124,10 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er for { var ( dnsCtx context.Context - cancel context.CancelFunc addressLimit bool ) dnsCtx, transport, strategy, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message)) - dnsCtx, cancel = context.WithTimeout(dnsCtx, C.DNSTimeout) if rule != nil && rule.WithAddressLimit() { addressLimit = true response, err = r.dnsClient.ExchangeWithResponseCheck(dnsCtx, transport, message, strategy, func(response *mDNS.Msg) bool { @@ -141,7 +138,6 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er addressLimit = false response, err = r.dnsClient.Exchange(dnsCtx, transport, message, strategy) } - cancel() var rejected bool if err != nil { if errors.Is(err, dns.ErrResponseRejectedCached) { @@ -206,7 +202,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS for { var ( dnsCtx context.Context - cancel context.CancelFunc addressLimit bool ) metadata.ResetRuleCache() @@ -215,7 +210,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS if strategy == dns.DomainStrategyAsIS { strategy = transportStrategy } - dnsCtx, cancel = context.WithTimeout(dnsCtx, C.DNSTimeout) if rule != nil && rule.WithAddressLimit() { addressLimit = true responseAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, strategy, func(responseAddrs []netip.Addr) bool { @@ -226,7 +220,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS addressLimit = false responseAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, strategy) } - cancel() if err != nil { if errors.Is(err, dns.ErrResponseRejectedCached) { r.dnsLogger.DebugContext(ctx, "response rejected for ", domain, " (cached)") From 7a2cd77798dd159bc82bf741059eb3ac1ce1e8ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 22 Jun 2024 14:11:49 +0800 Subject: [PATCH 12/25] Add custom options for TUN `auto-route` and `auto-redirect` --- docs/configuration/inbound/tun.md | 82 +++++++++++++++++++++------- docs/configuration/inbound/tun.zh.md | 70 +++++++++++++++++++----- inbound/tun.go | 4 +- option/tun.go | 36 +++++++++++- 4 files changed, 153 insertions(+), 39 deletions(-) diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 1e2bf40058..812b20d163 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -13,7 +13,11 @@ icon: material/new-box :material-plus: [route_exclude_address](#route_address) :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address) :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) + :material-plus: [iproute2_table_index](#iproute2_table_index) + :material-plus: [iproute2_rule_index](#iproute2_table_index) :material-plus: [auto_redirect](#auto_redirect) + :material-plus: [auto_redirect_input_mark](#auto_redirect_input_mark) + :material-plus: [auto_redirect_output_mark](#auto_redirect_output_mark) :material-plus: [route_address_set](#route_address_set) :material-plus: [route_exclude_address_set](#route_address_set) @@ -53,8 +57,12 @@ icon: material/new-box "mtu": 9000, "gso": false, "auto_route": true, - "strict_route": true, + "iproute2_table_index": 2022, + "iproute2_rule_index": 9000, "auto_redirect": false, + "auto_redirect_input_mark": "0x2023", + "auto_redirect_output_mark": "0x2024", + "strict_route": true, "route_address": [ "0.0.0.0/1", "128.0.0.0/1", @@ -129,8 +137,8 @@ icon: material/new-box "match_domain": [] } }, - - ... // Listen Fields + ... + // Listen Fields } ``` @@ -180,7 +188,7 @@ The maximum transmission unit. !!! quote "" - Only supported on Linux. + Only supported on Linux with `auto_route` enabled. Enable generic segmentation offload. @@ -196,24 +204,21 @@ Set the default route to the Tun. By default, VPN takes precedence over tun. To make tun go through VPN, enable `route.override_android_vpn`. -#### strict_route +#### iproute2_table_index -Enforce strict routing rules when `auto_route` is enabled: +!!! question "Since sing-box 1.10.0" -*In Linux*: +Linux iproute2 table index generated by `auto_route`. -* Let unsupported network unreachable -* Make ICMP traffic route to tun instead of upstream interfaces -* Route all connections to tun +`2022` is used by default. -It prevents IP address leaks and makes DNS hijacking work on Android. +#### iproute2_rule_index -*In Windows*: +!!! question "Since sing-box 1.10.0" -* Add firewall rules to prevent DNS leak caused by - Windows' [ordinary multihomed DNS resolution behavior](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) +Linux iproute2 rule start index generated by `auto_route`. -It may prevent some applications (such as VirtualBox) from working properly in certain situations. +`9000` is used by default. #### auto_redirect @@ -234,6 +239,41 @@ use [VPNHotspot](https://github.com/Mygod/VPNHotspot). `auto_route` with `auto_redirect` now works as expected on routers **without intervention**. +#### auto_redirect_input_mark + +!!! question "Since sing-box 1.10.0" + +Connection input mark used by `route_address_set` and `route_exclude_address_set`. + +`0x2023` is used by default. + +#### auto_redirect_output_mark + +!!! question "Since sing-box 1.10.0" + +Connection output mark used by `route_address_set` and `route_exclude_address_set`. + +`0x2024` is used by default. + +#### strict_route + +Enforce strict routing rules when `auto_route` is enabled: + +*In Linux*: + +* Let unsupported network unreachable +* Make ICMP traffic route to tun instead of upstream interfaces +* Route all connections to tun + +It prevents IP address leaks and makes DNS hijacking work on Android. + +*In Windows*: + +* Add firewall rules to prevent DNS leak caused by + Windows' [ordinary multihomed DNS resolution behavior](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) + +It may prevent some applications (such as VirtualBox) from working properly in certain situations. + #### route_address !!! question "Since sing-box 1.10.0" @@ -244,7 +284,8 @@ Use custom routes instead of default when `auto_route` is enabled. !!! failure "Deprecated in sing-box 1.10.0" - `inet4_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) instead. +`inet4_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) +instead. Use custom routes instead of default when `auto_route` is enabled. @@ -252,7 +293,8 @@ Use custom routes instead of default when `auto_route` is enabled. !!! failure "Deprecated in sing-box 1.10.0" - `inet6_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) instead. +`inet6_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) +instead. Use custom routes instead of default when `auto_route` is enabled. @@ -266,7 +308,8 @@ Exclude custom routes when `auto_route` is enabled. !!! failure "Deprecated in sing-box 1.10.0" - `inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_exclude_address](#route_exclude_address) instead. +`inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please +use [route_exclude_address](#route_exclude_address) instead. Exclude custom routes when `auto_route` is enabled. @@ -274,7 +317,8 @@ Exclude custom routes when `auto_route` is enabled. !!! failure "Deprecated in sing-box 1.10.0" - `inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_exclude_address](#route_exclude_address) instead. +`inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please +use [route_exclude_address](#route_exclude_address) instead. Exclude custom routes when `auto_route` is enabled. diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index 5b1d35afb5..bff493d4f9 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -12,8 +12,12 @@ icon: material/new-box :material-delete-clock: [inet6_route_address](#inet6_route_address) :material-plus: [route_exclude_address](#route_address) :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address) - :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) + :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) + :material-plus: [iproute2_table_index](#iproute2_table_index) + :material-plus: [iproute2_rule_index](#iproute2_table_index) :material-plus: [auto_redirect](#auto_redirect) + :material-plus: [auto_redirect_input_mark](#auto_redirect_input_mark) + :material-plus: [auto_redirect_output_mark](#auto_redirect_output_mark) :material-plus: [route_address_set](#route_address_set) :material-plus: [route_exclude_address_set](#route_address_set) @@ -53,8 +57,12 @@ icon: material/new-box "mtu": 9000, "gso": false, "auto_route": true, - "strict_route": true, + "iproute2_table_index": 2022, + "iproute2_rule_index": 9000, "auto_redirect": false, + "auto_redirect_input_mark": "0x2023", + "auto_redirect_output_mark": "0x2024", + "strict_route": true, "route_address": [ "0.0.0.0/1", "128.0.0.0/1", @@ -200,25 +208,21 @@ tun 接口的 IPv6 前缀。 VPN 默认优先于 tun。要使 tun 经过 VPN,启用 `route.override_android_vpn`。 -#### strict_route +#### iproute2_table_index -启用 `auto_route` 时执行严格的路由规则。 +!!! question "自 sing-box 1.10.0 起" -*在 Linux 中*: +`auto_route` 生成的 iproute2 路由表索引。 -* 让不支持的网络无法到达 -* 使 ICMP 流量路由到 tun 而不是上游接口 -* 将所有连接路由到 tun +默认使用 `2022`。 -它可以防止 IP 地址泄漏,并使 DNS 劫持在 Android 上工作。 +#### iproute2_rule_index -*在 Windows 中*: +!!! question "自 sing-box 1.10.0 起" -* 添加防火墙规则以阻止 Windows - 的 [普通多宿主 DNS 解析行为](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) - 造成的 DNS 泄露 +`auto_route` 生成的 iproute2 规则起始索引。 -它可能会使某些应用程序(如 VirtualBox)在某些情况下无法正常工作。 +默认使用 `9000`。 #### auto_redirect @@ -226,7 +230,7 @@ tun 接口的 IPv6 前缀。 !!! quote "" - 仅支持 Linux。 + 仅支持 Linux,且需要 `auto_route` 已启用。 自动配置 iptables 以重定向 TCP 连接。 @@ -238,6 +242,42 @@ tun 接口的 IPv6 前缀。 带有 `auto_redirect `的 `auto_route` 现在可以在路由器上按预期工作,**无需干预**。 +#### auto_redirect_input_mark + +!!! question "自 sing-box 1.10.0 起" + +`route_address_set` 和 `route_exclude_address_set` 使用的连接输入标记。 + +默认使用 `0x2023`。 + +#### auto_redirect_output_mark + +!!! question "自 sing-box 1.10.0 起" + +`route_address_set` 和 `route_exclude_address_set` 使用的连接输出标记。 + +默认使用 `0x2024`。 + +#### strict_route + +启用 `auto_route` 时执行严格的路由规则。 + +*在 Linux 中*: + +* 让不支持的网络无法到达 +* 使 ICMP 流量路由到 tun 而不是上游接口 +* 将所有连接路由到 tun + +它可以防止 IP 地址泄漏,并使 DNS 劫持在 Android 上工作。 + +*在 Windows 中*: + +* 添加防火墙规则以阻止 Windows + 的 [普通多宿主 DNS 解析行为](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) + 造成的 DNS 泄露 + +它可能会使某些应用程序(如 VirtualBox)在某些情况下无法正常工作。 + #### route_address !!! question "自 sing-box 1.10.0 起" diff --git a/inbound/tun.go b/inbound/tun.go index cb6a02c301..6cb65de249 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -141,11 +141,11 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger if ruleIndex == 0 { ruleIndex = tun.DefaultIPRoute2RuleIndex } - inputMark := options.AutoRedirectInputMark + inputMark := uint32(options.AutoRedirectInputMark) if inputMark == 0 { inputMark = tun.DefaultAutoRedirectInputMark } - outputMark := options.AutoRedirectOutputMark + outputMark := uint32(options.AutoRedirectOutputMark) if outputMark == 0 { outputMark = tun.DefaultAutoRedirectOutputMark } diff --git a/option/tun.go b/option/tun.go index cbc73e7d4b..dbb1bfeae0 100644 --- a/option/tun.go +++ b/option/tun.go @@ -1,6 +1,13 @@ package option -import "net/netip" +import ( + "net/netip" + "strconv" + + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json" +) type TunInboundOptions struct { InterfaceName string `json:"interface_name,omitempty"` @@ -11,8 +18,8 @@ type TunInboundOptions struct { IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"` IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"` AutoRedirect bool `json:"auto_redirect,omitempty"` - AutoRedirectInputMark uint32 `json:"auto_redirect_input_mark,omitempty"` - AutoRedirectOutputMark uint32 `json:"auto_redirect_output_mark,omitempty"` + AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"` + AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,omitempty"` StrictRoute bool `json:"strict_route,omitempty"` RouteAddress Listable[netip.Prefix] `json:"route_address,omitempty"` RouteAddressSet Listable[string] `json:"route_address_set,omitempty"` @@ -46,3 +53,26 @@ type TunInboundOptions struct { // Deprecated: merged to RouteExcludeAddress Inet6RouteExcludeAddress Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"` } + +type FwMark uint32 + +func (f FwMark) MarshalJSON() ([]byte, error) { + return json.Marshal(F.ToString("0x", strconv.FormatUint(uint64(f), 16))) +} + +func (f *FwMark) UnmarshalJSON(bytes []byte) error { + var stringValue string + err := json.Unmarshal(bytes, &stringValue) + if err != nil { + if rawErr := json.Unmarshal(bytes, (*uint32)(f)); rawErr == nil { + return nil + } + return E.Cause(err, "invalid number or string mark") + } + intValue, err := strconv.ParseUint(stringValue, 0, 32) + if err != nil { + return err + } + *f = FwMark(intValue) + return nil +} From 1e3c136440c3a0728b529c8333e7260b903ae039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 24 Jun 2024 09:41:00 +0800 Subject: [PATCH 13/25] Add accept empty DNS rule option --- adapter/inbound.go | 5 ++++- docs/configuration/dns/rule.md | 29 ++++++++++++++++++++++++-- docs/configuration/dns/rule.zh.md | 31 +++++++++++++++++++++++++--- docs/configuration/inbound/tun.zh.md | 12 +++++------ docs/configuration/route/rule.md | 23 ++++++++++++++++++++- docs/configuration/route/rule.zh.md | 23 ++++++++++++++++++++- option/rule.go | 25 +++++++++++++++++++--- option/rule_dns.go | 26 ++++++++++++++++++++--- route/router_dns.go | 17 +++++++++------ route/rule_default.go | 2 +- route/rule_dns.go | 2 +- route/rule_item_cidr.go | 19 +++++++++-------- route/rule_item_rule_set.go | 13 +++++++----- 13 files changed, 185 insertions(+), 42 deletions(-) diff --git a/adapter/inbound.go b/adapter/inbound.go index 063671c1a1..56eb562308 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -51,7 +51,9 @@ type InboundContext struct { // rule cache - IPCIDRMatchSource bool + IPCIDRMatchSource bool + IPCIDRAcceptEmpty bool + SourceAddressMatch bool SourcePortMatch bool DestinationAddressMatch bool @@ -62,6 +64,7 @@ type InboundContext struct { func (c *InboundContext) ResetRuleCache() { c.IPCIDRMatchSource = false + c.IPCIDRAcceptEmpty = false c.SourceAddressMatch = false c.SourcePortMatch = false c.DestinationAddressMatch = false diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 4c4abacb65..0faae0e680 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -2,6 +2,12 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.10.0" + + :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + !!! quote "Changes in sing-box 1.9.0" :material-plus: [geoip](#geoip) @@ -117,7 +123,10 @@ icon: material/new-box "geoip-cn", "geosite-cn" ], + // deprecated "rule_set_ipcidr_match_source": false, + "rule_set_ip_cidr_match_source": false, + "rule_set_ip_cidr_accept_empty": false, "invert": false, "outbound": [ "direct" @@ -309,7 +318,17 @@ Match [Rule Set](/configuration/route/#rule_set). !!! question "Since sing-box 1.9.0" -Make `ipcidr` in rule sets match the source IP. +!!! failure "Deprecated in sing-box 1.10.0" + + `rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. + +Make `ip_cidr` rule items in rule sets match the source IP. + +#### rule_set_ip_cidr_match_source + +!!! question "Since sing-box 1.10.0" + +Make `ip_cidr` rule items in rule sets match the source IP. #### invert @@ -347,7 +366,7 @@ Will overrides `dns.client_subnet` and `servers.[].client_subnet`. ### Address Filter Fields -Only takes effect for IP address requests. When the query results do not match the address filtering rule items, the current rule will be skipped. +Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped. !!! info "" @@ -375,6 +394,12 @@ Match IP CIDR with query response. Match private IP with query response. +#### rule_set_ip_cidr_accept_empty + +!!! question "Since sing-box 1.10.0" + +Make `ip_cidr` rules in rule sets accept empty query response. + ### Logical Fields #### type diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 796d29e8c4..eecddb3816 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -2,6 +2,12 @@ icon: material/new-box --- +!!! quote "sing-box 1.10.0 中的更改" + + :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + !!! quote "sing-box 1.9.0 中的更改" :material-plus: [geoip](#geoip) @@ -117,7 +123,10 @@ icon: material/new-box "geoip-cn", "geosite-cn" ], + // 已弃用 "rule_set_ipcidr_match_source": false, + "rule_set_ip_cidr_match_source": false, + "rule_set_ip_cidr_accept_empty": false, "invert": false, "outbound": [ "direct" @@ -307,7 +316,17 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! question "自 sing-box 1.9.0 起" -使规则集中的 `ipcidr` 规则匹配源 IP。 +!!! failure "已在 sing-box 1.10.0 废弃" + + `rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 移除。 + +使规则集中的 `ip_cidr` 规则匹配源 IP。 + +#### rule_set_ip_cidr_match_source + +!!! question "自 sing-box 1.10.0 起" + +使规则集中的 `ip_cidr` 规则匹配源 IP。 #### invert @@ -345,7 +364,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 ### 地址筛选字段 -仅对IP地址请求生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。 +仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。 !!! info "" @@ -365,7 +384,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! question "自 sing-box 1.9.0 起" -与查询相应匹配 IP CIDR。 +与查询响应匹配 IP CIDR。 #### ip_is_private @@ -373,6 +392,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 与查询响应匹配非公开 IP。 +#### rule_set_ip_cidr_accept_empty + +!!! question "自 sing-box 1.10.0 起" + +使规则集中的 `ip_cidr` 规则接受空查询响应。 + ### 逻辑字段 #### type diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index bff493d4f9..88e02fe767 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -168,7 +168,7 @@ tun 接口的 IPv4 和 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet4_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除. + `inet4_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除。 ==必填== @@ -178,7 +178,7 @@ tun 接口的 IPv4 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet6_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除. + `inet6_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除。 tun 接口的 IPv6 前缀。 @@ -288,7 +288,7 @@ tun 接口的 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除. + `inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除。 启用 `auto_route` 时使用自定义路由而不是默认路由。 @@ -296,7 +296,7 @@ tun 接口的 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除. + `inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除。 启用 `auto_route` 时使用自定义路由而不是默认路由。 @@ -310,7 +310,7 @@ tun 接口的 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除. + `inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除。 启用 `auto_route` 时排除自定义路由。 @@ -318,7 +318,7 @@ tun 接口的 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除. + `inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除。 启用 `auto_route` 时排除自定义路由。 diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 62d33c6c53..1d06a875a1 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -1,3 +1,12 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.10.0" + + :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + !!! quote "Changes in sing-box 1.8.0" :material-plus: [rule_set](#rule_set) @@ -105,7 +114,9 @@ "geoip-cn", "geosite-cn" ], + // deprecated "rule_set_ipcidr_match_source": false, + "rule_set_ip_cidr_match_source": false, "invert": false, "outbound": "direct" }, @@ -303,7 +314,17 @@ Match [Rule Set](/configuration/route/#rule_set). !!! question "Since sing-box 1.8.0" -Make `ipcidr` in rule sets match the source IP. +!!! failure "Deprecated in sing-box 1.10.0" + + `rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. + +Make `ip_cidr` in rule sets match the source IP. + +#### rule_set_ip_cidr_match_source + +!!! question "Since sing-box 1.10.0" + +Make `ip_cidr` in rule sets match the source IP. #### invert diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index cba35bc581..52d334f22c 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -1,3 +1,12 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.10.0 中的更改" + + :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + !!! quote "sing-box 1.8.0 中的更改" :material-plus: [rule_set](#rule_set) @@ -103,7 +112,9 @@ "geoip-cn", "geosite-cn" ], + // 已弃用 "rule_set_ipcidr_match_source": false, + "rule_set_ip_cidr_match_source": false, "invert": false, "outbound": "direct" }, @@ -301,7 +312,17 @@ !!! question "自 sing-box 1.8.0 起" -使规则集中的 `ipcidr` 规则匹配源 IP。 +!!! failure "已在 sing-box 1.10.0 废弃" + + `rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 移除。 + +使规则集中的 `ip_cidr` 规则匹配源 IP。 + +#### rule_set_ip_cidr_match_source + +!!! question "自 sing-box 1.10.0 起" + +使规则集中的 `ip_cidr` 规则匹配源 IP。 #### invert diff --git a/option/rule.go b/option/rule.go index 0ea133c75f..74dd13c615 100644 --- a/option/rule.go +++ b/option/rule.go @@ -64,7 +64,7 @@ func (r Rule) IsValid() bool { } } -type DefaultRule struct { +type _DefaultRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` Network Listable[string] `json:"network,omitempty"` @@ -94,12 +94,31 @@ type DefaultRule struct { 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"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` Invert bool `json:"invert,omitempty"` Outbound string `json:"outbound,omitempty"` + + // Deprecated: renamed to rule_set_ip_cidr_match_source + Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` +} + +type DefaultRule _DefaultRule + +func (r *DefaultRule) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_DefaultRule)(r)) + if err != nil { + return err + } + //nolint:staticcheck + //goland:noinspection GoDeprecation + if r.Deprecated_RulesetIPCIDRMatchSource { + r.Deprecated_RulesetIPCIDRMatchSource = false + r.RuleSetIPCIDRMatchSource = true + } + return nil } -func (r DefaultRule) IsValid() bool { +func (r *DefaultRule) IsValid() bool { var defaultValue DefaultRule defaultValue.Invert = r.Invert defaultValue.Outbound = r.Outbound diff --git a/option/rule_dns.go b/option/rule_dns.go index c5994e1cec..2afe245ea4 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -64,7 +64,7 @@ func (r DNSRule) IsValid() bool { } } -type DefaultDNSRule struct { +type _DefaultDNSRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` @@ -96,15 +96,35 @@ type DefaultDNSRule struct { 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"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` + RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` Invert bool `json:"invert,omitempty"` Server string `json:"server,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` + + // Deprecated: renamed to rule_set_ip_cidr_match_source + Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` +} + +type DefaultDNSRule _DefaultDNSRule + +func (r *DefaultDNSRule) UnmarshalJSON(bytes []byte) error { + err := json.UnmarshalDisallowUnknownFields(bytes, (*_DefaultDNSRule)(r)) + if err != nil { + return err + } + //nolint:staticcheck + //goland:noinspection GoDeprecation + if r.Deprecated_RulesetIPCIDRMatchSource { + r.Deprecated_RulesetIPCIDRMatchSource = false + r.RuleSetIPCIDRMatchSource = true + } + return nil } -func (r DefaultDNSRule) IsValid() bool { +func (r *DefaultDNSRule) IsValid() bool { var defaultValue DefaultDNSRule defaultValue.Invert = r.Invert defaultValue.Server = r.Server diff --git a/route/router_dns.go b/route/router_dns.go index 51994072ab..ead8c28943 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -104,7 +104,8 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er response, cached = r.dnsClient.ExchangeCache(ctx, message) if !cached { var metadata *adapter.InboundContext - ctx, metadata = adapter.AppendContext(ctx) + ctx, metadata = adapter.ExtendContext(ctx) + metadata.Destination = M.Socksaddr{} if len(message.Question) > 0 { metadata.QueryType = message.Question[0].Qtype switch metadata.QueryType { @@ -126,12 +127,16 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er dnsCtx context.Context addressLimit bool ) - dnsCtx, transport, strategy, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message)) + dnsCtx = adapter.OverrideContext(dnsCtx) if rule != nil && rule.WithAddressLimit() { addressLimit = true response, err = r.dnsClient.ExchangeWithResponseCheck(dnsCtx, transport, message, strategy, func(response *mDNS.Msg) bool { - metadata.DestinationAddresses, _ = dns.MessageToAddresses(response) + addresses, addrErr := dns.MessageToAddresses(response) + if addrErr != nil { + return false + } + metadata.DestinationAddresses = addresses return rule.MatchAddressLimit(metadata) }) } else { @@ -190,7 +195,8 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS return responseAddrs, nil } r.dnsLogger.DebugContext(ctx, "lookup domain ", domain) - ctx, metadata := adapter.AppendContext(ctx) + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Destination = M.Socksaddr{} metadata.Domain = domain var ( transport dns.Transport @@ -204,9 +210,8 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS dnsCtx context.Context addressLimit bool ) - metadata.ResetRuleCache() - metadata.DestinationAddresses = nil dnsCtx, transport, transportStrategy, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true) + dnsCtx = adapter.OverrideContext(dnsCtx) if strategy == dns.DomainStrategyAsIS { strategy = transportStrategy } diff --git a/route/rule_default.go b/route/rule_default.go index d1d13f7d72..53e53bdf86 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -205,7 +205,7 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.allItems = append(rule.allItems, item) } if len(options.RuleSet) > 0 { - item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource) + item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource, false) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } diff --git a/route/rule_dns.go b/route/rule_dns.go index 955526fc6f..1b79d30b25 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -219,7 +219,7 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.allItems = append(rule.allItems, item) } if len(options.RuleSet) > 0 { - item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource) + item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource, options.RuleSetIPCIDRAcceptEmpty) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } diff --git a/route/rule_item_cidr.go b/route/rule_item_cidr.go index 85b9c8d7d3..be0bb1369c 100644 --- a/route/rule_item_cidr.go +++ b/route/rule_item_cidr.go @@ -75,18 +75,19 @@ func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem { func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool { if r.isSource || metadata.IPCIDRMatchSource { return r.ipSet.Contains(metadata.Source.Addr) - } else { - if metadata.Destination.IsIP() { - return r.ipSet.Contains(metadata.Destination.Addr) - } else { - for _, address := range metadata.DestinationAddresses { - if r.ipSet.Contains(address) { - return true - } + } + if metadata.Destination.IsIP() { + return r.ipSet.Contains(metadata.Destination.Addr) + } + if len(metadata.DestinationAddresses) > 0 { + for _, address := range metadata.DestinationAddresses { + if r.ipSet.Contains(address) { + return true } } + return false } - return false + return metadata.IPCIDRAcceptEmpty } func (r *IPCIDRItem) String() string { diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go index 4ecf8c18f0..b80fca995c 100644 --- a/route/rule_item_rule_set.go +++ b/route/rule_item_rule_set.go @@ -15,14 +15,16 @@ type RuleSetItem struct { router adapter.Router tagList []string setList []adapter.RuleSet - ipcidrMatchSource bool + ipCidrMatchSource bool + ipCidrAcceptEmpty bool } -func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool) *RuleSetItem { +func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool, ipCidrAcceptEmpty bool) *RuleSetItem { return &RuleSetItem{ router: router, tagList: tagList, - ipcidrMatchSource: ipCIDRMatchSource, + ipCidrMatchSource: ipCIDRMatchSource, + ipCidrAcceptEmpty: ipCidrAcceptEmpty, } } @@ -39,7 +41,8 @@ func (r *RuleSetItem) Start() error { } func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { - metadata.IPCIDRMatchSource = r.ipcidrMatchSource + metadata.IPCIDRMatchSource = r.ipCidrMatchSource + metadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty for _, ruleSet := range r.setList { if ruleSet.Match(metadata) { return true @@ -49,7 +52,7 @@ func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { } func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool { - if r.ipcidrMatchSource { + if r.ipCidrMatchSource { return false } return common.Any(r.setList, func(ruleSet adapter.RuleSet) bool { From c2ab497ff40a307df3dbbdce4f4bf1bdb0b0b8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 26 Jun 2024 00:45:10 +0800 Subject: [PATCH 14/25] Unique rule-set names --- cmd/sing-box/cmd_rule_set.go | 2 +- cmd/sing-box/cmd_rule_set_match.go | 4 ++-- common/srs/binary.go | 2 +- docs/changelog.md | 26 +++++++++++++------------- docs/configuration/dns/rule.md | 12 ++++++------ docs/configuration/route/index.md | 2 +- docs/configuration/route/rule.md | 8 ++++---- docs/configuration/rule-set/index.md | 14 +++++++------- docs/deprecated.md | 4 ++-- docs/migration.md | 16 ++++++++-------- docs/migration.zh.md | 4 ++-- option/rule_set.go | 14 +++++++------- route/rule_set.go | 2 +- route/rule_set_local.go | 2 +- route/rule_set_remote.go | 2 +- 15 files changed, 57 insertions(+), 57 deletions(-) diff --git a/cmd/sing-box/cmd_rule_set.go b/cmd/sing-box/cmd_rule_set.go index f4112a087b..242ea8b666 100644 --- a/cmd/sing-box/cmd_rule_set.go +++ b/cmd/sing-box/cmd_rule_set.go @@ -6,7 +6,7 @@ import ( var commandRuleSet = &cobra.Command{ Use: "rule-set", - Short: "Manage rule sets", + Short: "Manage rule-sets", } func init() { diff --git a/cmd/sing-box/cmd_rule_set_match.go b/cmd/sing-box/cmd_rule_set_match.go index fb2560afb1..8bf2ec7e12 100644 --- a/cmd/sing-box/cmd_rule_set_match.go +++ b/cmd/sing-box/cmd_rule_set_match.go @@ -23,7 +23,7 @@ var flagRuleSetMatchFormat string var commandRuleSetMatch = &cobra.Command{ Use: "match ", - Short: "Check if an IP address or a domain matches the rule set", + Short: "Check if an IP address or a domain matches the rule-set", Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { err := ruleSetMatch(args[0], args[1]) @@ -70,7 +70,7 @@ func ruleSetMatch(sourcePath string, domain string) error { return err } default: - return E.New("unknown rule set format: ", flagRuleSetMatchFormat) + return E.New("unknown rule-set format: ", flagRuleSetMatchFormat) } ipAddress := M.ParseAddr(domain) var metadata adapter.InboundContext diff --git a/common/srs/binary.go b/common/srs/binary.go index 0bd5a9099b..69075f7881 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -46,7 +46,7 @@ func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err erro return } if magicBytes != MagicBytes { - err = E.New("invalid sing-box rule set file") + err = E.New("invalid sing-box rule-set file") return } var version uint8 diff --git a/docs/changelog.md b/docs/changelog.md index f5eb714cde..f835a68bd2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -367,7 +367,7 @@ See [Address Filter Fields](/configuration/dns/rule#address-filter-fields). Important changes since 1.7: * Migrate cache file from Clash API to independent options **1** -* Introducing [Rule Set](/configuration/rule-set/) **2** +* 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** * Independent `source_ip_is_private` and `ip_is_private` rules **5** @@ -387,7 +387,7 @@ See [Cache File](/configuration/experimental/cache-file/) and **2**: -Rule set is independent collections of rules that can be compiled into binaries to improve performance. +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. @@ -395,16 +395,16 @@ 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/), +[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). +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. +New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets. **4**: @@ -601,7 +601,7 @@ This change is intended to break incorrect usage and essentially requires no act **1**: -Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule sets, +Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule-sets, rather than completely following the AND logic. #### 1.8.0-alpha.5 @@ -617,7 +617,7 @@ Since GeoIP was deprecated, we made this rule independent, see [Migration](/migr #### 1.8.0-alpha.1 * Migrate cache file from Clash API to independent options **1** -* Introducing [Rule Set](/configuration/rule-set/) **2** +* 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** @@ -628,7 +628,7 @@ See [Cache File](/configuration/experimental/cache-file/) and **2**: -Rule set is independent collections of rules that can be compiled into binaries to improve performance. +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. @@ -636,16 +636,16 @@ 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/), +[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). +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. +New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets. **4**: diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 0faae0e680..6b3d65194f 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -166,7 +166,7 @@ icon: material/new-box (`source_port` || `source_port_range`) && `other fields` - Additionally, included rule sets can be considered merged rather than as a single rule sub-item. + Additionally, included rule-sets can be considered merged rather than as a single rule sub-item. #### inbound @@ -312,7 +312,7 @@ Match WiFi BSSID. !!! question "Since sing-box 1.8.0" -Match [Rule Set](/configuration/route/#rule_set). +Match [rule-set](/configuration/route/#rule_set). #### rule_set_ipcidr_match_source @@ -322,13 +322,13 @@ Match [Rule Set](/configuration/route/#rule_set). `rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. -Make `ip_cidr` rule items in rule sets match the source IP. +Make `ip_cidr` rule items in rule-sets match the source IP. #### rule_set_ip_cidr_match_source !!! question "Since sing-box 1.10.0" -Make `ip_cidr` rule items in rule sets match the source IP. +Make `ip_cidr` rule items in rule-sets match the source IP. #### invert @@ -370,7 +370,7 @@ Only takes effect for address requests (A/AAAA/HTTPS). When the query results do !!! info "" - `ip_cidr` items in included rule sets also takes effect as an address filtering field. + `ip_cidr` items in included rule-sets also takes effect as an address filtering field. !!! note "" @@ -398,7 +398,7 @@ Match private IP with query response. !!! question "Since sing-box 1.10.0" -Make `ip_cidr` rules in rule sets accept empty query response. +Make `ip_cidr` rules in rule-sets accept empty query response. ### Logical Fields diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 7b2a7e7ef2..507cb140b6 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -39,7 +39,7 @@ List of [Route Rule](./rule/) !!! question "Since sing-box 1.8.0" -List of [Rule Set](/configuration/rule-set/) +List of [rule-set](/configuration/rule-set/) #### final diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 1d06a875a1..23d2bf1b0c 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -148,7 +148,7 @@ icon: material/alert-decagram (`source_port` || `source_port_range`) && `other fields` - Additionally, included rule sets can be considered merged rather than as a single rule sub-item. + Additionally, included rule-sets can be considered merged rather than as a single rule sub-item. #### inbound @@ -308,7 +308,7 @@ Match WiFi BSSID. !!! question "Since sing-box 1.8.0" -Match [Rule Set](/configuration/route/#rule_set). +Match [rule-set](/configuration/route/#rule_set). #### rule_set_ipcidr_match_source @@ -318,13 +318,13 @@ Match [Rule Set](/configuration/route/#rule_set). `rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. -Make `ip_cidr` in rule sets match the source IP. +Make `ip_cidr` in rule-sets match the source IP. #### rule_set_ip_cidr_match_source !!! question "Since sing-box 1.10.0" -Make `ip_cidr` in rule sets match the source IP. +Make `ip_cidr` in rule-sets match the source IP. #### invert diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md index ba2f741e4f..b92d80f3d7 100644 --- a/docs/configuration/rule-set/index.md +++ b/docs/configuration/rule-set/index.md @@ -1,4 +1,4 @@ -# Rule Set +# rule-set !!! question "Since sing-box 1.8.0" @@ -50,19 +50,19 @@ ==Required== -Type of Rule Set, `local` or `remote`. +Type of rule-set, `local` or `remote`. #### tag ==Required== -Tag of Rule Set. +Tag of rule-set. #### format ==Required== -Format of Rule Set, `source` or `binary`. +Format of rule-set, `source` or `binary`. ### Local Fields @@ -70,7 +70,7 @@ Format of Rule Set, `source` or `binary`. ==Required== -File path of Rule Set. +File path of rule-set. ### Remote Fields @@ -78,7 +78,7 @@ File path of Rule Set. ==Required== -Download URL of Rule Set. +Download URL of rule-set. #### download_detour @@ -88,6 +88,6 @@ Default outbound will be used if empty. #### update_interval -Update interval of Rule Set. +Update interval of rule-set. `1d` will be used if empty. diff --git a/docs/deprecated.md b/docs/deprecated.md index 249bc492a2..eb0c1925c8 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -33,7 +33,7 @@ The maxmind GeoIP National Database, as an IP classification database, is not entirely suitable for traffic bypassing, and all existing implementations suffer from high memory usage and difficult management. -sing-box 1.8.0 introduces [Rule Set](/configuration/rule-set/), which can completely replace GeoIP, +sing-box 1.8.0 introduces [rule-set](/configuration/rule-set/), which can completely replace GeoIP, check [Migration](/migration/#migrate-geoip-to-rule-sets). #### Geosite @@ -43,7 +43,7 @@ Geosite is deprecated and may be removed in the future. Geosite, the `domain-list-community` project maintained by V2Ray as an early traffic bypassing solution, suffers from a number of problems, including lack of maintenance, inaccurate rules, and difficult management. -sing-box 1.8.0 introduces [Rule Set](/configuration/rule-set/), which can completely replace Geosite, +sing-box 1.8.0 introduces [rule-set](/configuration/rule-set/), which can completely replace Geosite, check [Migration](/migration/#migrate-geosite-to-rule-sets). ## 1.6.0 diff --git a/docs/migration.md b/docs/migration.md index 017ed2302e..71b6169216 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -145,7 +145,7 @@ which will disrupt the existing `process_path` use cases in Windows. } ``` -### :material-checkbox-intermediate: Migrate GeoIP to rule sets +### :material-checkbox-intermediate: Migrate GeoIP to rule-sets !!! info "References" @@ -153,11 +153,11 @@ which will disrupt the existing `process_path` use cases in Windows. [Route](/configuration/route/) / [Route Rule](/configuration/route/rule/) / [DNS Rule](/configuration/dns/rule/) / - [Rule Set](/configuration/rule-set/) + [rule-set](/configuration/rule-set/) !!! tip - `sing-box geoip` commands can help you convert custom GeoIP into rule sets. + `sing-box geoip` commands can help you convert custom GeoIP into rule-sets. === ":material-card-remove: Deprecated" @@ -224,13 +224,13 @@ which will disrupt the existing `process_path` use cases in Windows. }, "experimental": { "cache_file": { - "enabled": true // required to save Rule Set cache + "enabled": true // required to save rule-set cache } } } ``` -### :material-checkbox-intermediate: Migrate Geosite to rule sets +### :material-checkbox-intermediate: Migrate Geosite to rule-sets !!! info "References" @@ -238,11 +238,11 @@ which will disrupt the existing `process_path` use cases in Windows. [Route](/configuration/route/) / [Route Rule](/configuration/route/rule/) / [DNS Rule](/configuration/dns/rule/) / - [Rule Set](/configuration/rule-set/) + [rule-set](/configuration/rule-set/) !!! tip - `sing-box geosite` commands can help you convert custom Geosite into rule sets. + `sing-box geosite` commands can help you convert custom Geosite into rule-sets. === ":material-card-remove: Deprecated" @@ -285,7 +285,7 @@ which will disrupt the existing `process_path` use cases in Windows. }, "experimental": { "cache_file": { - "enabled": true // required to save Rule Set cache + "enabled": true // required to save rule-set cache } } } diff --git a/docs/migration.zh.md b/docs/migration.zh.md index a8a0774045..62fbe9edeb 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -222,7 +222,7 @@ sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\fold }, "experimental": { "cache_file": { - "enabled": true // required to save Rule Set cache + "enabled": true // required to save rule-set cache } } } @@ -283,7 +283,7 @@ sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\fold }, "experimental": { "cache_file": { - "enabled": true // required to save Rule Set cache + "enabled": true // required to save rule-set cache } } } diff --git a/option/rule_set.go b/option/rule_set.go index 5498400f2f..ec32d0a13a 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -31,7 +31,7 @@ func (r RuleSet) MarshalJSON() ([]byte, error) { case C.RuleSetTypeRemote: v = r.RemoteOptions default: - return nil, E.New("unknown rule set type: " + r.Type) + return nil, E.New("unknown rule-set type: " + r.Type) } return MarshallObjects((_RuleSet)(r), v) } @@ -49,7 +49,7 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error { return E.New("missing format") case C.RuleSetFormatSource, C.RuleSetFormatBinary: default: - return E.New("unknown rule set format: " + r.Format) + return E.New("unknown rule-set format: " + r.Format) } var v any switch r.Type { @@ -60,7 +60,7 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error { case "": return E.New("missing type") default: - return E.New("unknown rule set type: " + r.Type) + return E.New("unknown rule-set type: " + r.Type) } err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v) if err != nil { @@ -188,7 +188,7 @@ func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { case C.RuleSetVersion1, C.RuleSetVersion2: v = r.Options default: - return nil, E.New("unknown rule set version: ", r.Version) + return nil, E.New("unknown rule-set version: ", r.Version) } return MarshallObjects((_PlainRuleSetCompat)(r), v) } @@ -203,9 +203,9 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { case C.RuleSetVersion1, C.RuleSetVersion2: v = &r.Options case 0: - return E.New("missing rule set version") + return E.New("missing rule-set version") default: - return E.New("unknown rule set version: ", r.Version) + return E.New("unknown rule-set version: ", r.Version) } err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) if err != nil { @@ -220,7 +220,7 @@ func (r PlainRuleSetCompat) Upgrade() PlainRuleSet { case C.RuleSetVersion1, C.RuleSetVersion2: result = r.Options default: - panic("unknown rule set version: " + F.ToString(r.Version)) + panic("unknown rule-set version: " + F.ToString(r.Version)) } return result } diff --git a/route/rule_set.go b/route/rule_set.go index 7c02aa7a82..9907a1c673 100644 --- a/route/rule_set.go +++ b/route/rule_set.go @@ -25,7 +25,7 @@ func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.Contex case C.RuleSetTypeRemote: return NewRemoteRuleSet(ctx, router, logger, options), nil default: - return nil, E.New("unknown rule set type: ", options.Type) + return nil, E.New("unknown rule-set type: ", options.Type) } } diff --git a/route/rule_set_local.go b/route/rule_set_local.go index 87a393c27d..96d30dd5e8 100644 --- a/route/rule_set_local.go +++ b/route/rule_set_local.go @@ -52,7 +52,7 @@ func NewLocalRuleSet(ctx context.Context, router adapter.Router, options option. return nil, err } default: - return nil, E.New("unknown rule set format: ", options.Format) + return nil, E.New("unknown rule-set format: ", options.Format) } rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) var err error diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go index bf0cfe204c..1473a494a9 100644 --- a/route/rule_set_remote.go +++ b/route/rule_set_remote.go @@ -175,7 +175,7 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { return err } default: - return E.New("unknown rule set format: ", s.options.Format) + return E.New("unknown rule-set format: ", s.options.Format) } rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) for i, ruleOptions := range plainRuleSet.Rules { From 852c07c0504743f6938763540860a44f9d15b0c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 26 Jun 2024 00:43:51 +0800 Subject: [PATCH 15/25] Add inline rule-set & Add reload for local rule-set --- cmd/sing-box/cmd_rule_set_compile.go | 5 +- cmd/sing-box/cmd_rule_set_match.go | 5 +- cmd/sing-box/cmd_rule_set_upgrade.go | 5 +- common/tls/ech_server.go | 185 +++++++++------------------ common/tls/std_server.go | 61 +++------ constant/rule.go | 1 + docs/configuration/rule-set/index.md | 104 +++++++++------ docs/configuration/shared/tls.md | 18 ++- docs/configuration/shared/tls.zh.md | 16 ++- go.mod | 4 +- option/rule_set.go | 37 +++--- route/rule_set.go | 4 +- route/rule_set_local.go | 127 +++++++++++++----- route/rule_set_remote.go | 5 +- 14 files changed, 304 insertions(+), 273 deletions(-) diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go index 7e3753c928..4fae4d99e5 100644 --- a/cmd/sing-box/cmd_rule_set_compile.go +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -56,7 +56,10 @@ func compileRuleSet(sourcePath string) error { if err != nil { return err } - ruleSet := plainRuleSet.Upgrade() + ruleSet, err := plainRuleSet.Upgrade() + if err != nil { + return err + } var outputPath string if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { if strings.HasSuffix(sourcePath, ".json") { diff --git a/cmd/sing-box/cmd_rule_set_match.go b/cmd/sing-box/cmd_rule_set_match.go index 8bf2ec7e12..937458f2c6 100644 --- a/cmd/sing-box/cmd_rule_set_match.go +++ b/cmd/sing-box/cmd_rule_set_match.go @@ -63,7 +63,10 @@ func ruleSetMatch(sourcePath string, domain string) error { if err != nil { return err } - plainRuleSet = compat.Upgrade() + plainRuleSet, err = compat.Upgrade() + if err != nil { + return err + } case C.RuleSetFormatBinary: plainRuleSet, err = srs.Read(bytes.NewReader(content), false) if err != nil { diff --git a/cmd/sing-box/cmd_rule_set_upgrade.go b/cmd/sing-box/cmd_rule_set_upgrade.go index 0ec039fd71..e885d849e4 100644 --- a/cmd/sing-box/cmd_rule_set_upgrade.go +++ b/cmd/sing-box/cmd_rule_set_upgrade.go @@ -61,7 +61,10 @@ func upgradeRuleSet(sourcePath string) error { log.Info("already up-to-date") return nil } - plainRuleSet := plainRuleSetCompat.Upgrade() + plainRuleSet, err := plainRuleSetCompat.Upgrade() + if err != nil { + return err + } buffer := new(bytes.Buffer) encoder := json.NewEncoder(buffer) encoder.SetIndent("", " ") diff --git a/common/tls/ech_server.go b/common/tls/ech_server.go index 43ddd820ec..ac3e6279c3 100644 --- a/common/tls/ech_server.go +++ b/common/tls/ech_server.go @@ -11,12 +11,11 @@ import ( "strings" cftls "github.com/sagernet/cloudflare-tls" + "github.com/sagernet/fswatch" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/ntp" - - "github.com/fsnotify/fsnotify" ) type echServerConfig struct { @@ -26,9 +25,8 @@ type echServerConfig struct { key []byte certificatePath string keyPath string - watcher *fsnotify.Watcher echKeyPath string - echWatcher *fsnotify.Watcher + watcher *fswatch.Watcher } func (c *echServerConfig) ServerName() string { @@ -66,146 +64,84 @@ func (c *echServerConfig) Clone() Config { } func (c *echServerConfig) Start() error { - if c.certificatePath != "" && c.keyPath != "" { - err := c.startWatcher() - if err != nil { - c.logger.Warn("create fsnotify watcher: ", err) - } - } - if c.echKeyPath != "" { - err := c.startECHWatcher() - if err != nil { - c.logger.Warn("create fsnotify watcher: ", err) - } + err := c.startWatcher() + if err != nil { + c.logger.Warn("create credentials watcher: ", err) } return nil } func (c *echServerConfig) startWatcher() error { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } + var watchPath []string if c.certificatePath != "" { - err = watcher.Add(c.certificatePath) - if err != nil { - return err - } + watchPath = append(watchPath, c.certificatePath) } if c.keyPath != "" { - err = watcher.Add(c.keyPath) - if err != nil { - return err - } + watchPath = append(watchPath, c.keyPath) + } + if c.echKeyPath != "" { + watchPath = append(watchPath, c.echKeyPath) + } + if len(watchPath) == 0 { + return nil + } + watcher, err := fswatch.NewWatcher(fswatch.Options{ + Path: watchPath, + Callback: func(path string) { + err := c.credentialsUpdated(path) + if err != nil { + c.logger.Error(E.Cause(err, "reload credentials from ", path)) + } + }, + }) + if err != nil { + return err } c.watcher = watcher - go c.loopUpdate() return nil } -func (c *echServerConfig) loopUpdate() { - for { - select { - case event, ok := <-c.watcher.Events: - if !ok { - return - } - if event.Op&fsnotify.Write != fsnotify.Write { - continue - } - err := c.reloadKeyPair() +func (c *echServerConfig) credentialsUpdated(path string) error { + if path == c.certificatePath || path == c.keyPath { + if path == c.certificatePath { + certificate, err := os.ReadFile(c.certificatePath) if err != nil { - c.logger.Error(E.Cause(err, "reload TLS key pair")) + return err } - case err, ok := <-c.watcher.Errors: - if !ok { - return + c.certificate = certificate + } else { + key, err := os.ReadFile(c.keyPath) + if err != nil { + return err } - c.logger.Error(E.Cause(err, "fsnotify error")) + c.key = key } - } -} - -func (c *echServerConfig) reloadKeyPair() error { - if c.certificatePath != "" { - certificate, err := os.ReadFile(c.certificatePath) + keyPair, err := cftls.X509KeyPair(c.certificate, c.key) if err != nil { - return E.Cause(err, "reload certificate from ", c.certificatePath) + return E.Cause(err, "parse key pair") } - c.certificate = certificate - } - if c.keyPath != "" { - key, err := os.ReadFile(c.keyPath) + c.config.Certificates = []cftls.Certificate{keyPair} + c.logger.Info("reloaded TLS certificate") + } else { + echKeyContent, err := os.ReadFile(c.echKeyPath) if err != nil { - return E.Cause(err, "reload key from ", c.keyPath) + return err } - c.key = key - } - keyPair, err := cftls.X509KeyPair(c.certificate, c.key) - if err != nil { - return E.Cause(err, "reload key pair") - } - c.config.Certificates = []cftls.Certificate{keyPair} - c.logger.Info("reloaded TLS certificate") - return nil -} - -func (c *echServerConfig) startECHWatcher() error { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } - err = watcher.Add(c.echKeyPath) - if err != nil { - return err - } - c.echWatcher = watcher - go c.loopECHUpdate() - return nil -} - -func (c *echServerConfig) loopECHUpdate() { - for { - select { - case event, ok := <-c.echWatcher.Events: - if !ok { - return - } - if event.Op&fsnotify.Write != fsnotify.Write { - continue - } - err := c.reloadECHKey() - if err != nil { - c.logger.Error(E.Cause(err, "reload ECH key")) - } - case err, ok := <-c.echWatcher.Errors: - if !ok { - return - } - c.logger.Error(E.Cause(err, "fsnotify error")) + block, rest := pem.Decode(echKeyContent) + if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 { + return E.New("invalid ECH keys pem") } + echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes) + if err != nil { + return E.Cause(err, "parse ECH keys") + } + echKeySet, err := cftls.EXP_NewECHKeySet(echKeys) + if err != nil { + return E.Cause(err, "create ECH key set") + } + c.config.ServerECHProvider = echKeySet + c.logger.Info("reloaded ECH keys") } -} - -func (c *echServerConfig) reloadECHKey() error { - echKeyContent, err := os.ReadFile(c.echKeyPath) - if err != nil { - return err - } - block, rest := pem.Decode(echKeyContent) - if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 { - return E.New("invalid ECH keys pem") - } - echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes) - if err != nil { - return E.Cause(err, "parse ECH keys") - } - echKeySet, err := cftls.EXP_NewECHKeySet(echKeys) - if err != nil { - return E.Cause(err, "create ECH key set") - } - c.config.ServerECHProvider = echKeySet - c.logger.Info("reloaded ECH keys") return nil } @@ -213,12 +149,7 @@ func (c *echServerConfig) Close() error { var err error if c.watcher != nil { err = E.Append(err, c.watcher.Close(), func(err error) error { - return E.Cause(err, "close certificate watcher") - }) - } - if c.echWatcher != nil { - err = E.Append(err, c.echWatcher.Close(), func(err error) error { - return E.Cause(err, "close ECH key watcher") + return E.Cause(err, "close credentials watcher") }) } return err diff --git a/common/tls/std_server.go b/common/tls/std_server.go index 7184bdb36b..7001bd3a04 100644 --- a/common/tls/std_server.go +++ b/common/tls/std_server.go @@ -7,14 +7,13 @@ import ( "os" "strings" + "github.com/sagernet/fswatch" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/ntp" - - "github.com/fsnotify/fsnotify" ) var errInsecureUnused = E.New("tls: insecure unused") @@ -27,7 +26,7 @@ type STDServerConfig struct { key []byte certificatePath string keyPath string - watcher *fsnotify.Watcher + watcher *fswatch.Watcher } func (c *STDServerConfig) ServerName() string { @@ -88,59 +87,37 @@ func (c *STDServerConfig) Start() error { } func (c *STDServerConfig) startWatcher() error { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } + var watchPath []string if c.certificatePath != "" { - err = watcher.Add(c.certificatePath) - if err != nil { - return err - } + watchPath = append(watchPath, c.certificatePath) } if c.keyPath != "" { - err = watcher.Add(c.keyPath) - if err != nil { - return err - } + watchPath = append(watchPath, c.keyPath) } - c.watcher = watcher - go c.loopUpdate() - return nil -} - -func (c *STDServerConfig) loopUpdate() { - for { - select { - case event, ok := <-c.watcher.Events: - if !ok { - return - } - if event.Op&fsnotify.Write != fsnotify.Write { - continue - } - err := c.reloadKeyPair() + watcher, err := fswatch.NewWatcher(fswatch.Options{ + Path: watchPath, + Callback: func(path string) { + err := c.certificateUpdated(path) if err != nil { - c.logger.Error(E.Cause(err, "reload TLS key pair")) - } - case err, ok := <-c.watcher.Errors: - if !ok { - return + c.logger.Error(err) } - c.logger.Error(E.Cause(err, "fsnotify error")) - } + }, + }) + if err != nil { + return err } + c.watcher = watcher + return nil } -func (c *STDServerConfig) reloadKeyPair() error { - if c.certificatePath != "" { +func (c *STDServerConfig) certificateUpdated(path string) error { + if path == c.certificatePath { certificate, err := os.ReadFile(c.certificatePath) if err != nil { return E.Cause(err, "reload certificate from ", c.certificatePath) } c.certificate = certificate - } - if c.keyPath != "" { + } else if path == c.keyPath { key, err := os.ReadFile(c.keyPath) if err != nil { return E.Cause(err, "reload key from ", c.keyPath) diff --git a/constant/rule.go b/constant/rule.go index fb0f39e22e..718e79a5e6 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -11,6 +11,7 @@ const ( ) const ( + RuleSetTypeInline = "inline" RuleSetTypeLocal = "local" RuleSetTypeRemote = "remote" RuleSetFormatSource = "source" diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md index b92d80f3d7..dfe71d9e39 100644 --- a/docs/configuration/rule-set/index.md +++ b/docs/configuration/rule-set/index.md @@ -1,48 +1,56 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: `type: inline` + # 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": "" -} -``` +=== "Inline" + + !!! question "Since sing-box 1.10.0" + + ```json + { + "type": "inline", // optional + "tag": "", + "rules": [] + } + ``` + +=== "Local File" + + ```json + { + "type": "local", + "tag": "", + "format": "source", // or binary + "path": "" + } + ``` + +=== "Remote File" + + !!! info "" + + Remote rule-set will be cached if `experimental.cache_file.enabled`. + + ```json + { + "type": "remote", + "tag": "", + "format": "source", // or binary + "url": "", + "download_detour": "", // optional + "update_interval": "" // optional + } + ``` ### Fields @@ -58,11 +66,23 @@ Type of rule-set, `local` or `remote`. Tag of rule-set. +### Inline Fields + +!!! question "Since sing-box 1.10.0" + +#### rules + +==Required== + +List of [Headless Rule](./headless-rule.md/). + +### Local or Remote Fields + #### format ==Required== -Format of rule-set, `source` or `binary`. +Format of rule-set file, `source` or `binary`. ### Local Fields @@ -70,6 +90,10 @@ Format of rule-set, `source` or `binary`. ==Required== +!!! note "" + + Will be automatically reloaded if file modified since sing-box 1.10.0. + File path of rule-set. ### Remote Fields diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index b1441a8abc..799aa0b09d 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -178,6 +178,10 @@ The server certificate line array, in PEM format. #### certificate_path +!!! note "" + + Will be automatically reloaded if file modified. + The path to the server certificate, in PEM format. #### key @@ -190,6 +194,10 @@ The server private key line array, in PEM format. ==Server only== +!!! note "" + + Will be automatically reloaded if file modified. + The path to the server private key, in PEM format. ## Custom TLS support @@ -266,6 +274,10 @@ ECH key line array, in PEM format. ==Server only== +!!! note "" + + Will be automatically reloaded if file modified. + The path to ECH key, in PEM format. #### config @@ -397,8 +409,4 @@ A hexadecimal string with zero to eight digits. The maximum time difference between the server and the client. -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 +Check disabled if empty. \ No newline at end of file diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index 360c453642..68de98459f 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -176,12 +176,20 @@ TLS 版本值: #### certificate_path +!!! note "" + + 文件更改时将自动重新加载。 + 服务器 PEM 证书路径。 #### key ==仅服务器== +!!! note "" + + 文件更改时将自动重新加载。 + 服务器 PEM 私钥行数组。 #### key_path @@ -258,6 +266,10 @@ ECH PEM 密钥行数组 ==仅服务器== +!!! note "" + + 文件更改时将自动重新加载。 + ECH PEM 密钥路径 #### config @@ -384,7 +396,3 @@ ACME DNS01 验证字段。如果配置,将禁用其他验证方法。 服务器与和客户端之间允许的最大时间差。 默认禁用检查。 - -### 重载 - -对于服务器配置,如果修改,证书和密钥将自动重新加载。 \ No newline at end of file diff --git a/go.mod b/go.mod index ffcd0d796d..7666dd24e5 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/caddyserver/certmagic v0.20.0 github.com/cloudflare/circl v1.3.7 github.com/cretz/bine v0.2.0 - github.com/fsnotify/fsnotify v1.7.0 github.com/go-chi/chi/v5 v5.0.12 github.com/go-chi/cors v1.2.1 github.com/go-chi/render v1.0.3 @@ -23,6 +22,7 @@ require ( github.com/oschwald/maxminddb-golang v1.12.0 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 + github.com/sagernet/fswatch v0.1.1 github.com/sagernet/gomobile v0.1.4 github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f github.com/sagernet/quic-go v0.47.0-beta.2 @@ -59,6 +59,7 @@ require ( github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gaukas/godicttls v0.0.4 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect @@ -81,7 +82,6 @@ require ( 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.4.1 // indirect - github.com/sagernet/fswatch v0.1.1 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/spf13/pflag v1.0.5 // indirect diff --git a/option/rule_set.go b/option/rule_set.go index ec32d0a13a..4e9eb0f8b3 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -14,9 +14,10 @@ import ( ) type _RuleSet struct { - Type string `json:"type"` + Type string `json:"type,omitempty"` Tag string `json:"tag"` - Format string `json:"format"` + Format string `json:"format,omitempty"` + InlineOptions PlainRuleSet `json:"-"` LocalOptions LocalRuleSet `json:"-"` RemoteOptions RemoteRuleSet `json:"-"` } @@ -26,6 +27,9 @@ type RuleSet _RuleSet func (r RuleSet) MarshalJSON() ([]byte, error) { var v any switch r.Type { + case "", C.RuleSetTypeInline: + r.Type = "" + v = r.InlineOptions case C.RuleSetTypeLocal: v = r.LocalOptions case C.RuleSetTypeRemote: @@ -44,21 +48,26 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error { if r.Tag == "" { return E.New("missing tag") } - switch r.Format { - case "": - return E.New("missing format") - case C.RuleSetFormatSource, C.RuleSetFormatBinary: - default: - return E.New("unknown rule-set format: " + r.Format) + if r.Type != C.RuleSetTypeInline { + switch r.Format { + case "": + return E.New("missing format") + case C.RuleSetFormatSource, C.RuleSetFormatBinary: + default: + return E.New("unknown rule-set format: " + r.Format) + } + } else { + r.Format = "" } var v any switch r.Type { + case "", C.RuleSetTypeInline: + r.Type = C.RuleSetTypeInline + v = &r.InlineOptions case C.RuleSetTypeLocal: v = &r.LocalOptions case C.RuleSetTypeRemote: v = &r.RemoteOptions - case "": - return E.New("missing type") default: return E.New("unknown rule-set type: " + r.Type) } @@ -214,15 +223,13 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { return nil } -func (r PlainRuleSetCompat) Upgrade() PlainRuleSet { - var result PlainRuleSet +func (r PlainRuleSetCompat) Upgrade() (PlainRuleSet, error) { switch r.Version { case C.RuleSetVersion1, C.RuleSetVersion2: - result = r.Options default: - panic("unknown rule-set version: " + F.ToString(r.Version)) + return PlainRuleSet{}, E.New("unknown rule-set version: " + F.ToString(r.Version)) } - return result + return r.Options, nil } type PlainRuleSet struct { diff --git a/route/rule_set.go b/route/rule_set.go index 9907a1c673..39d51e6f54 100644 --- a/route/rule_set.go +++ b/route/rule_set.go @@ -20,8 +20,8 @@ import ( 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(ctx, router, options) + case C.RuleSetTypeInline, C.RuleSetTypeLocal, "": + return NewLocalRuleSet(ctx, router, logger, options) case C.RuleSetTypeRemote: return NewRemoteRuleSet(ctx, router, logger, options), nil default: diff --git a/route/rule_set_local.go b/route/rule_set_local.go index 96d30dd5e8..893842d5cc 100644 --- a/route/rule_set_local.go +++ b/route/rule_set_local.go @@ -3,8 +3,10 @@ package route import ( "context" "os" + "path/filepath" "strings" + "github.com/sagernet/fswatch" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/srs" C "github.com/sagernet/sing-box/constant" @@ -14,6 +16,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service/filemanager" @@ -23,50 +26,55 @@ import ( var _ adapter.RuleSet = (*LocalRuleSet)(nil) type LocalRuleSet struct { - tag string - rules []adapter.HeadlessRule - metadata adapter.RuleSetMetadata - refs atomic.Int32 + router adapter.Router + logger logger.Logger + tag string + rules []adapter.HeadlessRule + metadata adapter.RuleSetMetadata + fileFormat string + watcher *fswatch.Watcher + refs atomic.Int32 } -func NewLocalRuleSet(ctx context.Context, router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { - var plainRuleSet option.PlainRuleSet - switch options.Format { - case C.RuleSetFormatSource, "": - content, err := os.ReadFile(filemanager.BasePath(ctx, options.LocalOptions.Path)) - if err != nil { - return nil, err - } - compat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) - if err != nil { - return nil, err +func NewLocalRuleSet(ctx context.Context, router adapter.Router, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) { + ruleSet := &LocalRuleSet{ + router: router, + logger: logger, + tag: options.Tag, + fileFormat: options.Format, + } + if options.Type == C.RuleSetTypeInline { + if len(options.InlineOptions.Rules) == 0 { + return nil, E.New("empty inline rule-set") } - plainRuleSet = compat.Upgrade() - case C.RuleSetFormatBinary: - setFile, err := os.Open(filemanager.BasePath(ctx, options.LocalOptions.Path)) + err := ruleSet.reloadRules(options.InlineOptions.Rules) if err != nil { return nil, err } - plainRuleSet, err = srs.Read(setFile, false) + } else { + err := ruleSet.reloadFile(filemanager.BasePath(ctx, options.LocalOptions.Path)) if err != nil { return nil, err } - default: - return nil, E.New("unknown rule-set format: ", options.Format) } - rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) - var err error - for i, ruleOptions := range plainRuleSet.Rules { - rules[i], err = NewHeadlessRule(router, ruleOptions) + if options.Type == C.RuleSetTypeLocal { + var watcher *fswatch.Watcher + filePath, _ := filepath.Abs(options.LocalOptions.Path) + watcher, err := fswatch.NewWatcher(fswatch.Options{ + Path: []string{filePath}, + Callback: func(path string) { + uErr := ruleSet.reloadFile(path) + if uErr != nil { + logger.Error(E.Cause(uErr, "reload rule-set ", options.Tag)) + } + }, + }) if err != nil { - return nil, E.Cause(err, "parse rule_set.rules.[", i, "]") + return nil, err } + ruleSet.watcher = watcher } - var metadata adapter.RuleSetMetadata - metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) - metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) - metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) - return &LocalRuleSet{tag: options.Tag, rules: rules, metadata: metadata}, nil + return ruleSet, nil } func (s *LocalRuleSet) Name() string { @@ -78,6 +86,61 @@ func (s *LocalRuleSet) String() string { } func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error { + if s.watcher != nil { + err := s.watcher.Start() + if err != nil { + s.logger.Error(E.Cause(err, "watch rule-set file")) + } + } + return nil +} + +func (s *LocalRuleSet) reloadFile(path string) error { + var plainRuleSet option.PlainRuleSet + switch s.fileFormat { + case C.RuleSetFormatSource, "": + content, err := os.ReadFile(path) + if err != nil { + return err + } + compat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return err + } + plainRuleSet, err = compat.Upgrade() + if err != nil { + return err + } + case C.RuleSetFormatBinary: + setFile, err := os.Open(path) + if err != nil { + return err + } + plainRuleSet, err = srs.Read(setFile, false) + if err != nil { + return err + } + default: + return E.New("unknown rule-set format: ", s.fileFormat) + } + return s.reloadRules(plainRuleSet.Rules) +} + +func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error { + rules := make([]adapter.HeadlessRule, len(headlessRules)) + var err error + for i, ruleOptions := range headlessRules { + rules[i], err = NewHeadlessRule(s.router, ruleOptions) + if err != nil { + return E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + var metadata adapter.RuleSetMetadata + metadata.ContainsProcessRule = hasHeadlessRule(headlessRules, isProcessHeadlessRule) + metadata.ContainsWIFIRule = hasHeadlessRule(headlessRules, isWIFIHeadlessRule) + metadata.ContainsIPCIDRRule = hasHeadlessRule(headlessRules, isIPCIDRHeadlessRule) + s.rules = rules + s.metadata = metadata return nil } @@ -118,7 +181,7 @@ func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetU func (s *LocalRuleSet) Close() error { s.rules = nil - return nil + return common.Close(common.PtrOrNil(s.watcher)) } func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go index 1473a494a9..03662ee42c 100644 --- a/route/rule_set_remote.go +++ b/route/rule_set_remote.go @@ -168,7 +168,10 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { if err != nil { return err } - plainRuleSet = compat.Upgrade() + plainRuleSet, err = compat.Upgrade() + if err != nil { + return err + } case C.RuleSetFormatBinary: plainRuleSet, err = srs.Read(bytes.NewReader(content), false) if err != nil { From 5a2c7037fe2e8703fc3330d04ce19a628b86d83d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 7 Jul 2024 15:45:50 +0800 Subject: [PATCH 16/25] Improve QUIC sniffer --- adapter/inbound.go | 9 +- common/ja3/LICENSE | 29 +++ common/ja3/README.md | 3 + common/ja3/error.go | 31 +++ common/ja3/ja3.go | 83 +++++++ common/ja3/parser.go | 357 +++++++++++++++++++++++++++ common/sniff/bittorrent.go | 44 ++-- common/sniff/bittorrent_test.go | 11 +- common/sniff/dns.go | 20 +- common/sniff/dtls.go | 13 +- common/sniff/dtls_test.go | 7 +- common/sniff/http.go | 8 +- common/sniff/http_test.go | 7 +- common/sniff/quic.go | 214 ++++++++++------ common/sniff/quic_blacklist.go | 24 ++ common/sniff/quic_test.go | 56 ++++- common/sniff/sniff.go | 29 ++- common/sniff/stun.go | 11 +- common/sniff/stun_test.go | 7 +- common/sniff/tls.go | 8 +- constant/protocol.go | 8 + docs/configuration/route/rule.md | 15 +- docs/configuration/route/rule.zh.md | 15 +- docs/configuration/route/sniff.md | 29 ++- docs/configuration/route/sniff.zh.md | 29 ++- go.mod | 2 +- option/rule.go | 1 + route/router.go | 139 ++++++----- route/rule_default.go | 5 + route/rule_item_client.go | 37 +++ 30 files changed, 1012 insertions(+), 239 deletions(-) create mode 100644 common/ja3/LICENSE create mode 100644 common/ja3/README.md create mode 100644 common/ja3/error.go create mode 100644 common/ja3/ja3.go create mode 100644 common/ja3/parser.go create mode 100644 common/sniff/quic_blacklist.go create mode 100644 route/rule_item_client.go diff --git a/adapter/inbound.go b/adapter/inbound.go index 56eb562308..ffc4c56454 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -31,11 +31,16 @@ type InboundContext struct { Network string Source M.Socksaddr Destination M.Socksaddr - Domain string - Protocol string User string Outbound string + // sniffer + + Protocol string + Domain string + Client string + SniffContext any + // cache InboundDetour string diff --git a/common/ja3/LICENSE b/common/ja3/LICENSE new file mode 100644 index 0000000000..b42ec95db2 --- /dev/null +++ b/common/ja3/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2018, Open Systems AG +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/common/ja3/README.md b/common/ja3/README.md new file mode 100644 index 0000000000..5c0bd8ae27 --- /dev/null +++ b/common/ja3/README.md @@ -0,0 +1,3 @@ +# JA3 + +mod from: https://github.com/open-ch/ja3 \ No newline at end of file diff --git a/common/ja3/error.go b/common/ja3/error.go new file mode 100644 index 0000000000..cab8549255 --- /dev/null +++ b/common/ja3/error.go @@ -0,0 +1,31 @@ +// Copyright (c) 2018, Open Systems AG. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +package ja3 + +import "fmt" + +// Error types +const ( + LengthErr string = "length check %v failed" + ContentTypeErr string = "content type not matching" + VersionErr string = "version check %v failed" + HandshakeTypeErr string = "handshake type not matching" + SNITypeErr string = "SNI type not supported" +) + +// ParseError can be encountered while parsing a segment +type ParseError struct { + errType string + check int +} + +func (e *ParseError) Error() string { + if e.errType == LengthErr || e.errType == VersionErr { + return fmt.Sprintf(e.errType, e.check) + } + return fmt.Sprint(e.errType) +} diff --git a/common/ja3/ja3.go b/common/ja3/ja3.go new file mode 100644 index 0000000000..608819fe1e --- /dev/null +++ b/common/ja3/ja3.go @@ -0,0 +1,83 @@ +// Copyright (c) 2018, Open Systems AG. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +package ja3 + +import ( + "crypto/md5" + "encoding/hex" + + "golang.org/x/exp/slices" +) + +type ClientHello struct { + Version uint16 + CipherSuites []uint16 + Extensions []uint16 + EllipticCurves []uint16 + EllipticCurvePF []uint8 + Versions []uint16 + SignatureAlgorithms []uint16 + ServerName string + ja3ByteString []byte + ja3Hash string +} + +func (j *ClientHello) Equals(another *ClientHello, ignoreExtensionsSequence bool) bool { + if j.Version != another.Version { + return false + } + if !slices.Equal(j.CipherSuites, another.CipherSuites) { + return false + } + if !ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.Extensions) { + return false + } + if ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.sortedExtensions()) { + return false + } + if !slices.Equal(j.EllipticCurves, another.EllipticCurves) { + return false + } + if !slices.Equal(j.EllipticCurvePF, another.EllipticCurvePF) { + return false + } + if !slices.Equal(j.SignatureAlgorithms, another.SignatureAlgorithms) { + return false + } + return true +} + +func (j *ClientHello) sortedExtensions() []uint16 { + extensions := make([]uint16, len(j.Extensions)) + copy(extensions, j.Extensions) + slices.Sort(extensions) + return extensions +} + +func Compute(payload []byte) (*ClientHello, error) { + ja3 := ClientHello{} + err := ja3.parseSegment(payload) + return &ja3, err +} + +func (j *ClientHello) String() string { + if j.ja3ByteString == nil { + j.marshalJA3() + } + return string(j.ja3ByteString) +} + +func (j *ClientHello) Hash() string { + if j.ja3ByteString == nil { + j.marshalJA3() + } + if j.ja3Hash == "" { + h := md5.Sum(j.ja3ByteString) + j.ja3Hash = hex.EncodeToString(h[:]) + } + return j.ja3Hash +} diff --git a/common/ja3/parser.go b/common/ja3/parser.go new file mode 100644 index 0000000000..f9cca60382 --- /dev/null +++ b/common/ja3/parser.go @@ -0,0 +1,357 @@ +// Copyright (c) 2018, Open Systems AG. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +package ja3 + +import ( + "encoding/binary" + "strconv" +) + +const ( + // Constants used for parsing + recordLayerHeaderLen int = 5 + handshakeHeaderLen int = 6 + randomDataLen int = 32 + sessionIDHeaderLen int = 1 + cipherSuiteHeaderLen int = 2 + compressMethodHeaderLen int = 1 + extensionsHeaderLen int = 2 + extensionHeaderLen int = 4 + sniExtensionHeaderLen int = 5 + ecExtensionHeaderLen int = 2 + ecpfExtensionHeaderLen int = 1 + versionExtensionHeaderLen int = 1 + signatureAlgorithmsExtensionHeaderLen int = 2 + contentType uint8 = 22 + handshakeType uint8 = 1 + sniExtensionType uint16 = 0 + sniNameDNSHostnameType uint8 = 0 + ecExtensionType uint16 = 10 + ecpfExtensionType uint16 = 11 + versionExtensionType uint16 = 43 + signatureAlgorithmsExtensionType uint16 = 13 + + // Versions + // The bitmask covers the versions SSL3.0 to TLS1.2 + tlsVersionBitmask uint16 = 0xFFFC + tls13 uint16 = 0x0304 + + // GREASE values + // The bitmask covers all GREASE values + GreaseBitmask uint16 = 0x0F0F + + // Constants used for marshalling + dashByte = byte(45) + commaByte = byte(44) +) + +// parseSegment to populate the corresponding ClientHello object or return an error +func (j *ClientHello) parseSegment(segment []byte) error { + // Check if we can decode the next fields + if len(segment) < recordLayerHeaderLen { + return &ParseError{LengthErr, 1} + } + + // Check if we have "Content Type: Handshake (22)" + contType := uint8(segment[0]) + if contType != contentType { + return &ParseError{errType: ContentTypeErr} + } + + // Check if TLS record layer version is supported + tlsRecordVersion := uint16(segment[1])<<8 | uint16(segment[2]) + if tlsRecordVersion&tlsVersionBitmask != 0x0300 && tlsRecordVersion != tls13 { + return &ParseError{VersionErr, 1} + } + + // Check that the Handshake is as long as expected from the length field + segmentLen := uint16(segment[3])<<8 | uint16(segment[4]) + if len(segment[recordLayerHeaderLen:]) < int(segmentLen) { + return &ParseError{LengthErr, 2} + } + // Keep the Handshake messege, ignore any additional following record types + hs := segment[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)] + + err := j.parseHandshake(hs) + + return err +} + +// parseHandshake body +func (j *ClientHello) parseHandshake(hs []byte) error { + // Check if we can decode the next fields + if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen { + return &ParseError{LengthErr, 3} + } + + // Check if we have "Handshake Type: Client Hello (1)" + handshType := uint8(hs[0]) + if handshType != handshakeType { + return &ParseError{errType: HandshakeTypeErr} + } + + // Check if actual length of handshake matches (this is a great exclusion criterion for false positives, + // as these fields have to match the actual length of the rest of the segment) + handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3]) + if len(hs[4:]) != int(handshakeLen) { + return &ParseError{LengthErr, 4} + } + + // Check if Client Hello version is supported + tlsVersion := uint16(hs[4])<<8 | uint16(hs[5]) + if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 { + return &ParseError{VersionErr, 2} + } + j.Version = tlsVersion + + // Check if we can decode the next fields + sessionIDLen := uint8(hs[38]) + if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) { + return &ParseError{LengthErr, 5} + } + + // Cipher Suites + cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):] + + // Check if we can decode the next fields + if len(cs) < cipherSuiteHeaderLen { + return &ParseError{LengthErr, 6} + } + + csLen := uint16(cs[0])<<8 | uint16(cs[1]) + numCiphers := int(csLen / 2) + cipherSuites := make([]uint16, 0, numCiphers) + + // Check if we can decode the next fields + if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen { + return &ParseError{LengthErr, 7} + } + + for i := 0; i < numCiphers; i++ { + cipherSuite := uint16(cs[2+i<<1])<<8 | uint16(cs[3+i<<1]) + cipherSuites = append(cipherSuites, cipherSuite) + } + j.CipherSuites = cipherSuites + + // Check if we can decode the next fields + compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)]) + if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) { + return &ParseError{LengthErr, 8} + } + + // Extensions + exs := cs[cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen):] + + err := j.parseExtensions(exs) + + return err +} + +// parseExtensions of the handshake +func (j *ClientHello) parseExtensions(exs []byte) error { + // Check for no extensions, this fields header is nonexistent if no body is used + if len(exs) == 0 { + return nil + } + + // Check if we can decode the next fields + if len(exs) < extensionsHeaderLen { + return &ParseError{LengthErr, 9} + } + + exsLen := uint16(exs[0])<<8 | uint16(exs[1]) + exs = exs[extensionsHeaderLen:] + + // Check if we can decode the next fields + if len(exs) < int(exsLen) { + return &ParseError{LengthErr, 10} + } + + var sni []byte + var extensions, ellipticCurves []uint16 + var ellipticCurvePF []uint8 + var versions []uint16 + var signatureAlgorithms []uint16 + for len(exs) > 0 { + + // Check if we can decode the next fields + if len(exs) < extensionHeaderLen { + return &ParseError{LengthErr, 11} + } + + exType := uint16(exs[0])<<8 | uint16(exs[1]) + exLen := uint16(exs[2])<<8 | uint16(exs[3]) + // Ignore any GREASE extensions + extensions = append(extensions, exType) + // Check if we can decode the next fields + if len(exs) < extensionHeaderLen+int(exLen) { + return &ParseError{LengthErr, 12} + } + + sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)] + + switch exType { + case sniExtensionType: // Extensions: server_name + + // Check if we can decode the next fields + if len(sex) < sniExtensionHeaderLen { + return &ParseError{LengthErr, 13} + } + + sniType := uint8(sex[2]) + sniLen := uint16(sex[3])<<8 | uint16(sex[4]) + sex = sex[sniExtensionHeaderLen:] + + // Check if we can decode the next fields + if len(sex) != int(sniLen) { + return &ParseError{LengthErr, 14} + } + + switch sniType { + case sniNameDNSHostnameType: + sni = sex + default: + return &ParseError{errType: SNITypeErr} + } + case ecExtensionType: // Extensions: supported_groups + + // Check if we can decode the next fields + if len(sex) < ecExtensionHeaderLen { + return &ParseError{LengthErr, 15} + } + + ecsLen := uint16(sex[0])<<8 | uint16(sex[1]) + numCurves := int(ecsLen / 2) + ellipticCurves = make([]uint16, 0, numCurves) + sex = sex[ecExtensionHeaderLen:] + + // Check if we can decode the next fields + if len(sex) != int(ecsLen) { + return &ParseError{LengthErr, 16} + } + + for i := 0; i < numCurves; i++ { + ecType := uint16(sex[i*2])<<8 | uint16(sex[1+i*2]) + ellipticCurves = append(ellipticCurves, ecType) + } + + case ecpfExtensionType: // Extensions: ec_point_formats + + // Check if we can decode the next fields + if len(sex) < ecpfExtensionHeaderLen { + return &ParseError{LengthErr, 17} + } + + ecpfsLen := uint8(sex[0]) + numPF := int(ecpfsLen) + ellipticCurvePF = make([]uint8, numPF) + sex = sex[ecpfExtensionHeaderLen:] + + // Check if we can decode the next fields + if len(sex) != numPF { + return &ParseError{LengthErr, 18} + } + + for i := 0; i < numPF; i++ { + ellipticCurvePF[i] = uint8(sex[i]) + } + case versionExtensionType: + if len(sex) < versionExtensionHeaderLen { + return &ParseError{LengthErr, 19} + } + versionsLen := int(sex[0]) + for i := 0; i < versionsLen; i += 2 { + versions = append(versions, binary.BigEndian.Uint16(sex[1:][i:])) + } + case signatureAlgorithmsExtensionType: + if len(sex) < signatureAlgorithmsExtensionHeaderLen { + return &ParseError{LengthErr, 20} + } + ssaLen := binary.BigEndian.Uint16(sex) + for i := 0; i < int(ssaLen); i += 2 { + signatureAlgorithms = append(signatureAlgorithms, binary.BigEndian.Uint16(sex[2:][i:])) + } + } + exs = exs[4+exLen:] + } + j.ServerName = string(sni) + j.Extensions = extensions + j.EllipticCurves = ellipticCurves + j.EllipticCurvePF = ellipticCurvePF + j.Versions = versions + j.SignatureAlgorithms = signatureAlgorithms + return nil +} + +// marshalJA3 into a byte string +func (j *ClientHello) marshalJA3() { + // An uint16 can contain numbers with up to 5 digits and an uint8 can contain numbers with up to 3 digits, but we + // also need a byte for each separating character, except at the end. + byteStringLen := 6*(1+len(j.CipherSuites)+len(j.Extensions)+len(j.EllipticCurves)) + 4*len(j.EllipticCurvePF) - 1 + byteString := make([]byte, 0, byteStringLen) + + // Version + byteString = strconv.AppendUint(byteString, uint64(j.Version), 10) + byteString = append(byteString, commaByte) + + // Cipher Suites + if len(j.CipherSuites) != 0 { + for _, val := range j.CipherSuites { + if val&GreaseBitmask != 0x0A0A { + continue + } + byteString = strconv.AppendUint(byteString, uint64(val), 10) + byteString = append(byteString, dashByte) + } + // Replace last dash with a comma + byteString[len(byteString)-1] = commaByte + } else { + byteString = append(byteString, commaByte) + } + + // Extensions + if len(j.Extensions) != 0 { + for _, val := range j.Extensions { + if val&GreaseBitmask != 0x0A0A { + continue + } + byteString = strconv.AppendUint(byteString, uint64(val), 10) + byteString = append(byteString, dashByte) + } + // Replace last dash with a comma + byteString[len(byteString)-1] = commaByte + } else { + byteString = append(byteString, commaByte) + } + + // Elliptic curves + if len(j.EllipticCurves) != 0 { + for _, val := range j.EllipticCurves { + if val&GreaseBitmask != 0x0A0A { + continue + } + byteString = strconv.AppendUint(byteString, uint64(val), 10) + byteString = append(byteString, dashByte) + } + // Replace last dash with a comma + byteString[len(byteString)-1] = commaByte + } else { + byteString = append(byteString, commaByte) + } + + // ECPF + if len(j.EllipticCurvePF) != 0 { + for _, val := range j.EllipticCurvePF { + byteString = strconv.AppendUint(byteString, uint64(val), 10) + byteString = append(byteString, dashByte) + } + // Remove last dash + byteString = byteString[:len(byteString)-1] + } + + j.ja3ByteString = byteString +} diff --git a/common/sniff/bittorrent.go b/common/sniff/bittorrent.go index debc35ca4f..9e123c41e9 100644 --- a/common/sniff/bittorrent.go +++ b/common/sniff/bittorrent.go @@ -19,45 +19,44 @@ const ( // BitTorrent detects if the stream is a BitTorrent connection. // For the BitTorrent protocol specification, see https://www.bittorrent.org/beps/bep_0003.html -func BitTorrent(_ context.Context, reader io.Reader) (*adapter.InboundContext, error) { +func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { var first byte err := binary.Read(reader, binary.BigEndian, &first) if err != nil { - return nil, err + return err } if first != 19 { - return nil, os.ErrInvalid + return os.ErrInvalid } var protocol [19]byte _, err = reader.Read(protocol[:]) if err != nil { - return nil, err + return err } if string(protocol[:]) != "BitTorrent protocol" { - return nil, os.ErrInvalid + return os.ErrInvalid } - return &adapter.InboundContext{ - Protocol: C.ProtocolBitTorrent, - }, nil + metadata.Protocol = C.ProtocolBitTorrent + return nil } // UTP detects if the packet is a uTP connection packet. // For the uTP protocol specification, see // 1. https://www.bittorrent.org/beps/bep_0029.html // 2. https://github.com/bittorrent/libutp/blob/2b364cbb0650bdab64a5de2abb4518f9f228ec44/utp_internal.cpp#L112 -func UTP(_ context.Context, packet []byte) (*adapter.InboundContext, error) { +func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) error { // A valid uTP packet must be at least 20 bytes long. if len(packet) < 20 { - return nil, os.ErrInvalid + return os.ErrInvalid } version := packet[0] & 0x0F ty := packet[0] >> 4 if version != 1 || ty > 4 { - return nil, os.ErrInvalid + return os.ErrInvalid } // Validate the extensions @@ -66,36 +65,35 @@ func UTP(_ context.Context, packet []byte) (*adapter.InboundContext, error) { for extension != 0 { err := binary.Read(reader, binary.BigEndian, &extension) if err != nil { - return nil, err + return err } var length byte err = binary.Read(reader, binary.BigEndian, &length) if err != nil { - return nil, err + return err } _, err = reader.Seek(int64(length), io.SeekCurrent) if err != nil { - return nil, err + return err } } - - return &adapter.InboundContext{ - Protocol: C.ProtocolBitTorrent, - }, nil + metadata.Protocol = C.ProtocolBitTorrent + return nil } // UDPTracker detects if the packet is a UDP Tracker Protocol packet. // For the UDP Tracker Protocol specification, see https://www.bittorrent.org/beps/bep_0015.html -func UDPTracker(_ context.Context, packet []byte) (*adapter.InboundContext, error) { +func UDPTracker(_ context.Context, metadata *adapter.InboundContext, packet []byte) error { if len(packet) < trackerConnectMinSize { - return nil, os.ErrInvalid + return os.ErrInvalid } if binary.BigEndian.Uint64(packet[:8]) != trackerProtocolID { - return nil, os.ErrInvalid + return os.ErrInvalid } if binary.BigEndian.Uint32(packet[8:12]) != trackerConnectFlag { - return nil, os.ErrInvalid + return os.ErrInvalid } - return &adapter.InboundContext{Protocol: C.ProtocolBitTorrent}, nil + metadata.Protocol = C.ProtocolBitTorrent + return nil } diff --git a/common/sniff/bittorrent_test.go b/common/sniff/bittorrent_test.go index 6b3ab64e95..65f095bdac 100644 --- a/common/sniff/bittorrent_test.go +++ b/common/sniff/bittorrent_test.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "testing" + "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" @@ -24,7 +25,8 @@ func TestSniffBittorrent(t *testing.T) { for _, pkt := range packets { pkt, err := hex.DecodeString(pkt) require.NoError(t, err) - metadata, err := sniff.BitTorrent(context.TODO(), bytes.NewReader(pkt)) + var metadata adapter.InboundContext + err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt)) require.NoError(t, err) require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) } @@ -43,8 +45,8 @@ func TestSniffUTP(t *testing.T) { for _, pkt := range packets { pkt, err := hex.DecodeString(pkt) require.NoError(t, err) - - metadata, err := sniff.UTP(context.TODO(), pkt) + var metadata adapter.InboundContext + err = sniff.UTP(context.TODO(), &metadata, pkt) require.NoError(t, err) require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) } @@ -63,7 +65,8 @@ func TestSniffUDPTracker(t *testing.T) { pkt, err := hex.DecodeString(pkt) require.NoError(t, err) - metadata, err := sniff.UDPTracker(context.TODO(), pkt) + var metadata adapter.InboundContext + err = sniff.UDPTracker(context.TODO(), &metadata, pkt) require.NoError(t, err) require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) } diff --git a/common/sniff/dns.go b/common/sniff/dns.go index d69f3b158c..96670eca9c 100644 --- a/common/sniff/dns.go +++ b/common/sniff/dns.go @@ -17,18 +17,17 @@ import ( mDNS "github.com/miekg/dns" ) -func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.InboundContext, error) { +func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundContext, reader io.Reader) error { var length uint16 err := binary.Read(reader, binary.BigEndian, &length) if err != nil { - return nil, err + return os.ErrInvalid } if length == 0 { - return nil, os.ErrInvalid + return os.ErrInvalid } buffer := buf.NewSize(int(length)) defer buffer.Release() - readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100) var readTask task.Group readTask.Append0(func(ctx context.Context) error { @@ -37,19 +36,20 @@ func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter. err = readTask.Run(readCtx) cancel() if err != nil { - return nil, err + return err } - return DomainNameQuery(readCtx, buffer.Bytes()) + return DomainNameQuery(readCtx, metadata, buffer.Bytes()) } -func DomainNameQuery(ctx context.Context, packet []byte) (*adapter.InboundContext, error) { +func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { var msg mDNS.Msg err := msg.Unpack(packet) if err != nil { - return nil, err + return err } if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) { - return nil, os.ErrInvalid + return os.ErrInvalid } - return &adapter.InboundContext{Protocol: C.ProtocolDNS}, nil + metadata.Protocol = C.ProtocolDNS + return nil } diff --git a/common/sniff/dtls.go b/common/sniff/dtls.go index 6469b09709..e2704a27ae 100644 --- a/common/sniff/dtls.go +++ b/common/sniff/dtls.go @@ -8,24 +8,25 @@ import ( C "github.com/sagernet/sing-box/constant" ) -func DTLSRecord(ctx context.Context, packet []byte) (*adapter.InboundContext, error) { +func DTLSRecord(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { const fixedHeaderSize = 13 if len(packet) < fixedHeaderSize { - return nil, os.ErrInvalid + return os.ErrInvalid } contentType := packet[0] switch contentType { case 20, 21, 22, 23, 25: default: - return nil, os.ErrInvalid + return os.ErrInvalid } versionMajor := packet[1] if versionMajor != 0xfe { - return nil, os.ErrInvalid + return os.ErrInvalid } versionMinor := packet[2] if versionMinor != 0xff && versionMinor != 0xfd { - return nil, os.ErrInvalid + return os.ErrInvalid } - return &adapter.InboundContext{Protocol: C.ProtocolDTLS}, nil + metadata.Protocol = C.ProtocolDTLS + return nil } diff --git a/common/sniff/dtls_test.go b/common/sniff/dtls_test.go index 45f7712642..48c9b42b31 100644 --- a/common/sniff/dtls_test.go +++ b/common/sniff/dtls_test.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "testing" + "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" @@ -15,7 +16,8 @@ func TestSniffDTLSClientHello(t *testing.T) { t.Parallel() packet, err := hex.DecodeString("16fefd0000000000000000007e010000720000000000000072fefd668a43523798e064bd806d0c87660de9c611a59bbdfc3892c4e072d94f2cafc40000000cc02bc02fc00ac014c02cc0300100003c000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100000e000900060008000700010000170000") require.NoError(t, err) - metadata, err := sniff.DTLSRecord(context.Background(), packet) + var metadata adapter.InboundContext + err = sniff.DTLSRecord(context.Background(), &metadata, packet) require.NoError(t, err) require.Equal(t, metadata.Protocol, C.ProtocolDTLS) } @@ -24,7 +26,8 @@ func TestSniffDTLSClientApplicationData(t *testing.T) { t.Parallel() packet, err := hex.DecodeString("17fefd000100000000000100440001000000000001a4f682b77ecadd10f3f3a2f78d90566212366ff8209fd77314f5a49352f9bb9bd12f4daba0b4736ae29e46b9714d3b424b3e6d0234736619b5aa0d3f") require.NoError(t, err) - metadata, err := sniff.DTLSRecord(context.Background(), packet) + var metadata adapter.InboundContext + err = sniff.DTLSRecord(context.Background(), &metadata, packet) require.NoError(t, err) require.Equal(t, metadata.Protocol, C.ProtocolDTLS) } diff --git a/common/sniff/http.go b/common/sniff/http.go index faf05fd304..0e6ab406c6 100644 --- a/common/sniff/http.go +++ b/common/sniff/http.go @@ -11,10 +11,12 @@ import ( "github.com/sagernet/sing/protocol/http" ) -func HTTPHost(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) { +func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { request, err := http.ReadRequest(std_bufio.NewReader(reader)) if err != nil { - return nil, err + return err } - return &adapter.InboundContext{Protocol: C.ProtocolHTTP, Domain: M.ParseSocksaddr(request.Host).AddrString()}, nil + metadata.Protocol = C.ProtocolHTTP + metadata.Domain = M.ParseSocksaddr(request.Host).AddrString() + return nil } diff --git a/common/sniff/http_test.go b/common/sniff/http_test.go index 98100e092a..9f64efa85e 100644 --- a/common/sniff/http_test.go +++ b/common/sniff/http_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" "github.com/stretchr/testify/require" @@ -13,7 +14,8 @@ import ( func TestSniffHTTP1(t *testing.T) { t.Parallel() pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n" - metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt)) + var metadata adapter.InboundContext + err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt)) require.NoError(t, err) require.Equal(t, metadata.Domain, "www.google.com") } @@ -21,7 +23,8 @@ func TestSniffHTTP1(t *testing.T) { func TestSniffHTTP1WithPort(t *testing.T) { t.Parallel() pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n" - metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt)) + var metadata adapter.InboundContext + err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt)) require.NoError(t, err) require.Equal(t, metadata.Domain, "www.gov.cn") } diff --git a/common/sniff/quic.go b/common/sniff/quic.go index 55dc1c0023..adec7008b1 100644 --- a/common/sniff/quic.go +++ b/common/sniff/quic.go @@ -5,95 +5,99 @@ import ( "context" "crypto" "crypto/aes" + "crypto/tls" "encoding/binary" "io" "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/ja3" "github.com/sagernet/sing-box/common/sniff/internal/qtls" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" "golang.org/x/crypto/hkdf" ) -func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContext, error) { - reader := bytes.NewReader(packet) +var ErrClientHelloFragmented = E.New("need more packet for chromium QUIC connection") +func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { + reader := bytes.NewReader(packet) typeByte, err := reader.ReadByte() if err != nil { - return nil, err + return err } if typeByte&0x40 == 0 { - return nil, E.New("bad type byte") + return E.New("bad type byte") } var versionNumber uint32 err = binary.Read(reader, binary.BigEndian, &versionNumber) if err != nil { - return nil, err + return err } if versionNumber != qtls.VersionDraft29 && versionNumber != qtls.Version1 && versionNumber != qtls.Version2 { - return nil, E.New("bad version") + return E.New("bad version") } packetType := (typeByte & 0x30) >> 4 if packetType == 0 && versionNumber == qtls.Version2 || packetType == 2 && versionNumber != qtls.Version2 || packetType > 2 { - return nil, E.New("bad packet type") + return E.New("bad packet type") } destConnIDLen, err := reader.ReadByte() if err != nil { - return nil, err + return err } if destConnIDLen == 0 || destConnIDLen > 20 { - return nil, E.New("bad destination connection id length") + return E.New("bad destination connection id length") } destConnID := make([]byte, destConnIDLen) _, err = io.ReadFull(reader, destConnID) if err != nil { - return nil, err + return err } srcConnIDLen, err := reader.ReadByte() if err != nil { - return nil, err + return err } _, err = io.CopyN(io.Discard, reader, int64(srcConnIDLen)) if err != nil { - return nil, err + return err } tokenLen, err := qtls.ReadUvarint(reader) if err != nil { - return nil, err + return err } _, err = io.CopyN(io.Discard, reader, int64(tokenLen)) if err != nil { - return nil, err + return err } packetLen, err := qtls.ReadUvarint(reader) if err != nil { - return nil, err + return err } hdrLen := int(reader.Size()) - reader.Len() if hdrLen+int(packetLen) > len(packet) { - return nil, os.ErrInvalid + return os.ErrInvalid } _, err = io.CopyN(io.Discard, reader, 4) if err != nil { - return nil, err + return err } pnBytes := make([]byte, aes.BlockSize) _, err = io.ReadFull(reader, pnBytes) if err != nil { - return nil, err + return err } var salt []byte @@ -117,7 +121,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex hpKey := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, hkdfHeaderProtectionLabel, 16) block, err := aes.NewCipher(hpKey) if err != nil { - return nil, err + return err } mask := make([]byte, aes.BlockSize) block.Encrypt(mask, pnBytes) @@ -129,7 +133,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex } packetNumberLength := newPacket[0]&0x3 + 1 if hdrLen+int(packetNumberLength) > int(packetLen)+hdrLen { - return nil, os.ErrInvalid + return os.ErrInvalid } var packetNumber uint32 switch packetNumberLength { @@ -142,7 +146,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex case 4: packetNumber = binary.BigEndian.Uint32(newPacket[hdrLen:]) default: - return nil, E.New("bad packet number length") + return E.New("bad packet number length") } extHdrLen := hdrLen + int(packetNumberLength) copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:]) @@ -166,138 +170,208 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber)) decrypted, err := cipher.Open(newPacket[extHdrLen:extHdrLen], nonce, data, newPacket[:extHdrLen]) if err != nil { - return nil, err + return err } var frameType byte - var frameLen uint64 - var fragments []struct { - offset uint64 - length uint64 - payload []byte - } + var fragments []qCryptoFragment decryptedReader := bytes.NewReader(decrypted) + const ( + frameTypePadding = 0x00 + frameTypePing = 0x01 + frameTypeAck = 0x02 + frameTypeAck2 = 0x03 + frameTypeCrypto = 0x06 + frameTypeConnectionClose = 0x1c + ) + var frameTypeList []uint8 for { frameType, err = decryptedReader.ReadByte() if err == io.EOF { break } + frameTypeList = append(frameTypeList, frameType) switch frameType { - case 0x00: // PADDING + case frameTypePadding: continue - case 0x01: // PING + case frameTypePing: continue - case 0x02, 0x03: // ACK + case frameTypeAck, frameTypeAck2: _, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged if err != nil { - return nil, err + return err } _, err = qtls.ReadUvarint(decryptedReader) // ACK Delay if err != nil { - return nil, err + return err } ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count if err != nil { - return nil, err + return err } _, err = qtls.ReadUvarint(decryptedReader) // First ACK Range if err != nil { - return nil, err + return err } for i := 0; i < int(ackRangeCount); i++ { _, err = qtls.ReadUvarint(decryptedReader) // Gap if err != nil { - return nil, err + return err } _, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length if err != nil { - return nil, err + return err } } if frameType == 0x03 { _, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count if err != nil { - return nil, err + return err } _, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count if err != nil { - return nil, err + return err } _, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count if err != nil { - return nil, err + return err } } - case 0x06: // CRYPTO + case frameTypeCrypto: var offset uint64 offset, err = qtls.ReadUvarint(decryptedReader) if err != nil { - return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err + return err } var length uint64 length, err = qtls.ReadUvarint(decryptedReader) if err != nil { - return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err + return err } index := len(decrypted) - decryptedReader.Len() - fragments = append(fragments, struct { - offset uint64 - length uint64 - payload []byte - }{offset, length, decrypted[index : index+int(length)]}) - frameLen += length + fragments = append(fragments, qCryptoFragment{offset, length, decrypted[index : index+int(length)]}) _, err = decryptedReader.Seek(int64(length), io.SeekCurrent) if err != nil { - return nil, err + return err } - case 0x1c: // CONNECTION_CLOSE + case frameTypeConnectionClose: _, err = qtls.ReadUvarint(decryptedReader) // Error Code if err != nil { - return nil, err + return err } _, err = qtls.ReadUvarint(decryptedReader) // Frame Type if err != nil { - return nil, err + return err } var length uint64 length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length if err != nil { - return nil, err + return err } _, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase if err != nil { - return nil, err + return err } default: - return nil, os.ErrInvalid + return os.ErrInvalid } } - tlsHdr := make([]byte, 5) - tlsHdr[0] = 0x16 - binary.BigEndian.PutUint16(tlsHdr[1:], uint16(0x0303)) - binary.BigEndian.PutUint16(tlsHdr[3:], uint16(frameLen)) + if metadata.SniffContext != nil { + fragments = append(fragments, metadata.SniffContext.([]qCryptoFragment)...) + metadata.SniffContext = nil + } + var frameLen uint64 + for _, fragment := range fragments { + frameLen += fragment.length + } + buffer := buf.NewSize(5 + int(frameLen)) + defer buffer.Release() + buffer.WriteByte(0x16) + binary.Write(buffer, binary.BigEndian, uint16(0x0303)) + binary.Write(buffer, binary.BigEndian, uint16(frameLen)) var index uint64 var length int - var readers []io.Reader - readers = append(readers, bytes.NewReader(tlsHdr)) find: for { for _, fragment := range fragments { if fragment.offset == index { - readers = append(readers, bytes.NewReader(fragment.payload)) + buffer.Write(fragment.payload) index = fragment.offset + fragment.length length++ continue find } } - if length == len(fragments) { + break + } + metadata.Protocol = C.ProtocolQUIC + fingerprint, err := ja3.Compute(buffer.Bytes()) + if err != nil { + metadata.Protocol = C.ProtocolQUIC + metadata.Client = C.ClientChromium + metadata.SniffContext = fragments + return ErrClientHelloFragmented + } + metadata.Domain = fingerprint.ServerName + for metadata.Client == "" { + if len(frameTypeList) == 1 { + metadata.Client = C.ClientFirefox + break + } + if frameTypeList[0] == frameTypeCrypto && isZero(frameTypeList[1:]) { + if len(fingerprint.Versions) == 2 && fingerprint.Versions[0]&ja3.GreaseBitmask == 0x0A0A && + len(fingerprint.EllipticCurves) == 5 && fingerprint.EllipticCurves[0]&ja3.GreaseBitmask == 0x0A0A { + metadata.Client = C.ClientSafari + break + } + if len(fingerprint.CipherSuites) == 1 && fingerprint.CipherSuites[0] == tls.TLS_AES_256_GCM_SHA384 && + len(fingerprint.EllipticCurves) == 1 && fingerprint.EllipticCurves[0] == uint16(tls.X25519) && + len(fingerprint.SignatureAlgorithms) == 1 && fingerprint.SignatureAlgorithms[0] == uint16(tls.ECDSAWithP256AndSHA256) { + metadata.Client = C.ClientSafari + break + } + } + + if frameTypeList[len(frameTypeList)-1] == frameTypeCrypto && isZero(frameTypeList[:len(frameTypeList)-1]) { + metadata.Client = C.ClientQUICGo break } - return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, E.New("bad fragments") + + if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 { + if maybeUQUIC(fingerprint) { + metadata.Client = C.ClientQUICGo + } else { + metadata.Client = C.ClientChromium + } + break + } + + metadata.Client = C.ClientUnknown + //nolint:staticcheck + break } - metadata, err := TLSClientHello(ctx, io.MultiReader(readers...)) - if err != nil { - return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err + return nil +} + +func isZero(slices []uint8) bool { + for _, slice := range slices { + if slice != 0 { + return false + } } - metadata.Protocol = C.ProtocolQUIC - return metadata, nil + return true +} + +func count(slices []uint8, value uint8) int { + var times int + for _, slice := range slices { + if slice == value { + times++ + } + } + return times +} + +type qCryptoFragment struct { + offset uint64 + length uint64 + payload []byte } diff --git a/common/sniff/quic_blacklist.go b/common/sniff/quic_blacklist.go new file mode 100644 index 0000000000..7d5f91e79b --- /dev/null +++ b/common/sniff/quic_blacklist.go @@ -0,0 +1,24 @@ +package sniff + +import ( + "crypto/tls" + + "github.com/sagernet/sing-box/common/ja3" +) + +// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior +// The cronet without this behavior does not have version 115 +var uQUICChrome115 = &ja3.ClientHello{ + Version: tls.VersionTLS12, + CipherSuites: []uint16{4865, 4866, 4867}, + Extensions: []uint16{0, 10, 13, 16, 27, 43, 45, 51, 57, 17513}, + EllipticCurves: []uint16{29, 23, 24}, + SignatureAlgorithms: []uint16{1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513}, +} + +func maybeUQUIC(fingerprint *ja3.ClientHello) bool { + if uQUICChrome115.Equals(fingerprint, true) { + return true + } + return false +} diff --git a/common/sniff/quic_test.go b/common/sniff/quic_test.go index 807197654b..e75e86a5d2 100644 --- a/common/sniff/quic_test.go +++ b/common/sniff/quic_test.go @@ -5,31 +5,69 @@ import ( "encoding/hex" "testing" + "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" + C "github.com/sagernet/sing-box/constant" "github.com/stretchr/testify/require" ) -func TestSniffQUICv1(t *testing.T) { +func TestSniffQUICChromium(t *testing.T) { t.Parallel() - pkt, err := hex.DecodeString("cc0000000108d2dc7bad02241f5003796e71004215a71bfcb05159416c724be418537389acdd9a4047306283dcb4d7a9cad5cc06322042d204da67a8dbaa328ab476bb428b48fd001501863afd203f8d4ef085629d664f1a734a65969a47e4a63d4e01a21f18c1d90db0c027180906dc135f9ae421bb8617314c8d54c175fef3d3383d310d0916ebcbd6eed9329befbbb109d8fd4af1d2cf9d6adce8e6c1260a7f8256e273e326da0aa7cc148d76e7a08489dc9d52ade89c027cbc3491ada46417c2c04e2ca768e9a7dd6aa00c594e48b678927325da796817693499bb727050cb3baf3d3291a397c3a8d868e8ec7b8f7295e347455c9dadbe2252ae917ac793d958c7fb8a3d2cdb34e3891eb4286f18617556ff7216dd60256aa5b1d11ff4753459fc5f9dedf11d483a26a0835dc6cd50e1c1f54f86e8f1e502821183cd874f6447a74e818bf3445c7795acf4559d1c1fac474911d2ead5c8d23e4aa4f67afb66efe305a30a0b5d825679b31ddc186cbea936535795c7e8c378c87b8c5adc065154d15bae8f85ac8fec2da40c3aa623b682a065440831555011d7647cde44446a0fb4cf5892f2c088ae1920643094be72e3c499fe8d265caf939e8ab607a5b9317917d2a32a812e8a0e6a2f84721bbb5984ffd242838f705d13f4cfb249bc6a5c80d58ac2595edf56648ec3fe21d787573c253a79805252d6d81e26d367d4ff29ef66b5fe8992086af7bada8cad10b82a7c0dc406c5b6d0c5ec3c583e767f759ce08cad6c3c8f91e5a8") + pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620") require.NoError(t, err) - metadata, err := sniff.QUICClientHello(context.Background(), pkt) + var metadata adapter.InboundContext + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) + require.Equal(t, metadata.Protocol, C.ProtocolQUIC) + require.Equal(t, metadata.Client, C.ClientChromium) + require.ErrorIs(t, err, sniff.ErrClientHelloFragmented) + pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28") require.NoError(t, err) - require.Equal(t, metadata.Domain, "cloudflare-quic.com") + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) + require.NoError(t, err) + require.Equal(t, metadata.Domain, "google.com") +} + +func TestSniffUQUICChrome115(t *testing.T) { + t.Parallel() + pkt, err := hex.DecodeString("cb0000000108181e17c387120abc000044d0705b6a3ef9ee37a8d3949a7d393ed078243c2ee2c3627fad1c3f107c117f4f071131ad61848068fcbbe5c65803c147f7f8ec5e2cd77b77beea23ba779d936dccac540f8396400e3190ea35cc2942af4171a04cb14272491920f90124959f44e80143678c0b52f5d31af319aaa589db2f940f004562724d0af40f737e1bb0002a071e6a1dbc9f52c64f070806a5010abed0298053634d9c9126bd7949ae5087998ade762c0ad06691d99c0875a38c601fc1ee77bfc3b8c11381829f2c9bdd022f4499c43ff1d6aee1a0d296861461dda217d22c568b276016ef3929e59d2f7d7ddf7809920fb7dc805641608949f3f8466ab3d37149aac501f0b107d808f3add4acfc657e4a82e2b88e97a6c74a00c419548760ab3414ba13915c78a1ca79dceee8d59fbe299f20b671ac44823218368b2a026baa55170cf549519ac21dbb6d31d248bd339438a4e663bcdca1fe3ae3f045a5dc19b122e9db9d7af9757076666dda4e9ace1c67def77fa14786f0cab3ebf7a270ea6e2b37838318c95779f80c3b8471948d0046c3614b3a13477c939a39a7855d85d13522a45ae0765739cd5eedef87237e824a929983ace27640c6495dbf5a72fa0b96893dc5d28f3988249a57bdb458d460b4a57043de3da750a76b6e5d2259247ca27cd864ea18f0d09aa62ab6eb7c014fb43179b2a1963d170b756cce83eeaebff78a828d025c811848e16ff862a8080d093478cd2208c8ab0803178325bc0d9d6bb25e62fa50c4ad15cf80916da6578796932036c72e43eb480d1e423ed812ac75a97722f8416529b82ba8ee2219c535012282bb17066bd53e78b87a71abdb7ebdb2a7c2766ff8397962e87d0f85485b64b4ee81cc84f99c47f33f2b0872716441992773f59186e38d32dbf5609a6fda94cb928cd25f5a7a3ab736b5a4236b6d5409ab18892c6a4d3480fc2350abfdf0bab1cedb55bdf0760fdb703e6688f4de596254eed4ed3e67eb03d0717b8e15b31e735214e588c87ae36bc6c310e1894b4c15143e4ccf287b2dbc707a946bf9671ae3c574f9486b2c82eec784bba4cbc76113cbe0f97ac8c13cfa38f2925ab9d06887a612ce48280a91d7e074e6caf898d88e2bbf71360899abf48a03f9a70cf2891199f2d63b116f4871af0ebb4f4906792f66cc21d1609f189138532875c129a68c73e7bcd3b5d8100beac1d8ac4b20d94a59ac8df5a5af58a9acb20413eadf97189f5f19ff889155f0c4d37514ec184eb6903967ff38a41fc087abb0f2cad3761d6e3f95f92a09a72f5c065b16e188088b87460241f27ecdb1bc6ece92c8d36b2d68b58d0fb4d4b3c928c579ade8ae5a995833aadd297c30a37f7bc35440fc97070e1b198e0fac00157452177d16d2803b4239997452b4ad3a951173bdec47a033fd7f8a7942accaa9aaa905b3c5a2175e7c3e07c48bf25331727fd69cd1e64d74d8c9d4a6f8f4491adb7bc911505cb19877083d8f21a12475e313fccf57877ff3556318e81ed9145dd9427f2b65275440893035f417481f721c69215af8ae103530cd0a1d35bf2cb5a27628f8d44d7c6f5ec12ce79d0a8333e0eb48771115d0a191304e46b8db19bbe5c40f1c346dde98e76ff5e21ff38d2c34e60cb07766ed529dd6d2cbacd7fbf1ed8a0e6e40decad0ca5021e91552be87c156d3ae2fffef41c65b14ba6d488f2c3227a1ab11ffce0e2dc47723a69da27a67a7f26e1cb13a7103af9b87a8db8e18ea") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) + require.NoError(t, err) + require.Equal(t, metadata.Protocol, C.ProtocolQUIC) + require.Equal(t, metadata.Client, C.ClientQUICGo) + require.Equal(t, metadata.Domain, "www.google.com") +} + +func TestSniffQUICFirefox(t *testing.T) { + t.Parallel() + pkt, err := hex.DecodeString("c8000000010867f174d7ebfe1b0803cd9c20004286de068f7963cf1736349ee6ebe0ddcd3e4cd0041a51ced3f7ce9eea1fb595458e74bdb4b792b16449bd8cae71419862c4fcbe766eaec7d1af65cd298e1dd46f8bd94a77ab4ca28c54b8e9773de3f02d7cb2463c9f7dcacfb311f024b0266ec6ab7bfb615b4148333fb4d4ece7c4cd90029ca30c2cbae2216b428499ec873fa125797e71c5a5da85087760ad37ca610020f71b76e82651c47576e20bf33cf676cb2d400b8c09d3c8cb4e21c47d2b21f6b68732bef30c8cefd5c723fc23eb29e6f7f65a5e52aad9055c1fb3d8b1811f0380b38d7e2eee8eb37dd5bd5d4ca4b66540175d916289d88a9df7c161964d713999c5057d27edb298ef5164352568b0d4bac3c15d90456e8fd460e41b81d0ec1b1e94b87d3333cc6908b018e0914ae1f214d73e75398da3d55a0106161d3a75897b4eb66e98c59010fae75f0d367d38be48c3a5c58bc8a30773c3fff50690ac9d487822f85d4f5713d626baa92d36e858dd21259cf814bce0b90d18da88a1ade40113e5a088cdb304a2558879152a8cf15c1839e056378aa41acba6fcb9974dee54bd50b5d4eb2c475654e06c0ec06b7f18f4462c808684843a1071041b9bfb2688324e0120144944416e30e83eedbbbcbc275b1f53762d3db18f0998ce54f0e1c512946b4098f07781d49264fa148f4c8220a3b02e73d7f15554aa370aafeff73cb75c52c494edf90f0261abfdd32a4d670f729de50266162687aa8efe14b8506f313b058b02aaaab5825428f5f4510b8e49451fdcb7b5a4af4b59c831afcb89fb4f64dba78e3b38387e87e9e8cdaa1f3b700a87c7d442388863b8950296e5773b38f308d62f52548c0bbf308e40540747cca5bf99b1345bc0d70b8f0e69a83b85a8d69f795b87f93e2bfccf52b529afea4ff6fd456957000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) + require.NoError(t, err) + require.Equal(t, metadata.Protocol, C.ProtocolQUIC) + require.Equal(t, metadata.Client, C.ClientFirefox) + require.Equal(t, metadata.Domain, "www.google.com") } -func TestSniffQUICFragment(t *testing.T) { +func TestSniffQUICSafari(t *testing.T) { t.Parallel() - pkt, err := hex.DecodeString("cc00000001082e3d5d1b64040c55000044d0ccea69e773f6631c1d18b04ae9ee75fcfc34ef74fa62533c93534338a86f101a05d70e0697fb483063fa85db1c59ccfbda5c35234931d8524d8aac37eaaad649470a67794cd754b23c98695238b8363452333bc8c4858376b4166e001da2006e35cf98a91e11a56419b2786775284942d0f7163982f7c248867d12dd374957481dbc564013ff785e1916195eef671f725908f761099d992d69231336ba81d9e25fe2fa3a6eff4318a6ccf10176fc841a1b315f7b35c5b292266fc869d76ca533e7d14e86d82db2e22eacd350977e47d2e012d8a5891c5aaf2a0f4c2b2dae897c161e5b68cbb4dee952472bdc1e21504b8f02534ec4366ce3f8bf86efc78e0232778fbd554457567112abdcafcf6d4d8fcf35083c25d9495679614aba21696e338c62b585046cc55ba8c09c844361d889a47c3ea703b4e23545a9ab2c0bb369693a9ddfb5daffa85cf80fdd6ad66738664e5b0a551729b4955cff7255afcb04dee88c2f072c9de7400947a1bd9327ac5d012a33000ada021d4c03d249fb017d6ac9200b2f9436beab8183ddfbe2d8aee31ffb7df9e1cc181c1af80c39a89965d18ed12da8e3ebe2ae1fbe4b348f83ba19e3e3d1c9b22bcf03ab6ad9b30fe180623faa291ebad83bcd71d7b57f2f5e2f3b8e81d24fb70b2f2159239e8f21ffafef2747aba47d97ab4081e603c018b10678cf99cab1fb42156a14486fa435153979d7279fd22cd40af7088bfc7eff41af2f4b3c0c8864d0040d74dff427f7bffdb8c278474ea00311326cf4925471a8cf596cb92119f19e0f789490ba9cb77b98015a987d93e0324cf1a38b55109f00c3e6ddc5180fb107bf468323afec9bb49fd6a86418569789d66cafe3b8253c2aebb3af3782c1c54dd560487d031d28e6a6e23e159581bb1d47efc4da3fe1d169f9ffb0ca9ba61af0a38a92fde5bc5e6ec026e8378a6315a7b95abf1d2da790a391306ce74d0baf8e2ce648ca74c487f2c0a76a28a80cdf5bd34316eb607684fe7e6d9e83824a00e07660d0b90e3cddd61ebf10748263474afa88c300549e64ce2e90560bb1a12dee7e9484f729a8a4ee7c5651adb5194b3b3ae38e501567c7dbf36e7bb37a2c20b74655f47f2d9af18e52e9d4c9c9eee8e63745779b8f0b06f3a09d846ba62eb978ad77c85de1ee2fee3fbb4c2d283c73e1ccba56a4658e48a2665d200f7f9342f8e84c2ba490094a4f94feec89e42d2f654f564c2beb2997bafa1fc2c68ad8e160b63587d49abc31b834878d52acfb05fb73d0e059b206162e3c90b40c4bc08407ffcb3c08431895b691a3fea923f1f3b48db75d3e6b91fd319ffe4d486e0e14bd5c6affc838dee63d9e0b80f169b5e6c02c7321dcb20deb2b8e707b60e345a308d505bbf26a93d8f18b39d62632e9a77cbe48b3b32eb8819d6311a49820d40f5acbf0273c91c36b2269a03e72ee64df3dfb10ddefe73c64ef60870b2b77bd99dea655f5fe791b538a929a14d99f6d69685d72431ea5f0f4b27a044f2f575ab474fcc3857895934de1ca2581798eaef2c17fe5aaf2e6add97fa32997c7026f15c1b1ad0e6043ae506027a7c0242546fdc851cca39a204e56879f2cef838be8ec66e0f2292f8c862e06f810eb9b80c7a467ce6e90155206352c7f82b1173ba3b98d35bb72c259a60db20dd1a43fe6d7aef0265e6eaa5caafd9b64b448ff745a2046acbdb65cf2a5007809808a4828dc99097feedc734c236260c584") + pkt, err := hex.DecodeString("c70000000108e4e75af2e223198a0000449ef2d83cb4473a62765eba67424cd4a5817315cbf55a9e8daaca360904b0bae60b1629cfeba11e2dfbbf5ea4c588cb134e31af36fd7a409fb0fcc0187e9b56037ac37964ed20a8c1ca19fd6cfd53398324b3d0c71537294f769db208fa998b6811234a4a7eb3b5eceb457ae92e3a2d98f7c110702db8064b5c29fa3298eb1d0529fd445a84a5fd6ff8709be90f8af4f94998d8a8f2953bb05ad08c80668eca784c6aec959114e68e5b827e7c41c79f2277c716a967e7fcc8d1b77442e6cb18329dbedb34b473516b468cba5fc20659e655fbe37f36408289b9a475fcee091bd82828d3be00367e9e5cec9423bb97854abdada1d7562a3777756eb3bddef826ddc1ef46137cb01bb504a54d410d9bcb74cd5f959050c84edf343fa6a49708c228a758ee7adbbadf260b2f1984911489712e2cb364a3d6520badba4b7e539b9c163eeddfd96c0abb0de151e47496bb9750be76ee17ccdb61d35d2c6795174037d6f9d282c3f36c4d9a90b64f3b6ddd0cf4d9ed8e6f7805e25928fa04b087e63ae02761df30720cc01dfc32b64c575c8a66ef82e9a17400ff80cd8609b93ba16d668f4aa734e71c4a5d145f14ee1151bec970214e0ff83fc3e1e85d8694f2975f9155c57c18b7b69bb6a36832a9435f1f4b346a7be188f3a75f9ad2cc6ad0a3d26d6fa7d4c1179bd49bd5989d15ba43ff602890107db96484695086627356750d7b2b3b714ba65d564654e8f60ac10f5b6d3bfb507e8eaa31bab1da2d676195046d165c7f8b32829c9f9b68d97b2af7ac04a1369357e4b65de2b2f24eaf27cc8d95e05db001adebe726f927a94e43e62ce671e6e306e16f05aafcbe6c49080e80286d7939f375023d110a5ad9069364ae928ca480454a9dcddd61bc48b7efeb716a5bd6c7cd39c486ceb20c738af6abf22ba1ddd8b4a3b781fc2f251173409e1aadccbd7514e97106d0ebfc3af6e59445f74cd733a1ba99b10fce3fb4e9f7c88f5e25b567f5ba2b8dabacd375e7faf7634bfa178cbe51aee63032c5126b196ea47b02385fc3062a000fb7e4b4d0d12e74579f8830ede20d10829496032b2cc56743287f9a9b4d5091877a82fea44deb2cffac8a379f78a151d99e28cbc74d732c083bf06d50584e3f18f254e71a48d6ababaf6fff6f425e9be001510dfbe6a32a27792c00ada036b62ddb90c706d7b882c76a7072f5dd11c69a1f49d4ba183cb0b57545419fa27b9b9706098848935ae9c9e8fbe9fac165d1339128b991a73d20e7795e8d6a8c6adfbf20bf13ada43f2aef3ba78c14697910507132623f721387dce60c4707225b84d9782d469a5d9eaa099f35d6a590ef142ddef766495cf3337815ceef5ff2b3ed352637e72b5c23a2a8ff7d7440236a19b981d47f8e519a0431ebfbc0b78d8a36798b4c060c0c6793499f1e2e818862560a5b501c8d02ba1517be1941da2af5b174e0189c62978d878eb0f9c9db3a9221c28fb94645cf6e85ff2eea8c65ba3083a7382b131b83102dd67aa5453ad7375a4eb8c69fc479fbd29dab8924f801d253f2c997120b705c6e5217fb74702e2f1038917dd5fb0eeb7ae1bf7a668fc7d50c034b4cd5a057a8482e6bc9c921297f44e76967265623a167cd9883eb6e64bc77856dc333bd605d7df3bed0e5cecb5a99fe8b62873d58530f") require.NoError(t, err) - metadata, err := sniff.QUICClientHello(context.Background(), pkt) + var metadata adapter.InboundContext + err = sniff.QUICClientHello(context.Background(), &metadata, pkt) require.NoError(t, err) - require.Equal(t, metadata.Domain, "cloudflare-quic.com") + require.Equal(t, metadata.Protocol, C.ProtocolQUIC) + require.Equal(t, metadata.Client, C.ClientSafari) + require.Equal(t, metadata.Domain, "www.google.com") } func FuzzSniffQUIC(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { - sniff.QUICClientHello(context.Background(), data) + var metadata adapter.InboundContext + err := sniff.QUICClientHello(context.Background(), &metadata, data) + require.Error(t, err) }) } diff --git a/common/sniff/sniff.go b/common/sniff/sniff.go index 2d2999a83e..80bb2984f9 100644 --- a/common/sniff/sniff.go +++ b/common/sniff/sniff.go @@ -14,8 +14,8 @@ import ( ) type ( - StreamSniffer = func(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) - PacketSniffer = func(ctx context.Context, packet []byte) (*adapter.InboundContext, error) + StreamSniffer = func(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error + PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error ) func Skip(metadata adapter.InboundContext) bool { @@ -34,7 +34,7 @@ func Skip(metadata adapter.InboundContext) bool { return false } -func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) (*adapter.InboundContext, error) { +func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error { if timeout == 0 { timeout = C.ReadPayloadTimeout } @@ -43,7 +43,7 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout for i := 0; ; i++ { err := conn.SetReadDeadline(deadline) if err != nil { - return nil, E.Cause(err, "set read deadline") + return E.Cause(err, "set read deadline") } _, err = buffer.ReadOnceFrom(conn) _ = conn.SetReadDeadline(time.Time{}) @@ -51,29 +51,28 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout if i > 0 { break } - return nil, E.Cause(err, "read payload") + return E.Cause(err, "read payload") } errors = nil - var metadata *adapter.InboundContext for _, sniffer := range sniffers { - metadata, err = sniffer(ctx, bytes.NewReader(buffer.Bytes())) - if metadata != nil { - return metadata, nil + err = sniffer(ctx, metadata, bytes.NewReader(buffer.Bytes())) + if err == nil { + return nil } errors = append(errors, err) } } - return nil, E.Errors(errors...) + return E.Errors(errors...) } -func PeekPacket(ctx context.Context, packet []byte, sniffers ...PacketSniffer) (*adapter.InboundContext, error) { +func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error { var errors []error for _, sniffer := range sniffers { - metadata, err := sniffer(ctx, packet) - if metadata != nil { - return metadata, nil + err := sniffer(ctx, metadata, packet) + if err == nil { + return nil } errors = append(errors, err) } - return nil, E.Errors(errors...) + return E.Errors(errors...) } diff --git a/common/sniff/stun.go b/common/sniff/stun.go index 66a72d7e17..dfd1125978 100644 --- a/common/sniff/stun.go +++ b/common/sniff/stun.go @@ -9,16 +9,17 @@ import ( C "github.com/sagernet/sing-box/constant" ) -func STUNMessage(ctx context.Context, packet []byte) (*adapter.InboundContext, error) { +func STUNMessage(_ context.Context, metadata *adapter.InboundContext, packet []byte) error { pLen := len(packet) if pLen < 20 { - return nil, os.ErrInvalid + return os.ErrInvalid } if binary.BigEndian.Uint32(packet[4:8]) != 0x2112A442 { - return nil, os.ErrInvalid + return os.ErrInvalid } if len(packet) < 20+int(binary.BigEndian.Uint16(packet[2:4])) { - return nil, os.ErrInvalid + return os.ErrInvalid } - return &adapter.InboundContext{Protocol: C.ProtocolSTUN}, nil + metadata.Protocol = C.ProtocolSTUN + return nil } diff --git a/common/sniff/stun_test.go b/common/sniff/stun_test.go index 6da89162a9..f465763e1f 100644 --- a/common/sniff/stun_test.go +++ b/common/sniff/stun_test.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "testing" + "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" @@ -15,14 +16,16 @@ func TestSniffSTUN(t *testing.T) { t.Parallel() packet, err := hex.DecodeString("000100002112a44224b1a025d0c180c484341306") require.NoError(t, err) - metadata, err := sniff.STUNMessage(context.Background(), packet) + var metadata adapter.InboundContext + err = sniff.STUNMessage(context.Background(), &metadata, packet) require.NoError(t, err) require.Equal(t, metadata.Protocol, C.ProtocolSTUN) } func FuzzSniffSTUN(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { - if _, err := sniff.STUNMessage(context.Background(), data); err == nil { + var metadata adapter.InboundContext + if err := sniff.STUNMessage(context.Background(), &metadata, data); err == nil { t.Fail() } }) diff --git a/common/sniff/tls.go b/common/sniff/tls.go index dcba195afe..6fe430e2d8 100644 --- a/common/sniff/tls.go +++ b/common/sniff/tls.go @@ -10,7 +10,7 @@ import ( "github.com/sagernet/sing/common/bufio" ) -func TLSClientHello(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) { +func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error { var clientHello *tls.ClientHelloInfo err := tls.Server(bufio.NewReadOnlyConn(reader), &tls.Config{ GetConfigForClient: func(argHello *tls.ClientHelloInfo) (*tls.Config, error) { @@ -19,7 +19,9 @@ func TLSClientHello(ctx context.Context, reader io.Reader) (*adapter.InboundCont }, }).HandshakeContext(ctx) if clientHello != nil { - return &adapter.InboundContext{Protocol: C.ProtocolTLS, Domain: clientHello.ServerName}, nil + metadata.Protocol = C.ProtocolTLS + metadata.Domain = clientHello.ServerName + return nil } - return nil, err + return err } diff --git a/constant/protocol.go b/constant/protocol.go index 915e64ca19..8b1854d419 100644 --- a/constant/protocol.go +++ b/constant/protocol.go @@ -9,3 +9,11 @@ const ( ProtocolBitTorrent = "bittorrent" ProtocolDTLS = "dtls" ) + +const ( + ClientChromium = "chromium" + ClientSafari = "safari" + ClientFirefox = "firefox" + ClientQUICGo = "quic-go" + ClientUnknown = "unknown" +) diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 23d2bf1b0c..8e61e51df2 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -4,6 +4,7 @@ icon: material/alert-decagram !!! quote "Changes in sing-box 1.10.0" + :material-plus: [client](#client) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) @@ -40,6 +41,12 @@ icon: material/alert-decagram "http", "quic" ], + "client": [ + "chromium", + "safari", + "firefox", + "quic-go" + ], "domain": [ "test.com" ], @@ -166,7 +173,13 @@ Username, see each inbound for details. #### protocol -Sniffed protocol, see [Sniff](/configuration/route/sniff/) for details. +Sniffed protocol, see [Protocol Sniff](/configuration/route/sniff/) for details. + +#### client + +!!! question "Since sing-box 1.10.0" + +Sniffed client type, see [Protocol Sniff](/configuration/route/sniff/) for details. #### network diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 52d334f22c..68e66cf5c9 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -4,8 +4,9 @@ icon: material/alert-decagram !!! quote "sing-box 1.10.0 中的更改" + :material-plus: [client](#client) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) - :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) !!! quote "sing-box 1.8.0 中的更改" @@ -40,6 +41,12 @@ icon: material/alert-decagram "http", "quic" ], + "client": [ + "chromium", + "safari", + "firefox", + "quic-go" + ], "domain": [ "test.com" ], @@ -166,6 +173,12 @@ icon: material/alert-decagram 探测到的协议, 参阅 [协议探测](/zh/configuration/route/sniff/)。 +#### client + +!!! question "自 sing-box 1.10.0 起" + +探测到的客户端类型, 参阅 [协议探测](/zh/configuration/route/sniff/)。 + #### network `tcp` 或 `udp`。 diff --git a/docs/configuration/route/sniff.md b/docs/configuration/route/sniff.md index cc9a5c130d..ab96e75a6f 100644 --- a/docs/configuration/route/sniff.md +++ b/docs/configuration/route/sniff.md @@ -4,19 +4,28 @@ icon: material/new-box !!! quote "Changes in sing-box 1.10.0" - :material-plus: BitTorrent support + :material-plus: QUIC client type detect support for QUIC + :material-plus: Chromium support for QUIC + :material-plus: BitTorrent support :material-plus: DTLS support If enabled in the inbound, the protocol and domain name (if present) of by the connection can be sniffed. #### Supported Protocols -| Network | Protocol | Domain Name | -|:-------:|:------------:|:-----------:| -| TCP | `http` | Host | -| TCP | `tls` | Server Name | -| UDP | `quic` | Server Name | -| UDP | `stun` | / | -| TCP/UDP | `dns` | / | -| TCP/UDP | `bittorrent` | / | -| UDP | `dtls` | / | +| Network | Protocol | Domain Name | Client | +|:-------:|:------------:|:-----------:|:----------------:| +| TCP | `http` | Host | / | +| TCP | `tls` | Server Name | / | +| UDP | `quic` | Server Name | QUIC Client Type | +| UDP | `stun` | / | / | +| TCP/UDP | `dns` | / | / | +| TCP/UDP | `bittorrent` | / | / | +| UDP | `dtls` | / | / | + +| QUIC Client | Type | +|:------------------------:|:----------:| +| Chromium/Cronet | `chrimium` | +| Safari/Apple Network API | `safari` | +| Firefox / uquic firefox | `firefox` | +| quic-go / uquic chrome | `quic-go` | \ No newline at end of file diff --git a/docs/configuration/route/sniff.zh.md b/docs/configuration/route/sniff.zh.md index 523ed44b90..1ebc6d38d1 100644 --- a/docs/configuration/route/sniff.zh.md +++ b/docs/configuration/route/sniff.zh.md @@ -4,19 +4,28 @@ icon: material/new-box !!! quote "sing-box 1.10.0 中的更改" - :material-plus: BitTorrent 支持 + :material-plus: QUIC 的 客户端类型探测支持 + :material-plus: QUIC 的 Chromium 支持 + :material-plus: BitTorrent 支持 :material-plus: DTLS 支持 如果在入站中启用,则可以嗅探连接的协议和域名(如果存在)。 #### 支持的协议 -| 网络 | 协议 | 域名 | -|:-------:|:------------:|:-----------:| -| TCP | `http` | Host | -| TCP | `tls` | Server Name | -| UDP | `quic` | Server Name | -| UDP | `stun` | / | -| TCP/UDP | `dns` | / | -| TCP/UDP | `bittorrent` | / | -| UDP | `dtls` | / | +| 网络 | 协议 | 域名 | 客户端 | +|:-------:|:------------:|:-----------:|:----------:| +| TCP | `http` | Host | / | +| TCP | `tls` | Server Name | / | +| UDP | `quic` | Server Name | QUIC 客户端类型 | +| UDP | `stun` | / | / | +| TCP/UDP | `dns` | / | / | +| TCP/UDP | `bittorrent` | / | / | +| UDP | `dtls` | / | / | + +| QUIC 客户端 | 类型 | +|:------------------------:|:----------:| +| Chromium/Cronet | `chrimium` | +| Safari/Apple Network API | `safari` | +| Firefox / uquic firefox | `firefox` | +| quic-go / uquic chrome | `quic-go` | \ No newline at end of file diff --git a/go.mod b/go.mod index 7666dd24e5..f9b11d6736 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( go.uber.org/zap v1.27.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.25.0 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/net v0.27.0 golang.org/x/sys v0.25.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 @@ -89,7 +90,6 @@ require ( github.com/vishvananda/netns v0.0.4 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/text v0.18.0 // indirect diff --git a/option/rule.go b/option/rule.go index 74dd13c615..5c69ef00e3 100644 --- a/option/rule.go +++ b/option/rule.go @@ -70,6 +70,7 @@ type _DefaultRule struct { Network Listable[string] `json:"network,omitempty"` AuthUser Listable[string] `json:"auth_user,omitempty"` Protocol Listable[string] `json:"protocol,omitempty"` + Client Listable[string] `json:"client,omitempty"` Domain Listable[string] `json:"domain,omitempty"` DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` diff --git a/route/router.go b/route/router.go index 3e0e90db88..35e1e21f28 100644 --- a/route/router.go +++ b/route/router.go @@ -854,8 +854,9 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad if metadata.InboundOptions.SniffEnabled && !sniff.Skip(metadata) { buffer := buf.NewPacket() - sniffMetadata, err := sniff.PeekStream( + err := sniff.PeekStream( ctx, + &metadata, conn, buffer, time.Duration(metadata.InboundOptions.SniffTimeout), @@ -864,9 +865,7 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad sniff.HTTPHost, sniff.BitTorrent, ) - if sniffMetadata != nil { - metadata.Protocol = sniffMetadata.Protocol - metadata.Domain = sniffMetadata.Domain + if err == nil { if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) { metadata.Destination = M.Socksaddr{ Fqdn: metadata.Domain, @@ -878,8 +877,6 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad } else { r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol) } - } else if err != nil { - r.logger.TraceContext(ctx, "sniffed no protocol: ", err) } if !buffer.IsEmpty() { conn = bufio.NewCachedConn(conn, buffer) @@ -980,65 +977,87 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m }*/ if metadata.InboundOptions.SniffEnabled || metadata.Destination.Addr.IsUnspecified() { - var ( - buffer = buf.NewPacket() - destination M.Socksaddr - done = make(chan struct{}) - err error - ) - go func() { - sniffTimeout := C.ReadPayloadTimeout - if metadata.InboundOptions.SniffTimeout > 0 { - sniffTimeout = time.Duration(metadata.InboundOptions.SniffTimeout) - } - conn.SetReadDeadline(time.Now().Add(sniffTimeout)) - destination, err = conn.ReadPacket(buffer) - conn.SetReadDeadline(time.Time{}) - close(done) - }() - select { - case <-done: - case <-ctx.Done(): - conn.Close() - return ctx.Err() - } - if err != nil { - buffer.Release() - if !errors.Is(err, os.ErrDeadlineExceeded) { - return err - } - } else { - if metadata.Destination.Addr.IsUnspecified() { - metadata.Destination = destination + var bufferList []*buf.Buffer + for { + var ( + buffer = buf.NewPacket() + destination M.Socksaddr + done = make(chan struct{}) + err error + ) + go func() { + sniffTimeout := C.ReadPayloadTimeout + if metadata.InboundOptions.SniffTimeout > 0 { + sniffTimeout = time.Duration(metadata.InboundOptions.SniffTimeout) + } + conn.SetReadDeadline(time.Now().Add(sniffTimeout)) + destination, err = conn.ReadPacket(buffer) + conn.SetReadDeadline(time.Time{}) + close(done) + }() + select { + case <-done: + case <-ctx.Done(): + conn.Close() + return ctx.Err() } - if metadata.InboundOptions.SniffEnabled { - sniffMetadata, _ := sniff.PeekPacket( - ctx, - buffer.Bytes(), - sniff.DomainNameQuery, - sniff.QUICClientHello, - sniff.STUNMessage, - sniff.UTP, - sniff.UDPTracker, - sniff.DTLSRecord, - ) - if sniffMetadata != nil { - metadata.Protocol = sniffMetadata.Protocol - metadata.Domain = sniffMetadata.Domain - if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) { - metadata.Destination = M.Socksaddr{ - Fqdn: metadata.Domain, - Port: metadata.Destination.Port, - } - } - if metadata.Domain != "" { - r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain) + if err != nil { + buffer.Release() + if !errors.Is(err, os.ErrDeadlineExceeded) { + return err + } + } else { + if metadata.Destination.Addr.IsUnspecified() { + metadata.Destination = destination + } + if metadata.InboundOptions.SniffEnabled { + if len(bufferList) > 0 { + err = sniff.PeekPacket( + ctx, + &metadata, + buffer.Bytes(), + sniff.QUICClientHello, + ) } else { - r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol) + err = sniff.PeekPacket( + ctx, &metadata, + buffer.Bytes(), + sniff.DomainNameQuery, + sniff.QUICClientHello, + sniff.STUNMessage, + sniff.UTP, + sniff.UDPTracker, + sniff.DTLSRecord) + } + if E.IsMulti(err, sniff.ErrClientHelloFragmented) && len(bufferList) == 0 { + bufferList = append(bufferList, buffer) + r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello") + continue + } + if metadata.Protocol != "" { + if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) { + metadata.Destination = M.Socksaddr{ + Fqdn: metadata.Domain, + Port: metadata.Destination.Port, + } + } + if metadata.Domain != "" && metadata.Client != "" { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client) + } else if metadata.Domain != "" { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain) + } else if metadata.Client != "" { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", client: ", metadata.Client) + } else { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol) + } } } + conn = bufio.NewCachedPacketConn(conn, buffer, destination) } - conn = bufio.NewCachedPacketConn(conn, buffer, destination) + for _, cachedBuffer := range common.Reverse(bufferList) { + conn = bufio.NewCachedPacketConn(conn, cachedBuffer, destination) + } + break } } if r.dnsReverseMapping != nil && metadata.Domain == "" { diff --git a/route/rule_default.go b/route/rule_default.go index 53e53bdf86..4bbc2b6b2e 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -79,6 +79,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.Client) > 0 { + item := NewClientItem(options.Client) + 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) diff --git a/route/rule_item_client.go b/route/rule_item_client.go new file mode 100644 index 0000000000..eeab440240 --- /dev/null +++ b/route/rule_item_client.go @@ -0,0 +1,37 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*ClientItem)(nil) + +type ClientItem struct { + clients []string + clientMap map[string]bool +} + +func NewClientItem(clients []string) *ClientItem { + clientMap := make(map[string]bool) + for _, client := range clients { + clientMap[client] = true + } + return &ClientItem{ + clients: clients, + clientMap: clientMap, + } +} + +func (r *ClientItem) Match(metadata *adapter.InboundContext) bool { + return r.clientMap[metadata.Client] +} + +func (r *ClientItem) String() string { + if len(r.clients) == 1 { + return F.ToString("client=", r.clients[0]) + } + return F.ToString("client=[", strings.Join(r.clients, " "), "]") +} From 1e36c7533674b8e141f5b576b9d8a3f5e7669664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 26 Jul 2024 08:03:08 +0800 Subject: [PATCH 17/25] Add AdGuard DNS filter support --- cmd/sing-box/cmd_rule_set_convert.go | 88 +++++ .../internal/convertor/adguard/convertor.go | 346 ++++++++++++++++++ .../convertor/adguard/convertor_test.go | 140 +++++++ common/srs/binary.go | 22 ++ docs/configuration/rule-set/adguard.md | 71 ++++ mkdocs.yml | 1 + option/rule_set.go | 3 + route/rule_headless.go | 9 + route/rule_item_adguard.go | 43 +++ 9 files changed, 723 insertions(+) create mode 100644 cmd/sing-box/cmd_rule_set_convert.go create mode 100644 cmd/sing-box/internal/convertor/adguard/convertor.go create mode 100644 cmd/sing-box/internal/convertor/adguard/convertor_test.go create mode 100644 docs/configuration/rule-set/adguard.md create mode 100644 route/rule_item_adguard.go diff --git a/cmd/sing-box/cmd_rule_set_convert.go b/cmd/sing-box/cmd_rule_set_convert.go new file mode 100644 index 0000000000..8d2092ea28 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_convert.go @@ -0,0 +1,88 @@ +package main + +import ( + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard" + "github.com/sagernet/sing-box/common/srs" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var ( + flagRuleSetConvertType string + flagRuleSetConvertOutput string +) + +var commandRuleSetConvert = &cobra.Command{ + Use: "convert [source-path]", + Short: "Convert adguard DNS filter to rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := convertRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSet.AddCommand(commandRuleSetConvert) + commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertType, "type", "t", "", "Source type, available: adguard") + commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file") +} + +func convertRuleSet(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 + } + } + var rules []option.HeadlessRule + switch flagRuleSetConvertType { + case "adguard": + rules, err = adguard.Convert(reader) + case "": + return E.New("source type is required") + default: + return E.New("unsupported source type: ", flagRuleSetConvertType) + } + if err != nil { + return err + } + var outputPath string + if flagRuleSetConvertOutput == flagRuleSetCompileDefaultOutput { + if strings.HasSuffix(sourcePath, ".txt") { + outputPath = sourcePath[:len(sourcePath)-4] + ".srs" + } else { + outputPath = sourcePath + ".srs" + } + } else { + outputPath = flagRuleSetConvertOutput + } + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + defer outputFile.Close() + err = srs.Write(outputFile, option.PlainRuleSet{Rules: rules}, true) + if err != nil { + outputFile.Close() + os.Remove(outputPath) + return err + } + outputFile.Close() + return nil +} diff --git a/cmd/sing-box/internal/convertor/adguard/convertor.go b/cmd/sing-box/internal/convertor/adguard/convertor.go new file mode 100644 index 0000000000..ff60929b8d --- /dev/null +++ b/cmd/sing-box/internal/convertor/adguard/convertor.go @@ -0,0 +1,346 @@ +package adguard + +import ( + "bufio" + "io" + "net/netip" + "os" + "strconv" + "strings" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "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" +) + +type agdguardRuleLine struct { + ruleLine string + isRawDomain bool + isExclude bool + isSuffix bool + hasStart bool + hasEnd bool + isRegexp bool + isImportant bool +} + +func Convert(reader io.Reader) ([]option.HeadlessRule, error) { + scanner := bufio.NewScanner(reader) + var ( + ruleLines []agdguardRuleLine + ignoredLines int + ) +parseLine: + for scanner.Scan() { + ruleLine := scanner.Text() + if ruleLine == "" || ruleLine[0] == '!' || ruleLine[0] == '#' { + continue + } + originRuleLine := ruleLine + if M.IsDomainName(ruleLine) { + ruleLines = append(ruleLines, agdguardRuleLine{ + ruleLine: ruleLine, + isRawDomain: true, + }) + continue + } + hostLine, err := parseAdGuardHostLine(ruleLine) + if err == nil { + if hostLine != "" { + ruleLines = append(ruleLines, agdguardRuleLine{ + ruleLine: hostLine, + isRawDomain: true, + hasStart: true, + hasEnd: true, + }) + } + continue + } + if strings.HasSuffix(ruleLine, "|") { + ruleLine = ruleLine[:len(ruleLine)-1] + } + var ( + isExclude bool + isSuffix bool + hasStart bool + hasEnd bool + isRegexp bool + isImportant bool + ) + if !strings.HasPrefix(ruleLine, "/") && strings.Contains(ruleLine, "$") { + params := common.SubstringAfter(ruleLine, "$") + for _, param := range strings.Split(params, ",") { + paramParts := strings.Split(param, "=") + var ignored bool + if len(paramParts) > 0 && len(paramParts) <= 2 { + switch paramParts[0] { + case "app", "network": + // maybe support by package_name/process_name + case "dnstype": + // maybe support by query_type + case "important": + ignored = true + isImportant = true + case "dnsrewrite": + if len(paramParts) == 2 && M.ParseAddr(paramParts[1]).IsUnspecified() { + ignored = true + } + } + } + if !ignored { + ignoredLines++ + log.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", ruleLine) + continue parseLine + } + } + ruleLine = common.SubstringBefore(ruleLine, "$") + } + if strings.HasPrefix(ruleLine, "@@") { + ruleLine = ruleLine[2:] + isExclude = true + } + if strings.HasSuffix(ruleLine, "|") { + ruleLine = ruleLine[:len(ruleLine)-1] + } + if strings.HasPrefix(ruleLine, "||") { + ruleLine = ruleLine[2:] + isSuffix = true + } else if strings.HasPrefix(ruleLine, "|") { + ruleLine = ruleLine[1:] + hasStart = true + } + if strings.HasSuffix(ruleLine, "^") { + ruleLine = ruleLine[:len(ruleLine)-1] + hasEnd = true + } + if strings.HasPrefix(ruleLine, "/") && strings.HasSuffix(ruleLine, "/") { + ruleLine = ruleLine[1 : len(ruleLine)-1] + if ignoreIPCIDRRegexp(ruleLine) { + ignoredLines++ + log.Debug("ignored unsupported rule with IPCIDR regexp: ", ruleLine) + continue + } + isRegexp = true + } else { + if strings.Contains(ruleLine, "://") { + ruleLine = common.SubstringAfter(ruleLine, "://") + } + if strings.Contains(ruleLine, "/") { + ignoredLines++ + log.Debug("ignored unsupported rule with path: ", ruleLine) + continue + } + if strings.Contains(ruleLine, "##") { + ignoredLines++ + log.Debug("ignored unsupported rule with element hiding: ", ruleLine) + continue + } + if strings.Contains(ruleLine, "#$#") { + ignoredLines++ + log.Debug("ignored unsupported rule with element hiding: ", ruleLine) + continue + } + var domainCheck string + if strings.HasPrefix(ruleLine, ".") || strings.HasPrefix(ruleLine, "-") { + domainCheck = "r" + ruleLine + } else { + domainCheck = ruleLine + } + if ruleLine == "" { + ignoredLines++ + log.Debug("ignored unsupported rule with empty domain", originRuleLine) + continue + } else { + domainCheck = strings.ReplaceAll(domainCheck, "*", "x") + if !M.IsDomainName(domainCheck) { + _, ipErr := parseADGuardIPCIDRLine(ruleLine) + if ipErr == nil { + ignoredLines++ + log.Debug("ignored unsupported rule with IPCIDR: ", ruleLine) + continue + } + if M.ParseSocksaddr(domainCheck).Port != 0 { + log.Debug("ignored unsupported rule with port: ", ruleLine) + } else { + log.Debug("ignored unsupported rule with invalid domain: ", ruleLine) + } + ignoredLines++ + continue + } + } + } + ruleLines = append(ruleLines, agdguardRuleLine{ + ruleLine: ruleLine, + isExclude: isExclude, + isSuffix: isSuffix, + hasStart: hasStart, + hasEnd: hasEnd, + isRegexp: isRegexp, + isImportant: isImportant, + }) + } + if len(ruleLines) == 0 { + return nil, E.New("AdGuard rule-set is empty or all rules are unsupported") + } + if common.All(ruleLines, func(it agdguardRuleLine) bool { + return it.isRawDomain + }) { + return []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + Domain: common.Map(ruleLines, func(it agdguardRuleLine) string { + return it.ruleLine + }), + }, + }, + }, nil + } + mapDomain := func(it agdguardRuleLine) string { + ruleLine := it.ruleLine + if it.isSuffix { + ruleLine = "||" + ruleLine + } else if it.hasStart { + ruleLine = "|" + ruleLine + } + if it.hasEnd { + ruleLine += "^" + } + return ruleLine + } + + importantDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain) + importantDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && !it.isExclude }), mapDomain) + importantExcludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && it.isExclude }), mapDomain) + importantExcludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && it.isExclude }), mapDomain) + domain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain) + domainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && !it.isExclude }), mapDomain) + excludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && it.isExclude }), mapDomain) + excludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && it.isExclude }), mapDomain) + currentRule := option.HeadlessRule{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + AdGuardDomain: domain, + DomainRegex: domainRegex, + }, + } + if len(excludeDomain) > 0 || len(excludeDomainRegex) > 0 { + currentRule = option.HeadlessRule{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalHeadlessRule{ + Mode: C.LogicalTypeAnd, + Rules: []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + AdGuardDomain: excludeDomain, + DomainRegex: excludeDomainRegex, + Invert: true, + }, + }, + currentRule, + }, + }, + } + } + if len(importantDomain) > 0 || len(importantDomainRegex) > 0 { + currentRule = option.HeadlessRule{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalHeadlessRule{ + Mode: C.LogicalTypeOr, + Rules: []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + AdGuardDomain: importantDomain, + DomainRegex: importantDomainRegex, + }, + }, + currentRule, + }, + }, + } + } + if len(importantExcludeDomain) > 0 || len(importantExcludeDomainRegex) > 0 { + currentRule = option.HeadlessRule{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalHeadlessRule{ + Mode: C.LogicalTypeAnd, + Rules: []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + AdGuardDomain: importantExcludeDomain, + DomainRegex: importantExcludeDomainRegex, + Invert: true, + }, + }, + currentRule, + }, + }, + } + } + log.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines) + return []option.HeadlessRule{currentRule}, nil +} + +func ignoreIPCIDRRegexp(ruleLine string) bool { + if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") { + ruleLine = ruleLine[12:] + } else if strings.HasPrefix(ruleLine, "(https?:\\/\\/)") { + ruleLine = ruleLine[13:] + } else if strings.HasPrefix(ruleLine, "^") { + ruleLine = ruleLine[1:] + } else { + return false + } + _, parseErr := strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8) + return parseErr == nil +} + +func parseAdGuardHostLine(ruleLine string) (string, error) { + idx := strings.Index(ruleLine, " ") + if idx == -1 { + return "", os.ErrInvalid + } + address, err := netip.ParseAddr(ruleLine[:idx]) + if err != nil { + return "", err + } + if !address.IsUnspecified() { + return "", nil + } + domain := ruleLine[idx+1:] + if !M.IsDomainName(domain) { + return "", E.New("invalid domain name: ", domain) + } + return domain, nil +} + +func parseADGuardIPCIDRLine(ruleLine string) (netip.Prefix, error) { + var isPrefix bool + if strings.HasSuffix(ruleLine, ".") { + isPrefix = true + ruleLine = ruleLine[:len(ruleLine)-1] + } + ruleStringParts := strings.Split(ruleLine, ".") + if len(ruleStringParts) > 4 || len(ruleStringParts) < 4 && !isPrefix { + return netip.Prefix{}, os.ErrInvalid + } + ruleParts := make([]uint8, 0, len(ruleStringParts)) + for _, part := range ruleStringParts { + rulePart, err := strconv.ParseUint(part, 10, 8) + if err != nil { + return netip.Prefix{}, err + } + ruleParts = append(ruleParts, uint8(rulePart)) + } + bitLen := len(ruleParts) * 8 + for len(ruleParts) < 4 { + ruleParts = append(ruleParts, 0) + } + return netip.PrefixFrom(netip.AddrFrom4(*(*[4]byte)(ruleParts)), bitLen), nil +} diff --git a/cmd/sing-box/internal/convertor/adguard/convertor_test.go b/cmd/sing-box/internal/convertor/adguard/convertor_test.go new file mode 100644 index 0000000000..7da8a22841 --- /dev/null +++ b/cmd/sing-box/internal/convertor/adguard/convertor_test.go @@ -0,0 +1,140 @@ +package adguard + +import ( + "strings" + "testing" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/route" + + "github.com/stretchr/testify/require" +) + +func TestConverter(t *testing.T) { + t.Parallel() + rules, err := Convert(strings.NewReader(` +||example.org^ +|example.com^ +example.net^ +||example.edu +||example.edu.tw^ +|example.gov +example.arpa +@@|sagernet.example.org| +||sagernet.org^$important +@@|sing-box.sagernet.org^$important +`)) + require.NoError(t, err) + require.Len(t, rules, 1) + rule, err := route.NewHeadlessRule(nil, rules[0]) + require.NoError(t, err) + matchDomain := []string{ + "example.org", + "www.example.org", + "example.com", + "example.net", + "isexample.net", + "www.example.net", + "example.edu", + "example.edu.cn", + "example.edu.tw", + "www.example.edu", + "www.example.edu.cn", + "example.gov", + "example.gov.cn", + "example.arpa", + "www.example.arpa", + "isexample.arpa", + "example.arpa.cn", + "www.example.arpa.cn", + "isexample.arpa.cn", + "sagernet.org", + "www.sagernet.org", + } + notMatchDomain := []string{ + "example.org.cn", + "notexample.org", + "example.com.cn", + "www.example.com.cn", + "example.net.cn", + "notexample.edu", + "notexample.edu.cn", + "www.example.gov", + "notexample.gov", + "sagernet.example.org", + "sing-box.sagernet.org", + } + for _, domain := range matchDomain { + require.True(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } + for _, domain := range notMatchDomain { + require.False(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } +} + +func TestHosts(t *testing.T) { + t.Parallel() + rules, err := Convert(strings.NewReader(` +127.0.0.1 localhost +::1 localhost #[IPv6] +0.0.0.0 google.com +`)) + require.NoError(t, err) + require.Len(t, rules, 1) + rule, err := route.NewHeadlessRule(nil, rules[0]) + require.NoError(t, err) + matchDomain := []string{ + "google.com", + } + notMatchDomain := []string{ + "www.google.com", + "notgoogle.com", + "localhost", + } + for _, domain := range matchDomain { + require.True(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } + for _, domain := range notMatchDomain { + require.False(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } +} + +func TestSimpleHosts(t *testing.T) { + t.Parallel() + rules, err := Convert(strings.NewReader(` +example.com +www.example.org +`)) + require.NoError(t, err) + require.Len(t, rules, 1) + rule, err := route.NewHeadlessRule(nil, rules[0]) + require.NoError(t, err) + matchDomain := []string{ + "example.com", + "www.example.org", + } + notMatchDomain := []string{ + "example.com.cn", + "www.example.com", + "notexample.com", + "example.org", + } + for _, domain := range matchDomain { + require.True(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } + for _, domain := range notMatchDomain { + require.False(t, rule.Match(&adapter.InboundContext{ + Domain: domain, + }), domain) + } +} diff --git a/common/srs/binary.go b/common/srs/binary.go index 69075f7881..7e4f402b39 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -36,6 +36,7 @@ const ( ruleItemPackageName ruleItemWIFISSID ruleItemWIFIBSSID + ruleItemAdGuardDomain ruleItemFinal uint8 = 0xFF ) @@ -212,6 +213,17 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea rule.WIFISSID, err = readRuleItemString(reader) case ruleItemWIFIBSSID: rule.WIFIBSSID, err = readRuleItemString(reader) + case ruleItemAdGuardDomain: + if recover { + err = E.New("unable to decompile binary AdGuard rules to rule-set") + return + } + var matcher *domain.AdGuardMatcher + matcher, err = domain.ReadAdGuardMatcher(reader) + if err != nil { + return + } + rule.AdGuardDomainMatcher = matcher case ruleItemFinal: err = binary.Read(reader, binary.BigEndian, &rule.Invert) return @@ -332,6 +344,16 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen return err } } + if len(rule.AdGuardDomain) > 0 { + err = binary.Write(writer, binary.BigEndian, ruleItemAdGuardDomain) + if err != nil { + return err + } + err = domain.NewAdGuardMatcher(rule.AdGuardDomain).Write(writer) + if err != nil { + return err + } + } err = binary.Write(writer, binary.BigEndian, ruleItemFinal) if err != nil { return err diff --git a/docs/configuration/rule-set/adguard.md b/docs/configuration/rule-set/adguard.md new file mode 100644 index 0000000000..870972bf2f --- /dev/null +++ b/docs/configuration/rule-set/adguard.md @@ -0,0 +1,71 @@ +--- +icon: material/new-box +--- + +# AdGuard DNS Filter + +!!! question "Since sing-box 1.10.0" + +sing-box supports some rule-set formats from other projects which cannot be fully translated to sing-box, +currently only AdGuard DNS Filter. + +These formats are not directly supported as source formats, +instead you need to convert them to binary rule-set. + +## Convert + +Use `sing-box rule-set convert --type adguard [--output .srs] .txt` to convert to binary rule-set. + +## Performance + +AdGuard keeps all rules in memory and matches them sequentially, +while sing-box chooses high performance and smaller memory usage. +As a trade-off, you cannot know which rule item is matched. + +## Compatibility + +Almost all rules in [AdGuardSDNSFilter](https://github.com/AdguardTeam/AdGuardSDNSFilter) +and rules in rule-sets listed in [adguard-filter-list](https://github.com/ppfeufer/adguard-filter-list) +are supported. + +## Supported formats + +### AdGuard Filter + +#### Basic rule syntax + +| Syntax | Supported | +|--------|------------------| +| `@@` | :material-check: | +| `\|\|` | :material-check: | +| `\|` | :material-check: | +| `^` | :material-check: | +| `*` | :material-check: | + +#### Host syntax + +| Syntax | Example | Supported | +|-------------|--------------------------|--------------------------| +| Scheme | `https://` | :material-alert: Ignored | +| Domain Host | `example.org` | :material-check: | +| IP Host | `1.1.1.1`, `10.0.0.` | :material-close: | +| Regexp | `/regexp/` | :material-check: | +| Port | `example.org:80` | :material-close: | +| Path | `example.org/path/ad.js` | :material-close: | + +#### Modifier syntax + +| Modifier | Supported | +|-----------------------|--------------------------| +| `$important` | :material-check: | +| `$dnsrewrite=0.0.0.0` | :material-alert: Ignored | +| Any other modifiers | :material-close: | + +### Hosts + +Only items with `0.0.0.0` IP addresses will be accepted. + +### Simple + +When all rule lines are valid domains, they are treated as simple line-by-line domain rules which, +like hosts, only match the exact same domain. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index f8a40765f2..56bdf96a47 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -91,6 +91,7 @@ nav: - configuration/rule-set/index.md - Source Format: configuration/rule-set/source-format.md - Headless Rule: configuration/rule-set/headless-rule.md + - AdGuard DNS Filer: configuration/rule-set/adguard.md - Experimental: - configuration/experimental/index.md - Cache File: configuration/experimental/cache-file.md diff --git a/option/rule_set.go b/option/rule_set.go index 4e9eb0f8b3..a83b2c47b6 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -166,6 +166,9 @@ type DefaultHeadlessRule struct { DomainMatcher *domain.Matcher `json:"-"` SourceIPSet *netipx.IPSet `json:"-"` IPSet *netipx.IPSet `json:"-"` + + AdGuardDomain Listable[string] `json:"-"` + AdGuardDomainMatcher *domain.AdGuardMatcher `json:"-"` } func (r DefaultHeadlessRule) IsValid() bool { diff --git a/route/rule_headless.go b/route/rule_headless.go index 67ac3a1e44..d537df572c 100644 --- a/route/rule_headless.go +++ b/route/rule_headless.go @@ -142,6 +142,15 @@ func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadles rule.allItems = append(rule.allItems, item) } } + if len(options.AdGuardDomain) > 0 { + item := NewAdGuardDomainItem(options.AdGuardDomain) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.AdGuardDomainMatcher != nil { + item := NewRawAdGuardDomainItem(options.AdGuardDomainMatcher) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } diff --git a/route/rule_item_adguard.go b/route/rule_item_adguard.go new file mode 100644 index 0000000000..bdbb3b75fd --- /dev/null +++ b/route/rule_item_adguard.go @@ -0,0 +1,43 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/domain" +) + +var _ RuleItem = (*AdGuardDomainItem)(nil) + +type AdGuardDomainItem struct { + matcher *domain.AdGuardMatcher +} + +func NewAdGuardDomainItem(ruleLines []string) *AdGuardDomainItem { + return &AdGuardDomainItem{ + domain.NewAdGuardMatcher(ruleLines), + } +} + +func NewRawAdGuardDomainItem(matcher *domain.AdGuardMatcher) *AdGuardDomainItem { + return &AdGuardDomainItem{ + matcher, + } +} + +func (r *AdGuardDomainItem) Match(metadata *adapter.InboundContext) bool { + var domainHost string + if metadata.Domain != "" { + domainHost = metadata.Domain + } else { + domainHost = metadata.Destination.Fqdn + } + if domainHost == "" { + return false + } + return r.matcher.Match(strings.ToLower(domainHost)) +} + +func (r *AdGuardDomainItem) String() string { + return "!adguard_domain_rules=" +} From 795e1cf2f06fad90a664d79be22efbcaa20e48b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 10 Aug 2024 16:47:44 +0800 Subject: [PATCH 18/25] Write close error to log --- cmd/sing-box/cmd_run.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/sing-box/cmd_run.go b/cmd/sing-box/cmd_run.go index e717c5945d..6850cd19e3 100644 --- a/cmd/sing-box/cmd_run.go +++ b/cmd/sing-box/cmd_run.go @@ -188,9 +188,12 @@ func run() error { cancel() closeCtx, closed := context.WithCancel(context.Background()) go closeMonitor(closeCtx) - instance.Close() + err = instance.Close() closed() if osSignal != syscall.SIGHUP { + if err != nil { + log.Error(E.Cause(err, "sing-box did not closed properly")) + } return nil } break From 04613c27e28007e669d3b8f009afdb49aee4aa96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 20 Aug 2024 18:56:50 +0800 Subject: [PATCH 19/25] Add SSH sniffer --- common/sniff/ssh.go | 26 ++++++++++++++++++++++++++ common/sniff/ssh_test.go | 26 ++++++++++++++++++++++++++ constant/protocol.go | 1 + docs/configuration/route/sniff.md | 8 +++++--- docs/configuration/route/sniff.zh.md | 24 +++++++++++++----------- route/router.go | 3 ++- 6 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 common/sniff/ssh.go create mode 100644 common/sniff/ssh_test.go diff --git a/common/sniff/ssh.go b/common/sniff/ssh.go new file mode 100644 index 0000000000..194d0bda8c --- /dev/null +++ b/common/sniff/ssh.go @@ -0,0 +1,26 @@ +package sniff + +import ( + "bufio" + "context" + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" +) + +func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { + scanner := bufio.NewScanner(reader) + if !scanner.Scan() { + return os.ErrInvalid + } + fistLine := scanner.Text() + if !strings.HasPrefix(fistLine, "SSH-2.0-") { + return os.ErrInvalid + } + metadata.Protocol = C.ProtocolSSH + metadata.Client = fistLine[8:] + return nil +} diff --git a/common/sniff/ssh_test.go b/common/sniff/ssh_test.go new file mode 100644 index 0000000000..be53098079 --- /dev/null +++ b/common/sniff/ssh_test.go @@ -0,0 +1,26 @@ +package sniff_test + +import ( + "bytes" + "context" + "encoding/hex" + "testing" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/sniff" + C "github.com/sagernet/sing-box/constant" + + "github.com/stretchr/testify/require" +) + +func TestSniffSSH(t *testing.T) { + t.Parallel() + + pkt, err := hex.DecodeString("5353482d322e302d64726f70626561720d0a000001a40a1492892570d1223aef61b0d647972c8bd30000009f637572766532353531392d7368613235362c637572766532353531392d736861323536406c69627373682e6f72672c6469666669652d68656c6c6d616e2d67726f757031342d7368613235362c6469666669652d68656c6c6d616e2d67726f757031342d736861312c6b6578677565737332406d6174742e7563632e61736e2e61752c6b65782d7374726963742d732d763030406f70656e7373682e636f6d000000207373682d656432353531392c7273612d736861322d3235362c7373682d7273610000003363686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733235362d6374720000003363686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733235362d63747200000017686d61632d736861312c686d61632d736861322d32353600000017686d61632d736861312c686d61632d736861322d323536000000046e6f6e65000000046e6f6e65000000000000000000000000002aa6ed090585b7d635b6") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt)) + require.NoError(t, err) + require.Equal(t, C.ProtocolSSH, metadata.Protocol) + require.Equal(t, "dropbear", metadata.Client) +} diff --git a/constant/protocol.go b/constant/protocol.go index 8b1854d419..f867f428f6 100644 --- a/constant/protocol.go +++ b/constant/protocol.go @@ -8,6 +8,7 @@ const ( ProtocolSTUN = "stun" ProtocolBitTorrent = "bittorrent" ProtocolDTLS = "dtls" + ProtocolSSH = "ssh" ) const ( diff --git a/docs/configuration/route/sniff.md b/docs/configuration/route/sniff.md index ab96e75a6f..70e2acc85a 100644 --- a/docs/configuration/route/sniff.md +++ b/docs/configuration/route/sniff.md @@ -7,14 +7,15 @@ icon: material/new-box :material-plus: QUIC client type detect support for QUIC :material-plus: Chromium support for QUIC :material-plus: BitTorrent support - :material-plus: DTLS support + :material-plus: DTLS support + :material-plus: SSH support If enabled in the inbound, the protocol and domain name (if present) of by the connection can be sniffed. #### Supported Protocols | Network | Protocol | Domain Name | Client | -|:-------:|:------------:|:-----------:|:----------------:| +| :-----: | :----------: | :---------: | :--------------: | | TCP | `http` | Host | / | | TCP | `tls` | Server Name | / | | UDP | `quic` | Server Name | QUIC Client Type | @@ -22,9 +23,10 @@ If enabled in the inbound, the protocol and domain name (if present) of by the c | TCP/UDP | `dns` | / | / | | TCP/UDP | `bittorrent` | / | / | | UDP | `dtls` | / | / | +| TCP | `ssh` | / | SSH Client Name | | QUIC Client | Type | -|:------------------------:|:----------:| +| :----------------------: | :--------: | | Chromium/Cronet | `chrimium` | | Safari/Apple Network API | `safari` | | Firefox / uquic firefox | `firefox` | diff --git a/docs/configuration/route/sniff.zh.md b/docs/configuration/route/sniff.zh.md index 1ebc6d38d1..7421fd07e1 100644 --- a/docs/configuration/route/sniff.zh.md +++ b/docs/configuration/route/sniff.zh.md @@ -7,24 +7,26 @@ icon: material/new-box :material-plus: QUIC 的 客户端类型探测支持 :material-plus: QUIC 的 Chromium 支持 :material-plus: BitTorrent 支持 - :material-plus: DTLS 支持 + :material-plus: DTLS 支持 + :material-plus: SSH 支持 如果在入站中启用,则可以嗅探连接的协议和域名(如果存在)。 #### 支持的协议 -| 网络 | 协议 | 域名 | 客户端 | -|:-------:|:------------:|:-----------:|:----------:| -| TCP | `http` | Host | / | -| TCP | `tls` | Server Name | / | +| 网络 | 协议 | 域名 | 客户端 | +| :-----: | :----------: | :---------: | :-------------: | +| TCP | `http` | Host | / | +| TCP | `tls` | Server Name | / | | UDP | `quic` | Server Name | QUIC 客户端类型 | -| UDP | `stun` | / | / | -| TCP/UDP | `dns` | / | / | -| TCP/UDP | `bittorrent` | / | / | -| UDP | `dtls` | / | / | +| UDP | `stun` | / | / | +| TCP/UDP | `dns` | / | / | +| TCP/UDP | `bittorrent` | / | / | +| UDP | `dtls` | / | / | +| TCP | `SSH` | / | SSH 客户端名称 | -| QUIC 客户端 | 类型 | -|:------------------------:|:----------:| +| QUIC 客户端 | 类型 | +| :----------------------: | :--------: | | Chromium/Cronet | `chrimium` | | Safari/Apple Network API | `safari` | | Firefox / uquic firefox | `firefox` | diff --git a/route/router.go b/route/router.go index 35e1e21f28..aa121cb44e 100644 --- a/route/router.go +++ b/route/router.go @@ -860,9 +860,10 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad conn, buffer, time.Duration(metadata.InboundOptions.SniffTimeout), - sniff.StreamDomainNameQuery, sniff.TLSClientHello, sniff.HTTPHost, + sniff.StreamDomainNameQuery, + sniff.SSH, sniff.BitTorrent, ) if err == nil { From cb46b648d7241b693a4fde2f3eaccc9b95c49078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 28 Aug 2024 14:22:19 +0800 Subject: [PATCH 20/25] Add RDP sniffer --- common/sniff/rdp.go | 90 ++++++++++++++++++++++++++++ common/sniff/rdp_test.go | 25 ++++++++ constant/protocol.go | 1 + docs/configuration/route/sniff.md | 8 ++- docs/configuration/route/sniff.zh.md | 26 ++++---- 5 files changed, 135 insertions(+), 15 deletions(-) create mode 100644 common/sniff/rdp.go create mode 100644 common/sniff/rdp_test.go diff --git a/common/sniff/rdp.go b/common/sniff/rdp.go new file mode 100644 index 0000000000..391ebd2695 --- /dev/null +++ b/common/sniff/rdp.go @@ -0,0 +1,90 @@ +package sniff + +import ( + "context" + "encoding/binary" + "io" + "os" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common/rw" +) + +func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { + var tpktVersion uint8 + err := binary.Read(reader, binary.BigEndian, &tpktVersion) + if err != nil { + return err + } + if tpktVersion != 0x03 { + return os.ErrInvalid + } + + var tpktReserved uint8 + err = binary.Read(reader, binary.BigEndian, &tpktReserved) + if err != nil { + return err + } + if tpktReserved != 0x00 { + return os.ErrInvalid + } + + var tpktLength uint16 + err = binary.Read(reader, binary.BigEndian, &tpktLength) + if err != nil { + return err + } + + if tpktLength != 19 { + return os.ErrInvalid + } + + var cotpLength uint8 + err = binary.Read(reader, binary.BigEndian, &cotpLength) + if err != nil { + return err + } + + if cotpLength != 14 { + return os.ErrInvalid + } + + var cotpTpduType uint8 + err = binary.Read(reader, binary.BigEndian, &cotpTpduType) + if err != nil { + return err + } + if cotpTpduType != 0xE0 { + return os.ErrInvalid + } + + err = rw.SkipN(reader, 5) + if err != nil { + return err + } + + var rdpType uint8 + err = binary.Read(reader, binary.BigEndian, &rdpType) + if err != nil { + return err + } + if rdpType != 0x01 { + return os.ErrInvalid + } + var rdpFlags uint8 + err = binary.Read(reader, binary.BigEndian, &rdpFlags) + if err != nil { + return err + } + var rdpLength uint8 + err = binary.Read(reader, binary.BigEndian, &rdpLength) + if err != nil { + return err + } + if rdpLength != 8 { + return os.ErrInvalid + } + metadata.Protocol = C.ProtocolRDP + return nil +} diff --git a/common/sniff/rdp_test.go b/common/sniff/rdp_test.go new file mode 100644 index 0000000000..06fa3ab275 --- /dev/null +++ b/common/sniff/rdp_test.go @@ -0,0 +1,25 @@ +package sniff_test + +import ( + "bytes" + "context" + "encoding/hex" + "testing" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/sniff" + C "github.com/sagernet/sing-box/constant" + + "github.com/stretchr/testify/require" +) + +func TestSniffRDP(t *testing.T) { + t.Parallel() + + pkt, err := hex.DecodeString("030000130ee00000000000010008000b000000010008000b000000") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.RDP(context.TODO(), &metadata, bytes.NewReader(pkt)) + require.NoError(t, err) + require.Equal(t, C.ProtocolRDP, metadata.Protocol) +} diff --git a/constant/protocol.go b/constant/protocol.go index f867f428f6..1485408945 100644 --- a/constant/protocol.go +++ b/constant/protocol.go @@ -9,6 +9,7 @@ const ( ProtocolBitTorrent = "bittorrent" ProtocolDTLS = "dtls" ProtocolSSH = "ssh" + ProtocolRDP = "rdp" ) const ( diff --git a/docs/configuration/route/sniff.md b/docs/configuration/route/sniff.md index 70e2acc85a..40de038cb2 100644 --- a/docs/configuration/route/sniff.md +++ b/docs/configuration/route/sniff.md @@ -8,14 +8,15 @@ icon: material/new-box :material-plus: Chromium support for QUIC :material-plus: BitTorrent support :material-plus: DTLS support - :material-plus: SSH support + :material-plus: SSH support + :material-plus: RDP support If enabled in the inbound, the protocol and domain name (if present) of by the connection can be sniffed. #### Supported Protocols | Network | Protocol | Domain Name | Client | -| :-----: | :----------: | :---------: | :--------------: | +|:-------:|:------------:|:-----------:|:----------------:| | TCP | `http` | Host | / | | TCP | `tls` | Server Name | / | | UDP | `quic` | Server Name | QUIC Client Type | @@ -24,9 +25,10 @@ If enabled in the inbound, the protocol and domain name (if present) of by the c | TCP/UDP | `bittorrent` | / | / | | UDP | `dtls` | / | / | | TCP | `ssh` | / | SSH Client Name | +| TCP | `rdp` | / | / | | QUIC Client | Type | -| :----------------------: | :--------: | +|:------------------------:|:----------:| | Chromium/Cronet | `chrimium` | | Safari/Apple Network API | `safari` | | Firefox / uquic firefox | `firefox` | diff --git a/docs/configuration/route/sniff.zh.md b/docs/configuration/route/sniff.zh.md index 7421fd07e1..4efa453814 100644 --- a/docs/configuration/route/sniff.zh.md +++ b/docs/configuration/route/sniff.zh.md @@ -8,25 +8,27 @@ icon: material/new-box :material-plus: QUIC 的 Chromium 支持 :material-plus: BitTorrent 支持 :material-plus: DTLS 支持 - :material-plus: SSH 支持 + :material-plus: SSH 支持 + :material-plus: RDP 支持 如果在入站中启用,则可以嗅探连接的协议和域名(如果存在)。 #### 支持的协议 -| 网络 | 协议 | 域名 | 客户端 | -| :-----: | :----------: | :---------: | :-------------: | -| TCP | `http` | Host | / | -| TCP | `tls` | Server Name | / | +| 网络 | 协议 | 域名 | 客户端 | +|:-------:|:------------:|:-----------:|:----------:| +| TCP | `http` | Host | / | +| TCP | `tls` | Server Name | / | | UDP | `quic` | Server Name | QUIC 客户端类型 | -| UDP | `stun` | / | / | -| TCP/UDP | `dns` | / | / | -| TCP/UDP | `bittorrent` | / | / | -| UDP | `dtls` | / | / | -| TCP | `SSH` | / | SSH 客户端名称 | +| UDP | `stun` | / | / | +| TCP/UDP | `dns` | / | / | +| TCP/UDP | `bittorrent` | / | / | +| UDP | `dtls` | / | / | +| TCP | `ssh` | / | SSH 客户端名称 | +| TCP | `rdp` | / | / | -| QUIC 客户端 | 类型 | -| :----------------------: | :--------: | +| QUIC 客户端 | 类型 | +|:------------------------:|:----------:| | Chromium/Cronet | `chrimium` | | Safari/Apple Network API | `safari` | | Firefox / uquic firefox | `firefox` | From 6362b778c963ad7b2286586dbfae31d867a51328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 23 Aug 2024 13:38:38 +0800 Subject: [PATCH 21/25] clash-api: Add PNA support --- docs/configuration/experimental/clash-api.md | 96 +++++++++++++++---- .../experimental/clash-api.zh.md | 96 +++++++++++++++---- experimental/clashapi/server.go | 15 ++- go.mod | 2 +- go.sum | 4 +- option/experimental.go | 16 ++-- 6 files changed, 178 insertions(+), 51 deletions(-) diff --git a/docs/configuration/experimental/clash-api.md b/docs/configuration/experimental/clash-api.md index e1ca981521..7425143eb3 100644 --- a/docs/configuration/experimental/clash-api.md +++ b/docs/configuration/experimental/clash-api.md @@ -1,3 +1,12 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: [access_control_allow_origin](#access_control_allow_origin) + :material-plus: [access_control_allow_private_network](#access_control_allow_private_network) + !!! quote "Changes in sing-box 1.8.0" :material-delete-alert: [store_mode](#store_mode) @@ -8,24 +17,59 @@ ### 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": "" -} -``` +=== "Structure" + + ```json + { + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + "access_control_allow_origin": [], + "access_control_allow_private_network": false, + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" + } + ``` + +=== "Example (online)" + + !!! question "Since sing-box 1.10.0" + + ```json + { + "external_controller": "127.0.0.1:9090", + "access_control_allow_origin": [ + "http://127.0.0.1", + "http://yacd.haishan.me" + ], + "access_control_allow_private_network": true + } + ``` + +=== "Example (download)" + + !!! question "Since sing-box 1.10.0" + + ```json + { + "external_controller": "0.0.0.0:9090", + "external_ui": "dashboard" + // external_ui_download_detour: "direct" + } + ``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item ### Fields @@ -63,6 +107,22 @@ 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. +#### access_control_allow_origin + +!!! question "Since sing-box 1.10.0" + +CORS allowed origins, `*` will be used if empty. + +To access the Clash API on a private network from a public website, you must explicitly specify it in `access_control_allow_origin` instead of using `*`. + +#### access_control_allow_private_network + +!!! question "Since sing-box 1.10.0" + +Allow access from private network. + +To access the Clash API on a private network from a public website, `access_control_allow_private_network` must be enabled. + #### store_mode !!! failure "Deprecated in sing-box 1.8.0" diff --git a/docs/configuration/experimental/clash-api.zh.md b/docs/configuration/experimental/clash-api.zh.md index 092769ac93..b3d8aeaf99 100644 --- a/docs/configuration/experimental/clash-api.zh.md +++ b/docs/configuration/experimental/clash-api.zh.md @@ -1,3 +1,12 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.10.0 中的更改" + + :material-plus: [access_control_allow_origin](#access_control_allow_origin) + :material-plus: [access_control_allow_private_network](#access_control_allow_private_network) + !!! quote "sing-box 1.8.0 中的更改" :material-delete-alert: [store_mode](#store_mode) @@ -8,24 +17,59 @@ ### 结构 -```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": "" -} -``` +=== "结构" + + ```json + { + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + "access_control_allow_origin": [], + "access_control_allow_private_network": false, + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" + } + ``` + +=== "示例 (在线)" + + !!! question "自 sing-box 1.10.0 起" + + ```json + { + "external_controller": "127.0.0.1:9090", + "access_control_allow_origin": [ + "http://127.0.0.1", + "http://yacd.haishan.me" + ], + "access_control_allow_private_network": true + } + ``` + +=== "示例 (下载)" + + !!! question "自 sing-box 1.10.0 起" + + ```json + { + "external_controller": "0.0.0.0:9090", + "external_ui": "dashboard" + // external_ui_download_detour: "direct" + } + ``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 ### Fields @@ -61,6 +105,22 @@ Clash 中的默认模式,默认使用 `Rule`。 此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。 +#### access_control_allow_origin + +!!! question "自 sing-box 1.10.0 起" + +允许的 CORS 来源,默认使用 `*`。 + +要从公共网站访问私有网络上的 Clash API,必须在 `access_control_allow_origin` 中明确指定它而不是使用 `*`。 + +#### access_control_allow_private_network + +!!! question "自 sing-box 1.10.0 起" + +允许从私有网络访问。 + +要从公共网站访问私有网络上的 Clash API,必须启用 `access_control_allow_private_network`。 + #### store_mode !!! failure "已在 sing-box 1.8.0 废弃" diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 3428bbb771..889d191e0e 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -12,6 +12,7 @@ import ( "syscall" "time" + "github.com/sagernet/cors" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" @@ -29,7 +30,6 @@ import ( "github.com/sagernet/ws/wsutil" "github.com/go-chi/chi/v5" - "github.com/go-chi/cors" "github.com/go-chi/render" ) @@ -90,11 +90,16 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ 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.") } + allowedOrigins := options.AccessControlAllowOrigin + if len(allowedOrigins) == 0 { + allowedOrigins = []string{"*"} + } cors := cors.New(cors.Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, - AllowedHeaders: []string{"Content-Type", "Authorization"}, - MaxAge: 300, + AllowedOrigins: allowedOrigins, + AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowedHeaders: []string{"Content-Type", "Authorization"}, + AllowPrivateNetwork: options.AccessControlAllowPrivateNetwork, + MaxAge: 300, }) chiRouter.Use(cors.Handler) chiRouter.Group(func(r chi.Router) { diff --git a/go.mod b/go.mod index f9b11d6736..9bfdbebffd 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/cloudflare/circl v1.3.7 github.com/cretz/bine v0.2.0 github.com/go-chi/chi/v5 v5.0.12 - github.com/go-chi/cors v1.2.1 github.com/go-chi/render v1.0.3 github.com/gofrs/uuid/v5 v5.2.0 github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 @@ -22,6 +21,7 @@ require ( github.com/oschwald/maxminddb-golang v1.12.0 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 + github.com/sagernet/cors v1.2.1 github.com/sagernet/fswatch v0.1.1 github.com/sagernet/gomobile v0.1.4 github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f diff --git a/go.sum b/go.sum index 6deefbe260..21cd7047ec 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,6 @@ github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXb github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= -github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= @@ -102,6 +100,8 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 h1:YbmpqPQEMdlk9oFSKYWRqVuu9qzNiOayIonKmv1gCXY= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1/go.mod h1:J2yAxTFPDjrDPhuAi9aWFz2L3ox9it4qAluBBbN0H5k= +github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= +github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= github.com/sagernet/gomobile v0.1.4 h1:WzX9ka+iHdupMgy2Vdich+OAt7TM8C2cZbIbzNjBrJY= diff --git a/option/experimental.go b/option/experimental.go index 9f6071baee..6ab6638550 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -17,13 +17,15 @@ type CacheFileOptions struct { } 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:"-"` + 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:"-"` + AccessControlAllowOrigin Listable[string] `json:"access_control_allow_origin,omitempty"` + AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"` // Deprecated: migrated to global cache file CacheFile string `json:"cache_file,omitempty"` From a07219d16d1d4161aadfa394059ca0721069de3e Mon Sep 17 00:00:00 2001 From: srk24 <25836646+srk24@users.noreply.github.com> Date: Sun, 15 Sep 2024 11:42:57 +0800 Subject: [PATCH 22/25] Add `process_path_regex` rule type --- common/srs/binary.go | 9 ++++ docs/clients/android/features.md | 1 + docs/clients/apple/features.md | 1 + docs/configuration/dns/rule.md | 16 +++++- docs/configuration/dns/rule.zh.md | 16 +++++- docs/configuration/route/rule.md | 14 +++++ docs/configuration/route/rule.zh.md | 16 +++++- docs/configuration/rule-set/headless-rule.md | 13 +++++ option/rule.go | 1 + option/rule_dns.go | 1 + option/rule_set.go | 37 +++++++------- route/rule_default.go | 8 +++ route/rule_dns.go | 8 +++ route/rule_headless.go | 8 +++ route/rule_item_process_path_regex.go | 54 ++++++++++++++++++++ 15 files changed, 182 insertions(+), 21 deletions(-) create mode 100644 route/rule_item_process_path_regex.go diff --git a/common/srs/binary.go b/common/srs/binary.go index 7e4f402b39..489dc22e2f 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -37,6 +37,7 @@ const ( ruleItemWIFISSID ruleItemWIFIBSSID ruleItemAdGuardDomain + ruleItemProcessPathRegex ruleItemFinal uint8 = 0xFF ) @@ -207,6 +208,8 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea rule.ProcessName, err = readRuleItemString(reader) case ruleItemProcessPath: rule.ProcessPath, err = readRuleItemString(reader) + case ruleItemProcessPathRegex: + rule.ProcessPathRegex, err = readRuleItemString(reader) case ruleItemPackageName: rule.PackageName, err = readRuleItemString(reader) case ruleItemWIFISSID: @@ -326,6 +329,12 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen return err } } + if len(rule.ProcessPathRegex) > 0 { + err = writeRuleItemString(writer, ruleItemProcessPathRegex, rule.ProcessPathRegex) + if err != nil { + return err + } + } if len(rule.PackageName) > 0 { err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName) if err != nil { diff --git a/docs/clients/android/features.md b/docs/clients/android/features.md index 8fe84add26..f7f2caeec4 100644 --- a/docs/clients/android/features.md +++ b/docs/clients/android/features.md @@ -40,6 +40,7 @@ SFA provides an unprivileged TUN implementation through Android VpnService. |-----------------------|------------------|-----------------------------------| | `process_name` | :material-close: | No permission | | `process_path` | :material-close: | No permission | +| `process_path_regex` | :material-close: | No permission | | `package_name` | :material-check: | / | | `user` | :material-close: | Use `package_name` instead | | `user_id` | :material-close: | Use `package_name` instead | diff --git a/docs/clients/apple/features.md b/docs/clients/apple/features.md index 7d419103b5..d638517322 100644 --- a/docs/clients/apple/features.md +++ b/docs/clients/apple/features.md @@ -42,6 +42,7 @@ SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension |-----------------------|------------------|-----------------------| | `process_name` | :material-close: | No permission | | `process_path` | :material-close: | No permission | +| `process_path_regex` | :material-close: | No permission | | `package_name` | :material-close: | / | | `user` | :material-close: | No permission | | `user_id` | :material-close: | No permission | diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 6b3d65194f..03ca66bcc3 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -6,7 +6,8 @@ icon: material/new-box :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) - :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + :material-plus: [process_path_regex](#process_path_regex) !!! quote "Changes in sing-box 1.9.0" @@ -103,6 +104,9 @@ icon: material/new-box "process_path": [ "/usr/bin/curl" ], + "process_path_regex": [ + "^/usr/bin/.+" + ], "package_name": [ "com.termux" ], @@ -268,6 +272,16 @@ Match process name. Match process path. +#### process_path_regex + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path using regular expression. + #### package_name Match android package name. diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index eecddb3816..b484cbed6b 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -6,7 +6,8 @@ icon: material/new-box :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) - :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + :material-plus: [process_path_regex](#process_path_regex) !!! quote "sing-box 1.9.0 中的更改" @@ -103,6 +104,9 @@ icon: material/new-box "process_path": [ "/usr/bin/curl" ], + "process_path_regex": [ + "^/usr/bin/.+" + ], "package_name": [ "com.termux" ], @@ -266,6 +270,16 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配进程路径。 +#### process_path_regex + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +使用正则表达式匹配进程路径。 + #### package_name 匹配 Android 应用包名。 diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 8e61e51df2..91b432df1a 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -7,6 +7,7 @@ icon: material/alert-decagram :material-plus: [client](#client) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + :material-plus: [process_path_regex](#process_path_regex) !!! quote "Changes in sing-box 1.8.0" @@ -101,6 +102,9 @@ icon: material/alert-decagram "process_path": [ "/usr/bin/curl" ], + "process_path_regex": [ + "^/usr/bin/.+" + ], "package_name": [ "com.termux" ], @@ -277,6 +281,16 @@ Match process name. Match process path. +#### process_path_regex + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path using regular expression. + #### package_name Match android package name. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 68e66cf5c9..a93ce5e53d 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -6,7 +6,8 @@ icon: material/alert-decagram :material-plus: [client](#client) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) - :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) + :material-plus: [process_path_regex](#process_path_regex) + !!! quote "sing-box 1.8.0 中的更改" @@ -99,6 +100,9 @@ icon: material/alert-decagram "process_path": [ "/usr/bin/curl" ], + "process_path_regex": [ + "^/usr/bin/.+" + ], "package_name": [ "com.termux" ], @@ -275,6 +279,16 @@ icon: material/alert-decagram 匹配进程路径。 +#### process_path_regex + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +使用正则表达式匹配进程路径。 + #### package_name 匹配 Android 应用包名。 diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md index e766904b5f..891b127840 100644 --- a/docs/configuration/rule-set/headless-rule.md +++ b/docs/configuration/rule-set/headless-rule.md @@ -57,6 +57,9 @@ "process_path": [ "/usr/bin/curl" ], + "process_path_regex": [ + "^/usr/bin/.+" + ], "package_name": [ "com.termux" ], @@ -160,6 +163,16 @@ Match process name. Match process path. +#### process_path_regex + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path using regular expression. + #### package_name Match android package name. diff --git a/option/rule.go b/option/rule.go index 5c69ef00e3..5f15645c9b 100644 --- a/option/rule.go +++ b/option/rule.go @@ -88,6 +88,7 @@ type _DefaultRule struct { PortRange Listable[string] `json:"port_range,omitempty"` ProcessName Listable[string] `json:"process_name,omitempty"` ProcessPath Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex Listable[string] `json:"process_path_regex,omitempty"` PackageName Listable[string] `json:"package_name,omitempty"` User Listable[string] `json:"user,omitempty"` UserID Listable[int32] `json:"user_id,omitempty"` diff --git a/option/rule_dns.go b/option/rule_dns.go index 2afe245ea4..2e03b4252e 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -88,6 +88,7 @@ type _DefaultDNSRule struct { PortRange Listable[string] `json:"port_range,omitempty"` ProcessName Listable[string] `json:"process_name,omitempty"` ProcessPath Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex Listable[string] `json:"process_path_regex,omitempty"` PackageName Listable[string] `json:"package_name,omitempty"` User Listable[string] `json:"user,omitempty"` UserID Listable[int32] `json:"user_id,omitempty"` diff --git a/option/rule_set.go b/option/rule_set.go index a83b2c47b6..b6ec113e35 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -144,24 +144,25 @@ func (r HeadlessRule) IsValid() bool { } 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"` + 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"` + ProcessPathRegex Listable[string] `json:"process_path_regex,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:"-"` diff --git a/route/rule_default.go b/route/rule_default.go index 4bbc2b6b2e..40b93e5f96 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -179,6 +179,14 @@ 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.ProcessPathRegex) > 0 { + item, err := NewProcessPathRegexItem(options.ProcessPathRegex) + if err != nil { + return nil, E.Cause(err, "process_path_regex") + } + 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) diff --git a/route/rule_dns.go b/route/rule_dns.go index 1b79d30b25..616f956aaf 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -183,6 +183,14 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.ProcessPathRegex) > 0 { + item, err := NewProcessPathRegexItem(options.ProcessPathRegex) + if err != nil { + return nil, E.Cause(err, "process_path_regex") + } + 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) diff --git a/route/rule_headless.go b/route/rule_headless.go index d537df572c..23a98c7237 100644 --- a/route/rule_headless.go +++ b/route/rule_headless.go @@ -123,6 +123,14 @@ func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadles rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.ProcessPathRegex) > 0 { + item, err := NewProcessPathRegexItem(options.ProcessPathRegex) + if err != nil { + return nil, E.Cause(err, "process_path_regex") + } + 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) diff --git a/route/rule_item_process_path_regex.go b/route/rule_item_process_path_regex.go new file mode 100644 index 0000000000..01b2723cc6 --- /dev/null +++ b/route/rule_item_process_path_regex.go @@ -0,0 +1,54 @@ +package route + +import ( + "regexp" + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*ProcessPathRegexItem)(nil) + +type ProcessPathRegexItem struct { + matchers []*regexp.Regexp + description string +} + +func NewProcessPathRegexItem(expressions []string) (*ProcessPathRegexItem, error) { + matchers := make([]*regexp.Regexp, 0, len(expressions)) + for i, regex := range expressions { + matcher, err := regexp.Compile(regex) + if err != nil { + return nil, E.Cause(err, "parse expression ", i) + } + matchers = append(matchers, matcher) + } + description := "process_path_regex=" + eLen := len(expressions) + if eLen == 1 { + description += expressions[0] + } else if eLen > 3 { + description += F.ToString("[", strings.Join(expressions[:3], " "), "]") + } else { + description += F.ToString("[", strings.Join(expressions, " "), "]") + } + return &ProcessPathRegexItem{matchers, description}, nil +} + +func (r *ProcessPathRegexItem) Match(metadata *adapter.InboundContext) bool { + if metadata.ProcessInfo == nil || metadata.ProcessInfo.ProcessPath == "" { + return false + } + for _, matcher := range r.matchers { + if matcher.MatchString(metadata.ProcessInfo.ProcessPath) { + return true + } + } + return false +} + +func (r *ProcessPathRegexItem) String() string { + return r.description +} From a9bb07db4a5cbd83981ef30285e82c0f650dd83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Sep 2024 16:51:55 +0800 Subject: [PATCH 23/25] Update utls to v1.6.7 --- common/tls/utls_client.go | 12 ++-------- docs/configuration/inbound/trojan.md | 2 +- docs/configuration/route/rule.md | 2 +- docs/configuration/shared/tls.md | 31 +++++++++++++------------- docs/configuration/shared/tls.zh.md | 33 ++++++++++++++-------------- go.mod | 3 +-- go.sum | 6 ++--- 7 files changed, 40 insertions(+), 49 deletions(-) diff --git a/common/tls/utls_client.go b/common/tls/utls_client.go index 71ce8a4e0d..6364740f68 100644 --- a/common/tls/utls_client.go +++ b/common/tls/utls_client.go @@ -217,18 +217,10 @@ func init() { func uTLSClientHelloID(name string) (utls.ClientHelloID, error) { switch name { + case "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq": + fallthrough case "chrome", "": return utls.HelloChrome_Auto, nil - case "chrome_psk": - return utls.HelloChrome_100_PSK, nil - case "chrome_psk_shuffle": - return utls.HelloChrome_112_PSK_Shuf, nil - case "chrome_padding_psk_shuffle": - return utls.HelloChrome_114_Padding_PSK_Shuf, nil - case "chrome_pq": - return utls.HelloChrome_115_PQ, nil - case "chrome_pq_psk": - return utls.HelloChrome_115_PQ_PSK, nil case "firefox": return utls.HelloFirefox_Auto, nil case "edge": diff --git a/docs/configuration/inbound/trojan.md b/docs/configuration/inbound/trojan.md index bd6c73b311..e277236b06 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 -!!! quote "" +!!! failure "" 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/route/rule.md b/docs/configuration/route/rule.md index 91b432df1a..b5d17f215b 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -4,7 +4,7 @@ icon: material/alert-decagram !!! quote "Changes in sing-box 1.10.0" - :material-plus: [client](#client) + :material-plus: [client](#client) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) :material-plus: [process_path_regex](#process_path_regex) diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index 799aa0b09d..40343e18ef 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -1,4 +1,8 @@ -!!! quote "Changes in sing-box 1.8.0" +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.10.0" :material-alert-decagram: [utls](#utls) @@ -210,28 +214,25 @@ The path to the server private key, in PEM format. ==Client only== -!!! note "" - - uTLS is poorly maintained and the effect may be unproven, use at your own risk. +!!! failure "" + + There is no evidence that GFW detects and blocks servers based on TLS client fingerprinting, and using an imperfect emulation that has not been security reviewed could pose security risks. uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance. Available fingerprint values: -!!! question "Since sing-box 1.8.0" +!!! warning "Removed since sing-box 1.10.0" + + Some legacy chrome fingerprints have been removed and will fallback to chrome: - :material-plus: chrome_psk - :material-plus: chrome_psk_shuffle - :material-plus: chrome_padding_psk_shuffle - :material-plus: chrome_pq - :material-plus: chrome_pq_psk + :material-close: chrome_psk + :material-close: chrome_psk_shuffle + :material-close: chrome_padding_psk_shuffle + :material-close: chrome_pq + :material-close: chrome_pq_psk * chrome -* chrome_psk -* chrome_psk_shuffle -* chrome_padding_psk_shuffle -* chrome_pq -* chrome_pq_psk * firefox * edge * safari diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index 68de98459f..b254e08389 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -1,4 +1,8 @@ -!!! quote "sing-box 1.8.0 中的更改" +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.10.0 中的更改" :material-alert-decagram: [utls](#utls) @@ -44,8 +48,8 @@ "handshake": { "server": "google.com", "server_port": 443, - - ... // 拨号字段 + ... + // 拨号字段 }, "private_key": "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", "short_id": [ @@ -202,28 +206,25 @@ TLS 版本值: ==仅客户端== -!!! note "" +!!! failure "" - uTLS 维护不善且其效果可能未经证实,使用风险自负。 + 没有证据表明 GFW 根据 TLS 客户端指纹检测并阻止服务器,并且,使用一个未经安全审查的不完美模拟可能带来安全隐患。 uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。 可用的指纹值: -!!! question "自 sing-box 1.8.0 起" +!!! warning "已在 sing-box 1.10.0 移除" + + 一些旧 chrome 指纹已被删除,并将会退到 chrome: - :material-plus: chrome_psk - :material-plus: chrome_psk_shuffle - :material-plus: chrome_padding_psk_shuffle - :material-plus: chrome_pq - :material-plus: chrome_pq_psk + :material-close: chrome_psk + :material-close: chrome_psk_shuffle + :material-close: chrome_padding_psk_shuffle + :material-close: chrome_pq + :material-close: chrome_pq_psk * chrome -* chrome_psk -* chrome_psk_shuffle -* chrome_padding_psk_shuffle -* chrome_pq -* chrome_pq_psk * firefox * edge * safari diff --git a/go.mod b/go.mod index 9bfdbebffd..24a6181f09 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( github.com/sagernet/sing-tun v0.4.0-beta.16 github.com/sagernet/sing-vmess v0.1.12 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 - github.com/sagernet/utls v1.5.4 + github.com/sagernet/utls v1.6.7 github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/spf13/cobra v1.8.0 @@ -61,7 +61,6 @@ require ( github.com/andybalholm/brotli v1.0.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/gaukas/godicttls v0.0.4 // 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 diff --git a/go.sum b/go.sum index 21cd7047ec..19d8f9184c 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= -github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= @@ -137,8 +135,8 @@ github.com/sagernet/sing-vmess v0.1.12 h1:2gFD8JJb+eTFMoa8FIVMnknEi+vCSfaiTXTfEY github.com/sagernet/sing-vmess v0.1.12/go.mod h1:luTSsfyBGAc9VhtCqwjR+dt1QgqBhuYBCONB/POhF8I= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= -github.com/sagernet/utls v1.5.4 h1:KmsEGbB2dKUtCNC+44NwAdNAqnqQ6GA4pTO0Yik56co= -github.com/sagernet/utls v1.5.4/go.mod h1:CTGxPWExIloRipK3XFpYv0OVyhO8kk3XCGW/ieyTh1s= +github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8= +github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM= github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8 h1:R0OMYAScomNAVpTfbHFpxqJpvwuhxSRi+g6z7gZhABs= github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8/go.mod h1:K4J7/npM+VAMUeUmTa2JaA02JmyheP0GpRBOUvn3ecc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= From e68a9e623f09840552554ea15e4973b668a9d648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 7 Jun 2024 16:08:07 +0800 Subject: [PATCH 24/25] documentation: Bump version --- docs/changelog.md | 258 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 255 insertions(+), 3 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index f835a68bd2..e90b541770 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,10 +2,30 @@ icon: material/alert-decagram --- +#### 1.10.0-beta.12 + +* Fixes and improvements + ### 1.9.7 * Fixes and improvements +#### 1.10.0-beta.11 + +* Update uTLS to v1.6.7 **1** +* Add ipk in release artifacts + +**1**: + +Some legacy chrome fingerprints have been removed and will fallback to chrome, see [utls](/configuration/shared/tls#utls). + +#### 1.10.0-beta.10 + +* Add `process_path_regex` rule item +* Fixes and improvements + +_The macOS standalone versions of sing-box (>=1.9.5/<1.10.0-beta.11) now silently fail and require manual granting of the **Full Disk Access** permission to system extension to start, probably due to Apple's changed security policy. We will prompt users about this in feature versions._ + ### 1.9.6 * Fixes and improvements @@ -27,11 +47,40 @@ icon: material/alert-decagram See [Migration](/migration/#bundle-identifier-updates-in-apple-platform-clients). -We are still working on getting all sing-box apps back on the App Store. - -This work is expected to be completed within a week +We are still working on getting all sing-box apps back on the App Store, which should be completed within a week (SFI on the App Store and others on TestFlight are already available). +#### 1.10.0-beta.8 + +* Fixes and improvements + +_With the help of a netizen, we are in the process of getting sing-box apps back on the App Store, which should be completed within a month (TestFlight is already available)._ + +#### 1.10.0-beta.7 + +* Update quic-go to v0.47.0 +* Fixes and improvements + +#### 1.10.0-beta.6 + +* Add RDP sniffer +* Fixes and improvements + +#### 1.10.0-beta.5 + +* Add PNA support for [Clash API](/configuration/experimental/clash-api/) +* Fixes and improvements + +#### 1.10.0-beta.3 + +* Add SSH sniffer +* Fixes and improvements + +#### 1.10.0-beta.2 + +* Build with go1.23 +* Fixes and improvements + ### 1.9.4 * Update quic-go to v0.46.0 @@ -52,18 +101,221 @@ sing-box apps on Apple platforms are temporarily unavailable for download or upd If your company or organization is willing to help us return to the App Store, please [contact us](mailto:contact@sagernet.org)._ +#### 1.10.0-alpha.29 + +* Update quic-go to v0.46.0 +* Fixes and improvements + +#### 1.10.0-alpha.25 + +* Add AdGuard DNS Filter support **1** + +**1**: + +The new feature allows you to use AdGuard DNS Filter lists in a sing-box without AdGuard Home. + +See [AdGuard DNS Filter](/configuration/rule-set/adguard/). + +#### 1.10.0-alpha.23 + +* Add Chromium support for QUIC sniffer +* Add client type detect support for QUIC sniffer **1** +* Fixes and improvements + +**1**: + +Now the QUIC sniffer can correctly extract the server name from Chromium requests and +can identify common QUIC clients, including +Chromium, Safari, Firefox, quic-go (including uquic disguised as Chrome). + +See [Protocol Sniff](/configuration/route/sniff/) and [Route Rule](/configuration/route/rule/#client). + +#### 1.10.0-alpha.22 + +* Optimize memory usages of rule-sets **1** +* Fixes and improvements + +**1**: + +See [Source Format](/configuration/rule-set/source-format/#version). + +#### 1.10.0-alpha.20 + +* Add DTLS sniffer +* Fixes and improvements + +#### 1.10.0-alpha.19 + +* Add `rule-set decompile` command +* Add IP address support for `rule-set match` command +* Fixes and improvements + +#### 1.10.0-alpha.18 + +* Add new `inline` rule-set type **1** +* Add auto reload support for local rule-set +* Update fsnotify usages **2** +* Fixes and improvements + +**1**: + +The new [rule-set] type inline (which also becomes the default type) +allows you to write headless rules directly without creating a rule-set file. + +[rule-set]: /configuration/rule-set/ + +**2**: + +sing-box now uses fsnotify correctly and will not cancel watching +if the target file is deleted or recreated via rename (e.g. `mv`). + +This affects all path options that support reload, including +`tls.certificate_path`, `tls.key_path`, `tls.ech.key_path` and `rule_set.path`. + +#### 1.10.0-alpha.17 + +* Some chaotic changes **1** +* `rule_set_ipcidr_match_source` rule items are renamed **2** +* Add `rule_set_ip_cidr_accept_empty` DNS address filter rule item **3** +* Update quic-go to v0.45.1 +* Fixes and improvements + +**1**: + +Something may be broken, please actively report problems with this version. + +**2**: + +`rule_set_ipcidr_match_source` route and DNS rule items are renamed to +`rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. + +**3**: + +See [DNS Rule](/configuration/dns/rule/#rule_set_ip_cidr_accept_empty). + +#### 1.10.0-alpha.16 + +* Add custom options for `auto-route` and `auto-redirect` **1** +* Fixes and improvements + +**1**: + +See [iproute2_table_index](/configuration/inbound/tun/#iproute2_table_index), +[iproute2_rule_index](/configuration/inbound/tun/#iproute2_rule_index), +[auto_redirect_input_mark](/configuration/inbound/tun/#auto_redirect_input_mark) and +[auto_redirect_output_mark](/configuration/inbound/tun/#auto_redirect_output_mark). + +#### 1.10.0-alpha.13 + +* TUN address fields are merged **1** +* Add route address set support for auto-redirect **2** + +**1**: + +See [Migration](/migration/#tun-address-fields-are-merged). + +**2**: + +The new feature will allow you to configure the destination IP CIDR rules +in the specified rule-sets to the firewall automatically. + +Specified or unspecified destinations will bypass the sing-box routes to get better performance +(for example, keep hardware offloading of direct traffics on the router). + +See [route_address_set](/configuration/inbound/tun/#route_address_set) +and [route_exclude_address_set](/configuration/inbound/tun/#route_exclude_address_set). + +#### 1.10.0-alpha.12 + +* Fix auto-redirect not configuring nftables forward chain correctly +* Fixes and improvements + ### 1.9.3 * Fixes and improvements +#### 1.10.0-alpha.10 + +* Fixes and improvements + ### 1.9.2 * Fixes and improvements +#### 1.10.0-alpha.8 + +* Drop support for go1.18 and go1.19 **1** +* Update quic-go to v0.45.0 +* Update Hysteria2 BBR congestion control +* Fixes and improvements + +**1**: + +Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile. + ### 1.9.1 * Fixes and improvements +#### 1.10.0-alpha.7 + +* Fixes and improvements + +#### 1.10.0-alpha.5 + +* Improve auto-redirect **1** + +**1**: + +nftables support and DNS hijacking has been added. + +Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers **without intervention**. + +#### 1.10.0-alpha.4 + +* Fix auto-redirect **1** +* Improve auto-route on linux **2** + +**1**: + +Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers. + +**2**: + +Tun inbounds with `auto_route` and `strict_route` now works as expected on routers and servers, +but the usages of [exclude_interface](/configuration/inbound/tun/#exclude_interface) need to be updated. + +#### 1.10.0-alpha.2 + +* Move auto-redirect to Tun **1** +* Fixes and improvements + +**1**: + +Linux support are added. + +See [Tun](/configuration/inbound/tun/#auto_redirect). + +#### 1.10.0-alpha.1 + +* Add tailing comma support in JSON configuration +* Add simple auto-redirect for Android **1** +* Add BitTorrent sniffer **2** + +**1**: + +It allows you to use redirect inbound in the sing-box Android client +and automatically configures IPv4 TCP redirection via su. + +This may alleviate the symptoms of some OCD patients who think that +redirect can effectively save power compared to the system HTTP Proxy. + +See [Redirect](/configuration/inbound/redirect/). + +**2**: + +See [Protocol Sniff](/configuration/route/sniff/). + ### 1.9.0 * Fixes and improvements From 05a431d2031acf5c4ca9b58d5887db98143240d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 18:49:40 +0000 Subject: [PATCH 25/25] [dependencies] Update actions/checkout digest to eef6144 --- .github/workflows/debug.yml | 10 +++++----- .github/workflows/docker.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/linux.yml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/debug.yml b/.github/workflows/debug.yml index 2bca96e050..804eced92b 100644 --- a/.github/workflows/debug.yml +++ b/.github/workflows/debug.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 0 - name: Setup Go @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 0 - name: Setup Go @@ -57,7 +57,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 0 - name: Setup Go @@ -77,7 +77,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 0 - name: Setup Go @@ -207,7 +207,7 @@ jobs: TAGS: with_clash_api,with_quic steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 0 - name: Setup Go diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 61260b0fb5..bcf210ab1a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -39,7 +39,7 @@ jobs: echo "ref=$ref" echo "ref=$ref" >> $GITHUB_OUTPUT - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: ref: ${{ steps.ref.outputs.ref }} fetch-depth: 0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 08fb6c54d6..ba6a544dd9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 0 - name: Setup Go diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index bd2f1023f2..521477ed6f 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 0 - name: Setup Go