diff --git a/.gitignore b/.gitignore index 6630f428ce..55bdab3a0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.idea/ /vendor/ /*.json +/*.srs /*.db /site/ /bin/ diff --git a/Makefile b/Makefile index f1889ffc50..d2aa65d988 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ NAME = sing-box COMMIT = $(shell git rev-parse --short HEAD) -TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api -TAGS_GO120 = with_quic,with_ech +TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api +TAGS_GO120 = with_quic,with_ech,with_utls TAGS ?= $(TAGS_GO118),$(TAGS_GO120) TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server @@ -178,9 +178,8 @@ lib: go run ./cmd/internal/build_libbox -target ios lib_install: - go get -v -d - go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230915142329-c6740b6d2950 - go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230915142329-c6740b6d2950 + go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.1 + go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.1 docs: mkdocs serve diff --git a/adapter/experimental.go b/adapter/experimental.go index 87eb936c4f..2a6776cd0e 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -1,11 +1,16 @@ package adapter import ( + "bytes" "context" + "encoding/binary" + "io" "net" + "time" "github.com/sagernet/sing-box/common/urltest" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" ) type ClashServer interface { @@ -13,22 +18,83 @@ type ClashServer interface { PreStarter Mode() string ModeList() []string - StoreSelected() bool - StoreFakeIP() bool - CacheFile() ClashCacheFile HistoryStorage() *urltest.HistoryStorage RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker) } -type ClashCacheFile interface { +type CacheFile interface { + Service + PreStarter + + StoreFakeIP() bool + FakeIPStorage + LoadMode() string StoreMode(mode string) error LoadSelected(group string) string StoreSelected(group string, selected string) error LoadGroupExpand(group string) (isExpand bool, loaded bool) StoreGroupExpand(group string, expand bool) error - FakeIPStorage + LoadRuleSet(tag string) *SavedRuleSet + SaveRuleSet(tag string, set *SavedRuleSet) error +} + +type SavedRuleSet struct { + Content []byte + LastUpdated time.Time + LastEtag string +} + +func (s *SavedRuleSet) MarshalBinary() ([]byte, error) { + var buffer bytes.Buffer + err := binary.Write(&buffer, binary.BigEndian, uint8(1)) + if err != nil { + return nil, err + } + err = rw.WriteUVariant(&buffer, uint64(len(s.Content))) + if err != nil { + return nil, err + } + buffer.Write(s.Content) + err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix()) + if err != nil { + return nil, err + } + err = rw.WriteVString(&buffer, s.LastEtag) + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func (s *SavedRuleSet) UnmarshalBinary(data []byte) error { + reader := bytes.NewReader(data) + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return err + } + contentLen, err := rw.ReadUVariant(reader) + if err != nil { + return err + } + s.Content = make([]byte, contentLen) + _, err = io.ReadFull(reader, s.Content) + if err != nil { + return err + } + var lastUpdated int64 + err = binary.Read(reader, binary.BigEndian, &lastUpdated) + if err != nil { + return err + } + s.LastUpdated = time.Unix(lastUpdated, 0) + s.LastEtag, err = rw.ReadVString(reader) + if err != nil { + return err + } + return nil } type Tracker interface { diff --git a/adapter/inbound.go b/adapter/inbound.go index 2d24083c4a..f32b804d21 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -46,11 +46,24 @@ type InboundContext struct { SourceGeoIPCode string GeoIPCode string ProcessInfo *process.Info + QueryType uint16 FakeIP bool - // dns cache + // rule cache - QueryType uint16 + IPCIDRMatchSource bool + SourceAddressMatch bool + SourcePortMatch bool + DestinationAddressMatch bool + DestinationPortMatch bool +} + +func (c *InboundContext) ResetRuleCache() { + c.IPCIDRMatchSource = false + c.SourceAddressMatch = false + c.SourcePortMatch = false + c.DestinationAddressMatch = false + c.DestinationPortMatch = false } type inboundContextKey struct{} diff --git a/adapter/router.go b/adapter/router.go index 3d18eb387f..5828ab3530 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -2,12 +2,14 @@ package adapter import ( "context" + "net/http" "net/netip" "github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-dns" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" + N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" mdns "github.com/miekg/dns" @@ -15,11 +17,12 @@ import ( type Router interface { Service + PreStarter PostStarter Outbounds() []Outbound Outbound(tag string) (Outbound, bool) - DefaultOutbound(network string) Outbound + DefaultOutbound(network string) (Outbound, error) FakeIPStore() FakeIPStore @@ -28,6 +31,8 @@ type Router interface { GeoIPReader() *geoip.Reader LoadGeosite(code string) (Rule, error) + RuleSet(tag string) (RuleSet, bool) + Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) @@ -62,11 +67,15 @@ func RouterFromContext(ctx context.Context) Router { return service.FromContext[Router](ctx) } +type HeadlessRule interface { + Match(metadata *InboundContext) bool +} + type Rule interface { + HeadlessRule Service Type() string UpdateGeosite() error - Match(metadata *InboundContext) bool Outbound() string String() string } @@ -77,6 +86,24 @@ type DNSRule interface { RewriteTTL() *uint32 } +type RuleSet interface { + StartContext(ctx context.Context, startContext RuleSetStartContext) error + PostStart() error + Metadata() RuleSetMetadata + Close() error + HeadlessRule +} + +type RuleSetMetadata struct { + ContainsProcessRule bool + ContainsWIFIRule bool +} + +type RuleSetStartContext interface { + HTTPClient(detour string, dialer N.Dialer) *http.Client + Close() +} + type InterfaceUpdateListener interface { InterfaceUpdated() } diff --git a/box.go b/box.go index 506b20eebf..8835d59708 100644 --- a/box.go +++ b/box.go @@ -9,7 +9,10 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/taskmonitor" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" + "github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/inbound" "github.com/sagernet/sing-box/log" @@ -32,7 +35,8 @@ type Box struct { outbounds []adapter.Outbound logFactory log.Factory logger log.ContextLogger - preServices map[string]adapter.Service + preServices1 map[string]adapter.Service + preServices2 map[string]adapter.Service postServices map[string]adapter.Service done chan struct{} } @@ -45,17 +49,21 @@ type Options struct { } func New(options Options) (*Box, error) { + createdAt := time.Now() ctx := options.Context if ctx == nil { ctx = context.Background() } ctx = service.ContextWithDefaultRegistry(ctx) - ctx = pause.ContextWithDefaultManager(ctx) - createdAt := time.Now() + ctx = pause.WithDefaultManager(ctx) experimentalOptions := common.PtrValueOrDefault(options.Experimental) applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) + var needCacheFile bool var needClashAPI bool var needV2RayAPI bool + if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil { + needCacheFile = true + } if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil { needClashAPI = true } @@ -154,8 +162,17 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "initialize platform interface") } } - preServices := make(map[string]adapter.Service) + preServices1 := make(map[string]adapter.Service) + preServices2 := make(map[string]adapter.Service) postServices := make(map[string]adapter.Service) + if needCacheFile { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile == nil { + cacheFile = cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) + service.MustRegister[adapter.CacheFile](ctx, cacheFile) + } + preServices1["cache file"] = cacheFile + } if needClashAPI { clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options) @@ -164,7 +181,7 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "create clash api server") } router.SetClashServer(clashServer) - preServices["clash api"] = clashServer + preServices2["clash api"] = clashServer } if needV2RayAPI { v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI)) @@ -172,7 +189,7 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "create v2ray api server") } router.SetV2RayServer(v2rayServer) - preServices["v2ray api"] = v2rayServer + preServices2["v2ray api"] = v2rayServer } return &Box{ router: router, @@ -181,7 +198,8 @@ func New(options Options) (*Box, error) { createdAt: createdAt, logFactory: logFactory, logger: logFactory.Logger(), - preServices: preServices, + preServices1: preServices1, + preServices2: preServices2, postServices: postServices, done: make(chan struct{}), }, nil @@ -226,16 +244,38 @@ func (s *Box) Start() error { } func (s *Box) preStart() error { - for serviceName, service := range s.preServices { + monitor := taskmonitor.New(s.logger, C.DefaultStartTimeout) + monitor.Start("start logger") + err := s.logFactory.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "start logger") + } + for serviceName, service := range s.preServices1 { + if preService, isPreService := service.(adapter.PreStarter); isPreService { + monitor.Start("pre-start ", serviceName) + err := preService.PreStart() + monitor.Finish() + if err != nil { + return E.Cause(err, "pre-start ", serviceName) + } + } + } + for serviceName, service := range s.preServices2 { if preService, isPreService := service.(adapter.PreStarter); isPreService { - s.logger.Trace("pre-start ", serviceName) + monitor.Start("pre-start ", serviceName) err := preService.PreStart() + monitor.Finish() if err != nil { - return E.Cause(err, "pre-starting ", serviceName) + return E.Cause(err, "pre-start ", serviceName) } } } - err := s.startOutbounds() + err = s.router.PreStart() + if err != nil { + return E.Cause(err, "pre-start router") + } + err = s.startOutbounds() if err != nil { return err } @@ -247,8 +287,13 @@ func (s *Box) start() error { if err != nil { return err } - for serviceName, service := range s.preServices { - s.logger.Trace("starting ", serviceName) + for serviceName, service := range s.preServices1 { + err = service.Start() + if err != nil { + return E.Cause(err, "start ", serviceName) + } + } + for serviceName, service := range s.preServices2 { err = service.Start() if err != nil { return E.Cause(err, "start ", serviceName) @@ -261,7 +306,6 @@ func (s *Box) start() error { } else { tag = in.Tag() } - s.logger.Trace("initializing inbound/", in.Type(), "[", tag, "]") err = in.Start() if err != nil { return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]") @@ -272,7 +316,6 @@ func (s *Box) start() error { func (s *Box) postStart() error { for serviceName, service := range s.postServices { - s.logger.Trace("starting ", service) err := service.Start() if err != nil { return E.Cause(err, "start ", serviceName) @@ -280,14 +323,13 @@ func (s *Box) postStart() error { } for _, outbound := range s.outbounds { if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound { - s.logger.Trace("post-starting outbound/", outbound.Tag()) err := lateOutbound.PostStart() if err != nil { return E.Cause(err, "post-start outbound/", outbound.Tag()) } } } - s.logger.Trace("post-starting router") + return s.router.PostStart() } @@ -298,41 +340,53 @@ func (s *Box) Close() error { default: close(s.done) } + monitor := taskmonitor.New(s.logger, C.DefaultStopTimeout) var errors error for serviceName, service := range s.postServices { - s.logger.Trace("closing ", serviceName) + monitor.Start("close ", serviceName) errors = E.Append(errors, service.Close(), func(err error) error { return E.Cause(err, "close ", serviceName) }) + monitor.Finish() } for i, in := range s.inbounds { - s.logger.Trace("closing inbound/", in.Type(), "[", i, "]") + monitor.Start("close inbound/", in.Type(), "[", i, "]") errors = E.Append(errors, in.Close(), func(err error) error { return E.Cause(err, "close inbound/", in.Type(), "[", i, "]") }) + monitor.Finish() } for i, out := range s.outbounds { - s.logger.Trace("closing outbound/", out.Type(), "[", i, "]") + monitor.Start("close outbound/", out.Type(), "[", i, "]") errors = E.Append(errors, common.Close(out), func(err error) error { return E.Cause(err, "close outbound/", out.Type(), "[", i, "]") }) + monitor.Finish() } - s.logger.Trace("closing router") + monitor.Start("close router") if err := common.Close(s.router); err != nil { errors = E.Append(errors, err, func(err error) error { return E.Cause(err, "close router") }) } - for serviceName, service := range s.preServices { - s.logger.Trace("closing ", serviceName) + monitor.Finish() + for serviceName, service := range s.preServices1 { + monitor.Start("close ", serviceName) + errors = E.Append(errors, service.Close(), func(err error) error { + return E.Cause(err, "close ", serviceName) + }) + monitor.Finish() + } + for serviceName, service := range s.preServices2 { + monitor.Start("close ", serviceName) errors = E.Append(errors, service.Close(), func(err error) error { return E.Cause(err, "close ", serviceName) }) + monitor.Finish() } - s.logger.Trace("closing log factory") if err := common.Close(s.logFactory); err != nil { errors = E.Append(errors, err, func(err error) error { - return E.Cause(err, "close log factory") + return E.Cause(err, "close logger") }) } return errors diff --git a/box_outbound.go b/box_outbound.go index 676ae7af72..95ba950ad4 100644 --- a/box_outbound.go +++ b/box_outbound.go @@ -4,12 +4,15 @@ import ( "strings" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/taskmonitor" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" ) func (s *Box) startOutbounds() error { + monitor := taskmonitor.New(s.logger, C.DefaultStartTimeout) outboundTags := make(map[adapter.Outbound]string) outbounds := make(map[string]adapter.Outbound) for i, outboundToStart := range s.outbounds { @@ -43,8 +46,9 @@ func (s *Box) startOutbounds() error { started[outboundTag] = true canContinue = true if starter, isStarter := outboundToStart.(common.Starter); isStarter { - s.logger.Trace("initializing outbound/", outboundToStart.Type(), "[", outboundTag, "]") + monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]") err := starter.Start() + monitor.Finish() if err != nil { return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]") } diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go index 3bfe862c72..d50e03b375 100644 --- a/cmd/internal/build_libbox/main.go +++ b/cmd/internal/build_libbox/main.go @@ -7,7 +7,7 @@ import ( "path/filepath" "strings" - _ "github.com/sagernet/gomobile/event/key" + _ "github.com/sagernet/gomobile" "github.com/sagernet/sing-box/cmd/internal/build_shared" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common/rw" diff --git a/cmd/sing-box/cmd_format.go b/cmd/sing-box/cmd_format.go index 10a5497cd4..fc47c5a8dd 100644 --- a/cmd/sing-box/cmd_format.go +++ b/cmd/sing-box/cmd_format.go @@ -5,10 +5,10 @@ import ( "os" "path/filepath" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" "github.com/spf13/cobra" ) @@ -38,6 +38,10 @@ func format() error { return err } for _, optionsEntry := range optionsList { + optionsEntry.options, err = badjson.Omitempty(optionsEntry.options) + if err != nil { + return err + } buffer := new(bytes.Buffer) encoder := json.NewEncoder(buffer) encoder.SetIndent("", " ") @@ -69,41 +73,3 @@ func format() error { } return nil } - -func formatOne(configPath string) error { - configContent, err := os.ReadFile(configPath) - if err != nil { - return E.Cause(err, "read config") - } - var options option.Options - err = options.UnmarshalJSON(configContent) - if err != nil { - return E.Cause(err, "decode config") - } - buffer := new(bytes.Buffer) - encoder := json.NewEncoder(buffer) - encoder.SetIndent("", " ") - err = encoder.Encode(options) - if err != nil { - return E.Cause(err, "encode config") - } - if !commandFormatFlagWrite { - os.Stdout.WriteString(buffer.String() + "\n") - return nil - } - if bytes.Equal(configContent, buffer.Bytes()) { - return nil - } - output, err := os.Create(configPath) - if err != nil { - return E.Cause(err, "open output") - } - _, err = output.Write(buffer.Bytes()) - output.Close() - if err != nil { - return E.Cause(err, "write output") - } - outputPath, _ := filepath.Abs(configPath) - os.Stderr.WriteString(outputPath + "\n") - return nil -} diff --git a/cmd/sing-box/cmd_geoip.go b/cmd/sing-box/cmd_geoip.go new file mode 100644 index 0000000000..dbbbff135e --- /dev/null +++ b/cmd/sing-box/cmd_geoip.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var ( + geoipReader *maxminddb.Reader + commandGeoIPFlagFile string +) + +var commandGeoip = &cobra.Command{ + Use: "geoip", + Short: "GeoIP tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geoipPreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.PersistentFlags().StringVarP(&commandGeoIPFlagFile, "file", "f", "geoip.db", "geoip file") + mainCommand.AddCommand(commandGeoip) +} + +func geoipPreRun() error { + reader, err := maxminddb.Open(commandGeoIPFlagFile) + if err != nil { + return err + } + if reader.Metadata.DatabaseType != "sing-geoip" { + reader.Close() + return E.New("incorrect database type, expected sing-geoip, got ", reader.Metadata.DatabaseType) + } + geoipReader = reader + return nil +} diff --git a/cmd/sing-box/cmd_geoip_export.go b/cmd/sing-box/cmd_geoip_export.go new file mode 100644 index 0000000000..5787d2e5ad --- /dev/null +++ b/cmd/sing-box/cmd_geoip_export.go @@ -0,0 +1,98 @@ +package main + +import ( + "io" + "net" + "os" + "strings" + + 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/oschwald/maxminddb-golang" + "github.com/spf13/cobra" +) + +var flagGeoipExportOutput string + +const flagGeoipExportDefaultOutput = "geoip-.srs" + +var commandGeoipExport = &cobra.Command{ + Use: "export ", + Short: "Export geoip country as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoipExport.Flags().StringVarP(&flagGeoipExportOutput, "output", "o", flagGeoipExportDefaultOutput, "Output path") + commandGeoip.AddCommand(commandGeoipExport) +} + +func geoipExport(countryCode string) error { + networks := geoipReader.Networks(maxminddb.SkipAliasedNetworks) + countryMap := make(map[string][]*net.IPNet) + var ( + ipNet *net.IPNet + nextCountryCode string + err error + ) + for networks.Next() { + ipNet, err = networks.Network(&nextCountryCode) + if err != nil { + return err + } + countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet) + } + ipNets := countryMap[strings.ToLower(countryCode)] + if len(ipNets) == 0 { + return E.New("country code not found: ", countryCode) + } + + var ( + outputFile *os.File + outputWriter io.Writer + ) + if flagGeoipExportOutput == "stdout" { + outputWriter = os.Stdout + } else if flagGeoipExportOutput == flagGeoipExportDefaultOutput { + outputFile, err = os.Create("geoip-" + countryCode + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(flagGeoipExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + headlessRule.IPCIDR = make([]string, 0, len(ipNets)) + for _, cidr := range ipNets { + headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String()) + } + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geoip_list.go b/cmd/sing-box/cmd_geoip_list.go new file mode 100644 index 0000000000..54dd426ea9 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_list.go @@ -0,0 +1,31 @@ +package main + +import ( + "os" + + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var commandGeoipList = &cobra.Command{ + Use: "list", + Short: "List geoip country codes", + Run: func(cmd *cobra.Command, args []string) { + err := listGeoip() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipList) +} + +func listGeoip() error { + for _, code := range geoipReader.Metadata.Languages { + os.Stdout.WriteString(code + "\n") + } + return nil +} diff --git a/cmd/sing-box/cmd_geoip_lookup.go b/cmd/sing-box/cmd_geoip_lookup.go new file mode 100644 index 0000000000..d5157bb404 --- /dev/null +++ b/cmd/sing-box/cmd_geoip_lookup.go @@ -0,0 +1,47 @@ +package main + +import ( + "net/netip" + "os" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + + "github.com/spf13/cobra" +) + +var commandGeoipLookup = &cobra.Command{ + Use: "lookup
", + Short: "Lookup if an IP address is contained in the GeoIP database", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geoipLookup(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoip.AddCommand(commandGeoipLookup) +} + +func geoipLookup(address string) error { + addr, err := netip.ParseAddr(address) + if err != nil { + return E.Cause(err, "parse address") + } + if !N.IsPublicAddr(addr) { + os.Stdout.WriteString("private\n") + return nil + } + var code string + _ = geoipReader.Lookup(addr.AsSlice(), &code) + if code != "" { + os.Stdout.WriteString(code + "\n") + return nil + } + os.Stdout.WriteString("unknown\n") + return nil +} diff --git a/cmd/sing-box/cmd_geosite.go b/cmd/sing-box/cmd_geosite.go new file mode 100644 index 0000000000..95db935797 --- /dev/null +++ b/cmd/sing-box/cmd_geosite.go @@ -0,0 +1,41 @@ +package main + +import ( + "github.com/sagernet/sing-box/common/geosite" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var ( + commandGeoSiteFlagFile string + geositeReader *geosite.Reader + geositeCodeList []string +) + +var commandGeoSite = &cobra.Command{ + Use: "geosite", + Short: "Geosite tools", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := geositePreRun() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.PersistentFlags().StringVarP(&commandGeoSiteFlagFile, "file", "f", "geosite.db", "geosite file") + mainCommand.AddCommand(commandGeoSite) +} + +func geositePreRun() error { + reader, codeList, err := geosite.Open(commandGeoSiteFlagFile) + if err != nil { + return E.Cause(err, "open geosite file") + } + geositeReader = reader + geositeCodeList = codeList + return nil +} diff --git a/cmd/sing-box/cmd_geosite_export.go b/cmd/sing-box/cmd_geosite_export.go new file mode 100644 index 0000000000..2a6c27a0ed --- /dev/null +++ b/cmd/sing-box/cmd_geosite_export.go @@ -0,0 +1,81 @@ +package main + +import ( + "io" + "os" + + "github.com/sagernet/sing-box/common/geosite" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/json" + + "github.com/spf13/cobra" +) + +var commandGeositeExportOutput string + +const commandGeositeExportDefaultOutput = "geosite-.json" + +var commandGeositeExport = &cobra.Command{ + Use: "export ", + Short: "Export geosite category as rule-set", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := geositeExport(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeositeExport.Flags().StringVarP(&commandGeositeExportOutput, "output", "o", commandGeositeExportDefaultOutput, "Output path") + commandGeoSite.AddCommand(commandGeositeExport) +} + +func geositeExport(category string) error { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + var ( + outputFile *os.File + outputWriter io.Writer + ) + if commandGeositeExportOutput == "stdout" { + outputWriter = os.Stdout + } else if commandGeositeExportOutput == commandGeositeExportDefaultOutput { + outputFile, err = os.Create("geosite-" + category + ".json") + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } else { + outputFile, err = os.Create(commandGeositeExportOutput) + if err != nil { + return err + } + defer outputFile.Close() + outputWriter = outputFile + } + + encoder := json.NewEncoder(outputWriter) + encoder.SetIndent("", " ") + var headlessRule option.DefaultHeadlessRule + defaultRule := geosite.Compile(sourceSet) + headlessRule.Domain = defaultRule.Domain + headlessRule.DomainSuffix = defaultRule.DomainSuffix + headlessRule.DomainKeyword = defaultRule.DomainKeyword + headlessRule.DomainRegex = defaultRule.DomainRegex + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + return encoder.Encode(plainRuleSet) +} diff --git a/cmd/sing-box/cmd_geosite_list.go b/cmd/sing-box/cmd_geosite_list.go new file mode 100644 index 0000000000..cedb7adfd2 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_list.go @@ -0,0 +1,50 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + F "github.com/sagernet/sing/common/format" + + "github.com/spf13/cobra" +) + +var commandGeositeList = &cobra.Command{ + Use: "list ", + Short: "List geosite categories", + Run: func(cmd *cobra.Command, args []string) { + err := geositeList() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeList) +} + +func geositeList() error { + var geositeEntry []struct { + category string + items int + } + for _, category := range geositeCodeList { + sourceSet, err := geositeReader.Read(category) + if err != nil { + return err + } + geositeEntry = append(geositeEntry, struct { + category string + items int + }{category, len(sourceSet)}) + } + sort.SliceStable(geositeEntry, func(i, j int) bool { + return geositeEntry[i].items < geositeEntry[j].items + }) + for _, entry := range geositeEntry { + os.Stdout.WriteString(F.ToString(entry.category, " (", entry.items, ")\n")) + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_lookup.go b/cmd/sing-box/cmd_geosite_lookup.go new file mode 100644 index 0000000000..f648ce62a4 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_lookup.go @@ -0,0 +1,97 @@ +package main + +import ( + "os" + "sort" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/spf13/cobra" +) + +var commandGeositeLookup = &cobra.Command{ + Use: "lookup [category] ", + Short: "Check if a domain is in the geosite", + Args: cobra.RangeArgs(1, 2), + Run: func(cmd *cobra.Command, args []string) { + var ( + source string + target string + ) + switch len(args) { + case 1: + target = args[0] + case 2: + source = args[0] + target = args[1] + } + err := geositeLookup(source, target) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandGeoSite.AddCommand(commandGeositeLookup) +} + +func geositeLookup(source string, target string) error { + var sourceMatcherList []struct { + code string + matcher *searchGeositeMatcher + } + if source != "" { + sourceSet, err := geositeReader.Read(source) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+source) + } + sourceMatcherList = []struct { + code string + matcher *searchGeositeMatcher + }{ + { + code: source, + matcher: sourceMatcher, + }, + } + + } else { + for _, code := range geositeCodeList { + sourceSet, err := geositeReader.Read(code) + if err != nil { + return err + } + sourceMatcher, err := newSearchGeositeMatcher(sourceSet) + if err != nil { + return E.Cause(err, "compile code: "+code) + } + sourceMatcherList = append(sourceMatcherList, struct { + code string + matcher *searchGeositeMatcher + }{ + code: code, + matcher: sourceMatcher, + }) + } + } + sort.SliceStable(sourceMatcherList, func(i, j int) bool { + return sourceMatcherList[i].code < sourceMatcherList[j].code + }) + + for _, matcherItem := range sourceMatcherList { + if matchRule := matcherItem.matcher.Match(target); matchRule != "" { + os.Stdout.WriteString("Match code (") + os.Stdout.WriteString(matcherItem.code) + os.Stdout.WriteString(") ") + os.Stdout.WriteString(matchRule) + os.Stdout.WriteString("\n") + } + } + return nil +} diff --git a/cmd/sing-box/cmd_geosite_matcher.go b/cmd/sing-box/cmd_geosite_matcher.go new file mode 100644 index 0000000000..791dba2499 --- /dev/null +++ b/cmd/sing-box/cmd_geosite_matcher.go @@ -0,0 +1,56 @@ +package main + +import ( + "regexp" + "strings" + + "github.com/sagernet/sing-box/common/geosite" +) + +type searchGeositeMatcher struct { + domainMap map[string]bool + suffixList []string + keywordList []string + regexList []string +} + +func newSearchGeositeMatcher(items []geosite.Item) (*searchGeositeMatcher, error) { + options := geosite.Compile(items) + domainMap := make(map[string]bool) + for _, domain := range options.Domain { + domainMap[domain] = true + } + rule := &searchGeositeMatcher{ + domainMap: domainMap, + suffixList: options.DomainSuffix, + keywordList: options.DomainKeyword, + regexList: options.DomainRegex, + } + return rule, nil +} + +func (r *searchGeositeMatcher) Match(domain string) string { + if r.domainMap[domain] { + return "domain=" + domain + } + for _, suffix := range r.suffixList { + if strings.HasSuffix(domain, suffix) { + return "domain_suffix=" + suffix + } + } + for _, keyword := range r.keywordList { + if strings.Contains(domain, keyword) { + return "domain_keyword=" + keyword + } + } + for _, regexStr := range r.regexList { + regex, err := regexp.Compile(regexStr) + if err != nil { + continue + } + if regex.MatchString(domain) { + return "domain_regex=" + regexStr + } + } + return "" +} diff --git a/cmd/sing-box/cmd_merge.go b/cmd/sing-box/cmd_merge.go index 0aff750182..1d19ff17e1 100644 --- a/cmd/sing-box/cmd_merge.go +++ b/cmd/sing-box/cmd_merge.go @@ -6,19 +6,19 @@ import ( "path/filepath" "strings" - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/rw" "github.com/spf13/cobra" ) var commandMerge = &cobra.Command{ - Use: "merge [output]", + Use: "merge ", Short: "Merge configurations", Run: func(cmd *cobra.Command, args []string) { err := merge(args[0]) @@ -65,50 +65,26 @@ func merge(outputPath string) error { func mergePathResources(options *option.Options) error { for index, inbound := range options.Inbounds { - switch inbound.Type { - case C.TypeHTTP: - inbound.HTTPOptions.TLS = mergeTLSInboundOptions(inbound.HTTPOptions.TLS) - case C.TypeMixed: - inbound.MixedOptions.TLS = mergeTLSInboundOptions(inbound.MixedOptions.TLS) - case C.TypeVMess: - inbound.VMessOptions.TLS = mergeTLSInboundOptions(inbound.VMessOptions.TLS) - case C.TypeTrojan: - inbound.TrojanOptions.TLS = mergeTLSInboundOptions(inbound.TrojanOptions.TLS) - case C.TypeNaive: - inbound.NaiveOptions.TLS = mergeTLSInboundOptions(inbound.NaiveOptions.TLS) - case C.TypeHysteria: - inbound.HysteriaOptions.TLS = mergeTLSInboundOptions(inbound.HysteriaOptions.TLS) - case C.TypeVLESS: - inbound.VLESSOptions.TLS = mergeTLSInboundOptions(inbound.VLESSOptions.TLS) - case C.TypeTUIC: - inbound.TUICOptions.TLS = mergeTLSInboundOptions(inbound.TUICOptions.TLS) - case C.TypeHysteria2: - inbound.Hysteria2Options.TLS = mergeTLSInboundOptions(inbound.Hysteria2Options.TLS) - default: - continue + rawOptions, err := inbound.RawOptions() + if err != nil { + return err + } + if tlsOptions, containsTLSOptions := rawOptions.(option.InboundTLSOptionsWrapper); containsTLSOptions { + tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions())) } options.Inbounds[index] = inbound } for index, outbound := range options.Outbounds { + rawOptions, err := outbound.RawOptions() + if err != nil { + return err + } switch outbound.Type { - case C.TypeHTTP: - outbound.HTTPOptions.TLS = mergeTLSOutboundOptions(outbound.HTTPOptions.TLS) - case C.TypeVMess: - outbound.VMessOptions.TLS = mergeTLSOutboundOptions(outbound.VMessOptions.TLS) - case C.TypeTrojan: - outbound.TrojanOptions.TLS = mergeTLSOutboundOptions(outbound.TrojanOptions.TLS) - case C.TypeHysteria: - outbound.HysteriaOptions.TLS = mergeTLSOutboundOptions(outbound.HysteriaOptions.TLS) case C.TypeSSH: outbound.SSHOptions = mergeSSHOutboundOptions(outbound.SSHOptions) - case C.TypeVLESS: - outbound.VLESSOptions.TLS = mergeTLSOutboundOptions(outbound.VLESSOptions.TLS) - case C.TypeTUIC: - outbound.TUICOptions.TLS = mergeTLSOutboundOptions(outbound.TUICOptions.TLS) - case C.TypeHysteria2: - outbound.Hysteria2Options.TLS = mergeTLSOutboundOptions(outbound.Hysteria2Options.TLS) - default: - continue + } + if tlsOptions, containsTLSOptions := rawOptions.(option.OutboundTLSOptionsWrapper); containsTLSOptions { + tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions())) } options.Outbounds[index] = outbound } diff --git a/cmd/sing-box/cmd_rule_set.go b/cmd/sing-box/cmd_rule_set.go new file mode 100644 index 0000000000..f4112a087b --- /dev/null +++ b/cmd/sing-box/cmd_rule_set.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +var commandRuleSet = &cobra.Command{ + Use: "rule-set", + Short: "Manage rule sets", +} + +func init() { + mainCommand.AddCommand(commandRuleSet) +} diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go new file mode 100644 index 0000000000..6e065101a8 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -0,0 +1,84 @@ +package main + +import ( + "io" + "os" + "strings" + + "github.com/sagernet/sing-box/common/srs" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/json" + + "github.com/spf13/cobra" +) + +var flagRuleSetCompileOutput string + +const flagRuleSetCompileDefaultOutput = ".srs" + +var commandRuleSetCompile = &cobra.Command{ + Use: "compile [source-path]", + Short: "Compile rule-set json to binary", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := compileRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSet.AddCommand(commandRuleSetCompile) + commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file") +} + +func compileRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + content, err := io.ReadAll(reader) + if err != nil { + return err + } + plainRuleSet, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return err + } + if err != nil { + return err + } + ruleSet := plainRuleSet.Upgrade() + var outputPath string + if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { + if strings.HasSuffix(sourcePath, ".json") { + outputPath = sourcePath[:len(sourcePath)-5] + ".srs" + } else { + outputPath = sourcePath + ".srs" + } + } else { + outputPath = flagRuleSetCompileOutput + } + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + err = srs.Write(outputFile, ruleSet) + if err != nil { + outputFile.Close() + os.Remove(outputPath) + return err + } + outputFile.Close() + return nil +} diff --git a/cmd/sing-box/cmd_rule_set_format.go b/cmd/sing-box/cmd_rule_set_format.go new file mode 100644 index 0000000000..6276204c46 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_format.go @@ -0,0 +1,83 @@ +package main + +import ( + "bytes" + "io" + "os" + "path/filepath" + + "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 commandRuleSetFormatFlagWrite bool + +var commandRuleSetFormat = &cobra.Command{ + Use: "format ", + Short: "Format rule-set json", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := formatRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") + commandRuleSet.AddCommand(commandRuleSetFormat) +} + +func formatRuleSet(sourcePath string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return err + } + } + content, err := io.ReadAll(reader) + if err != nil { + return err + } + plainRuleSet, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return err + } + 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 !commandRuleSetFormatFlagWrite || sourcePath == "stdin" { + os.Stdout.WriteString(buffer.String() + "\n") + return nil + } + if bytes.Equal(content, buffer.Bytes()) { + return nil + } + output, err := os.Create(sourcePath) + if err != nil { + return E.Cause(err, "open output") + } + _, err = output.Write(buffer.Bytes()) + output.Close() + if err != nil { + return E.Cause(err, "write output") + } + os.Stderr.WriteString(outputPath + "\n") + return nil +} diff --git a/cmd/sing-box/cmd_run.go b/cmd/sing-box/cmd_run.go index 46f3495fc9..bde811f78a 100644 --- a/cmd/sing-box/cmd_run.go +++ b/cmd/sing-box/cmd_run.go @@ -13,10 +13,12 @@ import ( "time" "github.com/sagernet/sing-box" - "github.com/sagernet/sing-box/common/badjsonmerge" + 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/sagernet/sing/common/json/badjson" "github.com/spf13/cobra" ) @@ -55,8 +57,7 @@ func readConfigAt(path string) (*OptionsEntry, error) { if err != nil { return nil, E.Cause(err, "read config at ", path) } - var options option.Options - err = options.UnmarshalJSON(configContent) + options, err := json.UnmarshalExtended[option.Options](configContent) if err != nil { return nil, E.Cause(err, "decode config at ", path) } @@ -106,13 +107,18 @@ func readConfigAndMerge() (option.Options, error) { if len(optionsList) == 1 { return optionsList[0].options, nil } - var mergedOptions option.Options + var mergedMessage json.RawMessage for _, options := range optionsList { - mergedOptions, err = badjsonmerge.MergeOptions(options.options, mergedOptions) + mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage) if err != nil { return option.Options{}, E.Cause(err, "merge config at ", options.path) } } + var mergedOptions option.Options + err = mergedOptions.UnmarshalJSON(mergedMessage) + if err != nil { + return option.Options{}, E.Cause(err, "unmarshal merged config") + } return mergedOptions, nil } @@ -127,7 +133,7 @@ func create() (*box.Box, context.CancelFunc, error) { } options.Log.DisableColor = true } - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(globalCtx) instance, err := box.New(box.Options{ Context: ctx, Options: options, @@ -193,7 +199,7 @@ func run() error { } func closeMonitor(ctx context.Context) { - time.Sleep(3 * time.Second) + time.Sleep(C.DefaultStopFatalTimeout) select { case <-ctx.Done(): return diff --git a/cmd/sing-box/cmd_tools.go b/cmd/sing-box/cmd_tools.go index 460a50cd1c..c45f585576 100644 --- a/cmd/sing-box/cmd_tools.go +++ b/cmd/sing-box/cmd_tools.go @@ -38,11 +38,7 @@ func createPreStartedClient() (*box.Box, error) { func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) { if outboundTag == "" { - outbound := instance.Router().DefaultOutbound(N.NetworkName(network)) - if outbound == nil { - return nil, E.New("missing default outbound") - } - return outbound, nil + return instance.Router().DefaultOutbound(N.NetworkName(network)) } else { outbound, loaded := instance.Router().Outbound(outboundTag) if !loaded { diff --git a/cmd/sing-box/cmd_tools_connect.go b/cmd/sing-box/cmd_tools_connect.go index b904ebc9f5..3ea04bcd40 100644 --- a/cmd/sing-box/cmd_tools_connect.go +++ b/cmd/sing-box/cmd_tools_connect.go @@ -18,7 +18,7 @@ import ( var commandConnectFlagNetwork string var commandConnect = &cobra.Command{ - Use: "connect [address]", + Use: "connect
", Short: "Connect to an address", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { diff --git a/cmd/sing-box/main.go b/cmd/sing-box/main.go index f7974d2cc6..66b7daa1d9 100644 --- a/cmd/sing-box/main.go +++ b/cmd/sing-box/main.go @@ -1,16 +1,21 @@ package main import ( + "context" "os" + "os/user" + "strconv" "time" _ "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/service/filemanager" "github.com/spf13/cobra" ) var ( + globalCtx context.Context configPaths []string configDirectories []string workingDir string @@ -36,15 +41,30 @@ func main() { } func preRun(cmd *cobra.Command, args []string) { + globalCtx = context.Background() + sudoUser := os.Getenv("SUDO_USER") + sudoUID, _ := strconv.Atoi(os.Getenv("SUDO_UID")) + sudoGID, _ := strconv.Atoi(os.Getenv("SUDO_GID")) + if sudoUID == 0 && sudoGID == 0 && sudoUser != "" { + sudoUserObject, _ := user.Lookup(sudoUser) + if sudoUserObject != nil { + sudoUID, _ = strconv.Atoi(sudoUserObject.Uid) + sudoGID, _ = strconv.Atoi(sudoUserObject.Gid) + } + } + if sudoUID > 0 && sudoGID > 0 { + globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID) + } if disableColor { - log.SetStdLogger(log.NewFactory(log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, nil).Logger()) + log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger()) } if workingDir != "" { _, err := os.Stat(workingDir) if err != nil { - os.MkdirAll(workingDir, 0o777) + filemanager.MkdirAll(globalCtx, workingDir, 0o777) } - if err := os.Chdir(workingDir); err != nil { + err = os.Chdir(workingDir) + if err != nil { log.Fatal(err) } } diff --git a/common/badjson/array.go b/common/badjson/array.go deleted file mode 100644 index 2a731687dd..0000000000 --- a/common/badjson/array.go +++ /dev/null @@ -1,46 +0,0 @@ -package badjson - -import ( - "bytes" - - "github.com/sagernet/sing-box/common/json" - E "github.com/sagernet/sing/common/exceptions" -) - -type JSONArray []any - -func (a JSONArray) MarshalJSON() ([]byte, error) { - return json.Marshal([]any(a)) -} - -func (a *JSONArray) UnmarshalJSON(content []byte) error { - decoder := json.NewDecoder(bytes.NewReader(content)) - arrayStart, err := decoder.Token() - if err != nil { - return err - } else if arrayStart != json.Delim('[') { - return E.New("excepted array start, but got ", arrayStart) - } - err = a.decodeJSON(decoder) - if err != nil { - return err - } - arrayEnd, err := decoder.Token() - if err != nil { - return err - } else if arrayEnd != json.Delim(']') { - return E.New("excepted array end, but got ", arrayEnd) - } - return nil -} - -func (a *JSONArray) decodeJSON(decoder *json.Decoder) error { - for decoder.More() { - item, err := decodeJSON(decoder) - if err != nil { - return err - } - *a = append(*a, item) - } - return nil -} diff --git a/common/badjson/json.go b/common/badjson/json.go deleted file mode 100644 index e2185b8669..0000000000 --- a/common/badjson/json.go +++ /dev/null @@ -1,54 +0,0 @@ -package badjson - -import ( - "bytes" - - "github.com/sagernet/sing-box/common/json" - E "github.com/sagernet/sing/common/exceptions" -) - -func Decode(content []byte) (any, error) { - decoder := json.NewDecoder(bytes.NewReader(content)) - return decodeJSON(decoder) -} - -func decodeJSON(decoder *json.Decoder) (any, error) { - rawToken, err := decoder.Token() - if err != nil { - return nil, err - } - switch token := rawToken.(type) { - case json.Delim: - switch token { - case '{': - var object JSONObject - err = object.decodeJSON(decoder) - if err != nil { - return nil, err - } - rawToken, err = decoder.Token() - if err != nil { - return nil, err - } else if rawToken != json.Delim('}') { - return nil, E.New("excepted object end, but got ", rawToken) - } - return &object, nil - case '[': - var array JSONArray - err = array.decodeJSON(decoder) - if err != nil { - return nil, err - } - rawToken, err = decoder.Token() - if err != nil { - return nil, err - } else if rawToken != json.Delim(']') { - return nil, E.New("excepted array end, but got ", rawToken) - } - return array, nil - default: - return nil, E.New("excepted object or array end: ", token) - } - } - return rawToken, nil -} diff --git a/common/badjson/object.go b/common/badjson/object.go deleted file mode 100644 index d9c2a36ec0..0000000000 --- a/common/badjson/object.go +++ /dev/null @@ -1,79 +0,0 @@ -package badjson - -import ( - "bytes" - "strings" - - "github.com/sagernet/sing-box/common/json" - E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/x/linkedhashmap" -) - -type JSONObject struct { - linkedhashmap.Map[string, any] -} - -func (m JSONObject) MarshalJSON() ([]byte, error) { - buffer := new(bytes.Buffer) - buffer.WriteString("{") - items := m.Entries() - iLen := len(items) - for i, entry := range items { - keyContent, err := json.Marshal(entry.Key) - if err != nil { - return nil, err - } - buffer.WriteString(strings.TrimSpace(string(keyContent))) - buffer.WriteString(": ") - valueContent, err := json.Marshal(entry.Value) - if err != nil { - return nil, err - } - buffer.WriteString(strings.TrimSpace(string(valueContent))) - if i < iLen-1 { - buffer.WriteString(", ") - } - } - buffer.WriteString("}") - return buffer.Bytes(), nil -} - -func (m *JSONObject) UnmarshalJSON(content []byte) error { - decoder := json.NewDecoder(bytes.NewReader(content)) - m.Clear() - objectStart, err := decoder.Token() - if err != nil { - return err - } else if objectStart != json.Delim('{') { - return E.New("expected json object start, but starts with ", objectStart) - } - err = m.decodeJSON(decoder) - if err != nil { - return E.Cause(err, "decode json object content") - } - objectEnd, err := decoder.Token() - if err != nil { - return err - } else if objectEnd != json.Delim('}') { - return E.New("expected json object end, but ends with ", objectEnd) - } - return nil -} - -func (m *JSONObject) decodeJSON(decoder *json.Decoder) error { - for decoder.More() { - var entryKey string - keyToken, err := decoder.Token() - if err != nil { - return err - } - entryKey = keyToken.(string) - var entryValue any - entryValue, err = decodeJSON(decoder) - if err != nil { - return E.Cause(err, "decode value for ", entryKey) - } - m.Put(entryKey, entryValue) - } - return nil -} diff --git a/common/badjsonmerge/merge.go b/common/badjsonmerge/merge.go deleted file mode 100644 index 39635e6632..0000000000 --- a/common/badjsonmerge/merge.go +++ /dev/null @@ -1,80 +0,0 @@ -package badjsonmerge - -import ( - "encoding/json" - "reflect" - - "github.com/sagernet/sing-box/common/badjson" - "github.com/sagernet/sing-box/option" - E "github.com/sagernet/sing/common/exceptions" -) - -func MergeOptions(source option.Options, destination option.Options) (option.Options, error) { - rawSource, err := json.Marshal(source) - if err != nil { - return option.Options{}, E.Cause(err, "marshal source") - } - rawDestination, err := json.Marshal(destination) - if err != nil { - return option.Options{}, E.Cause(err, "marshal destination") - } - rawMerged, err := MergeJSON(rawSource, rawDestination) - if err != nil { - return option.Options{}, E.Cause(err, "merge options") - } - var merged option.Options - err = json.Unmarshal(rawMerged, &merged) - if err != nil { - return option.Options{}, E.Cause(err, "unmarshal merged options") - } - return merged, nil -} - -func MergeJSON(rawSource json.RawMessage, rawDestination json.RawMessage) (json.RawMessage, error) { - source, err := badjson.Decode(rawSource) - if err != nil { - return nil, E.Cause(err, "decode source") - } - destination, err := badjson.Decode(rawDestination) - if err != nil { - return nil, E.Cause(err, "decode destination") - } - merged, err := mergeJSON(source, destination) - if err != nil { - return nil, err - } - return json.Marshal(merged) -} - -func mergeJSON(anySource any, anyDestination any) (any, error) { - switch destination := anyDestination.(type) { - case badjson.JSONArray: - switch source := anySource.(type) { - case badjson.JSONArray: - destination = append(destination, source...) - default: - destination = append(destination, source) - } - return destination, nil - case *badjson.JSONObject: - switch source := anySource.(type) { - case *badjson.JSONObject: - for _, entry := range source.Entries() { - oldValue, loaded := destination.Get(entry.Key) - if loaded { - var err error - entry.Value, err = mergeJSON(entry.Value, oldValue) - if err != nil { - return nil, E.Cause(err, "merge object item ", entry.Key) - } - } - destination.Put(entry.Key, entry.Value) - } - default: - return nil, E.New("cannot merge json object into ", reflect.TypeOf(destination)) - } - return destination, nil - default: - return destination, nil - } -} diff --git a/common/badjsonmerge/merge_test.go b/common/badjsonmerge/merge_test.go deleted file mode 100644 index be4481b5fa..0000000000 --- a/common/badjsonmerge/merge_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package badjsonmerge - -import ( - "testing" - - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/option" - N "github.com/sagernet/sing/common/network" - - "github.com/stretchr/testify/require" -) - -func TestMergeJSON(t *testing.T) { - t.Parallel() - options := option.Options{ - Log: &option.LogOptions{ - Level: "info", - }, - Route: &option.RouteOptions{ - Rules: []option.Rule{ - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultRule{ - Network: []string{N.NetworkTCP}, - Outbound: "direct", - }, - }, - }, - }, - } - anotherOptions := option.Options{ - Outbounds: []option.Outbound{ - { - Type: C.TypeDirect, - Tag: "direct", - }, - }, - } - thirdOptions := option.Options{ - Route: &option.RouteOptions{ - Rules: []option.Rule{ - { - Type: C.RuleTypeDefault, - DefaultOptions: option.DefaultRule{ - Network: []string{N.NetworkUDP}, - Outbound: "direct", - }, - }, - }, - }, - } - mergeOptions, err := MergeOptions(options, anotherOptions) - require.NoError(t, err) - mergeOptions, err = MergeOptions(thirdOptions, mergeOptions) - require.NoError(t, err) - require.Equal(t, "info", mergeOptions.Log.Level) - require.Equal(t, 2, len(mergeOptions.Route.Rules)) - require.Equal(t, C.TypeDirect, mergeOptions.Outbounds[0].Type) -} diff --git a/common/badtls/badtls.go b/common/badtls/badtls.go deleted file mode 100644 index c5c55e3c6a..0000000000 --- a/common/badtls/badtls.go +++ /dev/null @@ -1,233 +0,0 @@ -//go:build go1.20 && !go1.21 - -package badtls - -import ( - "crypto/cipher" - "crypto/rand" - "crypto/tls" - "encoding/binary" - "io" - "net" - "reflect" - "sync" - "sync/atomic" - "unsafe" - - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - E "github.com/sagernet/sing/common/exceptions" - N "github.com/sagernet/sing/common/network" - aTLS "github.com/sagernet/sing/common/tls" -) - -type Conn struct { - *tls.Conn - writer N.ExtendedWriter - isHandshakeComplete *atomic.Bool - activeCall *atomic.Int32 - closeNotifySent *bool - version *uint16 - rand io.Reader - halfAccess *sync.Mutex - halfError *error - cipher cipher.AEAD - explicitNonceLen int - halfPtr uintptr - halfSeq []byte - halfScratchBuf []byte -} - -func TryCreate(conn aTLS.Conn) aTLS.Conn { - tlsConn, ok := conn.(*tls.Conn) - if !ok { - return conn - } - badConn, err := Create(tlsConn) - if err != nil { - log.Warn("initialize badtls: ", err) - return conn - } - return badConn -} - -func Create(conn *tls.Conn) (aTLS.Conn, error) { - rawConn := reflect.Indirect(reflect.ValueOf(conn)) - rawIsHandshakeComplete := rawConn.FieldByName("isHandshakeComplete") - if !rawIsHandshakeComplete.IsValid() || rawIsHandshakeComplete.Kind() != reflect.Struct { - return nil, E.New("badtls: invalid isHandshakeComplete") - } - isHandshakeComplete := (*atomic.Bool)(unsafe.Pointer(rawIsHandshakeComplete.UnsafeAddr())) - if !isHandshakeComplete.Load() { - return nil, E.New("handshake not finished") - } - rawActiveCall := rawConn.FieldByName("activeCall") - if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Struct { - return nil, E.New("badtls: invalid active call") - } - activeCall := (*atomic.Int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr())) - rawHalfConn := rawConn.FieldByName("out") - if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct { - return nil, E.New("badtls: invalid half conn") - } - rawVersion := rawConn.FieldByName("vers") - if !rawVersion.IsValid() || rawVersion.Kind() != reflect.Uint16 { - return nil, E.New("badtls: invalid version") - } - version := (*uint16)(unsafe.Pointer(rawVersion.UnsafeAddr())) - rawCloseNotifySent := rawConn.FieldByName("closeNotifySent") - if !rawCloseNotifySent.IsValid() || rawCloseNotifySent.Kind() != reflect.Bool { - return nil, E.New("badtls: invalid notify") - } - closeNotifySent := (*bool)(unsafe.Pointer(rawCloseNotifySent.UnsafeAddr())) - rawConfig := reflect.Indirect(rawConn.FieldByName("config")) - if !rawConfig.IsValid() || rawConfig.Kind() != reflect.Struct { - return nil, E.New("badtls: bad config") - } - config := (*tls.Config)(unsafe.Pointer(rawConfig.UnsafeAddr())) - randReader := config.Rand - if randReader == nil { - randReader = rand.Reader - } - rawHalfMutex := rawHalfConn.FieldByName("Mutex") - if !rawHalfMutex.IsValid() || rawHalfMutex.Kind() != reflect.Struct { - return nil, E.New("badtls: invalid half mutex") - } - halfAccess := (*sync.Mutex)(unsafe.Pointer(rawHalfMutex.UnsafeAddr())) - rawHalfError := rawHalfConn.FieldByName("err") - if !rawHalfError.IsValid() || rawHalfError.Kind() != reflect.Interface { - return nil, E.New("badtls: invalid half error") - } - halfError := (*error)(unsafe.Pointer(rawHalfError.UnsafeAddr())) - rawHalfCipherInterface := rawHalfConn.FieldByName("cipher") - if !rawHalfCipherInterface.IsValid() || rawHalfCipherInterface.Kind() != reflect.Interface { - return nil, E.New("badtls: invalid cipher interface") - } - rawHalfCipher := rawHalfCipherInterface.Elem() - aeadCipher, loaded := valueInterface(rawHalfCipher, false).(cipher.AEAD) - if !loaded { - return nil, E.New("badtls: invalid AEAD cipher") - } - var explicitNonceLen int - switch cipherName := reflect.Indirect(rawHalfCipher).Type().String(); cipherName { - case "tls.prefixNonceAEAD": - explicitNonceLen = aeadCipher.NonceSize() - case "tls.xorNonceAEAD": - default: - return nil, E.New("badtls: unknown cipher type: ", cipherName) - } - rawHalfSeq := rawHalfConn.FieldByName("seq") - if !rawHalfSeq.IsValid() || rawHalfSeq.Kind() != reflect.Array { - return nil, E.New("badtls: invalid seq") - } - halfSeq := rawHalfSeq.Bytes() - rawHalfScratchBuf := rawHalfConn.FieldByName("scratchBuf") - if !rawHalfScratchBuf.IsValid() || rawHalfScratchBuf.Kind() != reflect.Array { - return nil, E.New("badtls: invalid scratchBuf") - } - halfScratchBuf := rawHalfScratchBuf.Bytes() - return &Conn{ - Conn: conn, - writer: bufio.NewExtendedWriter(conn.NetConn()), - isHandshakeComplete: isHandshakeComplete, - activeCall: activeCall, - closeNotifySent: closeNotifySent, - version: version, - halfAccess: halfAccess, - halfError: halfError, - cipher: aeadCipher, - explicitNonceLen: explicitNonceLen, - rand: randReader, - halfPtr: rawHalfConn.UnsafeAddr(), - halfSeq: halfSeq, - halfScratchBuf: halfScratchBuf, - }, nil -} - -func (c *Conn) WriteBuffer(buffer *buf.Buffer) error { - if buffer.Len() > maxPlaintext { - defer buffer.Release() - return common.Error(c.Write(buffer.Bytes())) - } - for { - x := c.activeCall.Load() - if x&1 != 0 { - return net.ErrClosed - } - if c.activeCall.CompareAndSwap(x, x+2) { - break - } - } - defer c.activeCall.Add(-2) - c.halfAccess.Lock() - defer c.halfAccess.Unlock() - if err := *c.halfError; err != nil { - return err - } - if *c.closeNotifySent { - return errShutdown - } - dataLen := buffer.Len() - dataBytes := buffer.Bytes() - outBuf := buffer.ExtendHeader(recordHeaderLen + c.explicitNonceLen) - outBuf[0] = 23 - version := *c.version - if version == 0 { - version = tls.VersionTLS10 - } else if version == tls.VersionTLS13 { - version = tls.VersionTLS12 - } - binary.BigEndian.PutUint16(outBuf[1:], version) - var nonce []byte - if c.explicitNonceLen > 0 { - nonce = outBuf[5 : 5+c.explicitNonceLen] - if c.explicitNonceLen < 16 { - copy(nonce, c.halfSeq) - } else { - if _, err := io.ReadFull(c.rand, nonce); err != nil { - return err - } - } - } - if len(nonce) == 0 { - nonce = c.halfSeq - } - if *c.version == tls.VersionTLS13 { - buffer.FreeBytes()[0] = 23 - binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+1+c.cipher.Overhead())) - c.cipher.Seal(outBuf, nonce, outBuf[recordHeaderLen:recordHeaderLen+c.explicitNonceLen+dataLen+1], outBuf[:recordHeaderLen]) - buffer.Extend(1 + c.cipher.Overhead()) - } else { - binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen)) - additionalData := append(c.halfScratchBuf[:0], c.halfSeq...) - additionalData = append(additionalData, outBuf[:recordHeaderLen]...) - c.cipher.Seal(outBuf, nonce, dataBytes, additionalData) - buffer.Extend(c.cipher.Overhead()) - binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+c.explicitNonceLen+c.cipher.Overhead())) - } - incSeq(c.halfPtr) - log.Trace("badtls write ", buffer.Len()) - return c.writer.WriteBuffer(buffer) -} - -func (c *Conn) FrontHeadroom() int { - return recordHeaderLen + c.explicitNonceLen -} - -func (c *Conn) RearHeadroom() int { - return 1 + c.cipher.Overhead() -} - -func (c *Conn) WriterMTU() int { - return maxPlaintext -} - -func (c *Conn) Upstream() any { - return c.Conn -} - -func (c *Conn) UpstreamWriter() any { - return c.NetConn() -} diff --git a/common/badtls/badtls_stub.go b/common/badtls/badtls_stub.go deleted file mode 100644 index 2f0028f6e9..0000000000 --- a/common/badtls/badtls_stub.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !go1.19 || go1.21 - -package badtls - -import ( - "crypto/tls" - "os" - - aTLS "github.com/sagernet/sing/common/tls" -) - -func Create(conn *tls.Conn) (aTLS.Conn, error) { - return nil, os.ErrInvalid -} diff --git a/common/badtls/link.go b/common/badtls/link.go deleted file mode 100644 index b8d5f4bd1e..0000000000 --- a/common/badtls/link.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build go1.20 && !go.1.21 - -package badtls - -import ( - "reflect" - _ "unsafe" -) - -const ( - maxPlaintext = 16384 // maximum plaintext payload length - recordHeaderLen = 5 // record header length -) - -//go:linkname errShutdown crypto/tls.errShutdown -var errShutdown error - -//go:linkname incSeq crypto/tls.(*halfConn).incSeq -func incSeq(conn uintptr) - -//go:linkname valueInterface reflect.valueInterface -func valueInterface(v reflect.Value, safe bool) any diff --git a/common/badtls/read_wait.go b/common/badtls/read_wait.go new file mode 100644 index 0000000000..fdae8a1c35 --- /dev/null +++ b/common/badtls/read_wait.go @@ -0,0 +1,119 @@ +//go:build go1.21 && !without_badtls + +package badtls + +import ( + "bytes" + "os" + "reflect" + "sync" + "unsafe" + + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/tls" +) + +var _ N.ReadWaiter = (*ReadWaitConn)(nil) + +type ReadWaitConn struct { + *tls.STDConn + halfAccess *sync.Mutex + rawInput *bytes.Buffer + input *bytes.Reader + hand *bytes.Buffer + readWaitOptions N.ReadWaitOptions +} + +func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) { + stdConn, isSTDConn := conn.(*tls.STDConn) + if !isSTDConn { + return nil, os.ErrInvalid + } + rawConn := reflect.Indirect(reflect.ValueOf(stdConn)) + rawHalfConn := rawConn.FieldByName("in") + if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct { + return nil, E.New("badtls: invalid half conn") + } + rawHalfMutex := rawHalfConn.FieldByName("Mutex") + if !rawHalfMutex.IsValid() || rawHalfMutex.Kind() != reflect.Struct { + return nil, E.New("badtls: invalid half mutex") + } + halfAccess := (*sync.Mutex)(unsafe.Pointer(rawHalfMutex.UnsafeAddr())) + rawRawInput := rawConn.FieldByName("rawInput") + if !rawRawInput.IsValid() || rawRawInput.Kind() != reflect.Struct { + return nil, E.New("badtls: invalid raw input") + } + rawInput := (*bytes.Buffer)(unsafe.Pointer(rawRawInput.UnsafeAddr())) + rawInput0 := rawConn.FieldByName("input") + if !rawInput0.IsValid() || rawInput0.Kind() != reflect.Struct { + return nil, E.New("badtls: invalid input") + } + input := (*bytes.Reader)(unsafe.Pointer(rawInput0.UnsafeAddr())) + rawHand := rawConn.FieldByName("hand") + if !rawHand.IsValid() || rawHand.Kind() != reflect.Struct { + return nil, E.New("badtls: invalid hand") + } + hand := (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr())) + return &ReadWaitConn{ + STDConn: stdConn, + halfAccess: halfAccess, + rawInput: rawInput, + input: input, + hand: hand, + }, nil +} + +func (c *ReadWaitConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readWaitOptions = options + return false +} + +func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) { + err = c.Handshake() + if err != nil { + return + } + c.halfAccess.Lock() + defer c.halfAccess.Unlock() + for c.input.Len() == 0 { + err = tlsReadRecord(c.STDConn) + if err != nil { + return + } + for c.hand.Len() > 0 { + err = tlsHandlePostHandshakeMessage(c.STDConn) + if err != nil { + return + } + } + } + buffer = c.readWaitOptions.NewBuffer() + n, err := c.input.Read(buffer.FreeBytes()) + if err != nil { + buffer.Release() + return + } + buffer.Truncate(n) + + if n != 0 && c.input.Len() == 0 && c.rawInput.Len() > 0 && + // recordType(c.rawInput.Bytes()[0]) == recordTypeAlert { + c.rawInput.Bytes()[0] == 21 { + _ = tlsReadRecord(c.STDConn) + // return n, err // will be io.EOF on closeNotify + } + + c.readWaitOptions.PostReturn(buffer) + return +} + +func (c *ReadWaitConn) Upstream() any { + return c.STDConn +} + +//go:linkname tlsReadRecord crypto/tls.(*Conn).readRecord +func tlsReadRecord(c *tls.STDConn) error + +//go:linkname tlsHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage +func tlsHandlePostHandshakeMessage(c *tls.STDConn) error diff --git a/common/badtls/read_wait_stub.go b/common/badtls/read_wait_stub.go new file mode 100644 index 0000000000..c5c9946f8b --- /dev/null +++ b/common/badtls/read_wait_stub.go @@ -0,0 +1,13 @@ +//go:build !go1.21 || without_badtls + +package badtls + +import ( + "os" + + "github.com/sagernet/sing/common/tls" +) + +func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) { + return nil, os.ErrInvalid +} diff --git a/common/badversion/version_json.go b/common/badversion/version_json.go index 0647b2bfe0..7ec19663a2 100644 --- a/common/badversion/version_json.go +++ b/common/badversion/version_json.go @@ -1,6 +1,6 @@ package badversion -import "github.com/sagernet/sing-box/common/json" +import "github.com/sagernet/sing/common/json" func (v Version) MarshalJSON() ([]byte, error) { return json.Marshal(v.String()) diff --git a/common/dialer/default.go b/common/dialer/default.go index 6dd9738495..1d51d8827e 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -15,14 +15,17 @@ import ( N "github.com/sagernet/sing/common/network" ) +var _ WireGuardListener = (*DefaultDialer)(nil) + type DefaultDialer struct { - dialer4 tcpDialer - dialer6 tcpDialer - udpDialer4 net.Dialer - udpDialer6 net.Dialer - udpListener net.ListenConfig - udpAddr4 string - udpAddr6 string + dialer4 tcpDialer + dialer6 tcpDialer + udpDialer4 net.Dialer + udpDialer6 net.Dialer + udpListener net.ListenConfig + udpAddr4 string + udpAddr6 string + isWireGuardListener bool } func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDialer, error) { @@ -99,7 +102,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi setMultiPathTCP(&dialer4) } - var tlsFragment *TLSFragment=nil + var tlsFragment *TLSFragment = nil if options.TLSFragment != nil && options.TLSFragment.Enabled { tlsFragment = &TLSFragment{} if options.TCPFastOpen { @@ -122,6 +125,11 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi tlsFragment.SizeMax = size[1] } + if options.IsWireGuardListener { + for _, controlFn := range wgControlFns { + listener.Control = control.Append(listener.Control, controlFn) + } + } tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen, tlsFragment) if err != nil { return nil, err @@ -138,6 +146,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi listener, udpAddr4, udpAddr6, + options.IsWireGuardListener, }, nil } @@ -170,6 +179,10 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd } } +func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) { + return trackPacketConn(d.udpListener.ListenPacket(context.Background(), network, address)) +} + func trackConn(conn net.Conn, err error) (net.Conn, error) { if !conntrack.Enabled || err != nil { return conn, err diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index b059e38e34..bbb4b3a92e 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -6,15 +6,13 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-dns" - "github.com/sagernet/sing/common" N "github.com/sagernet/sing/common/network" ) -func MustNew(router adapter.Router, options option.DialerOptions) N.Dialer { - return common.Must1(New(router, options)) -} - func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) { + if options.IsWireGuardListener { + return NewDefault(router, options) + } var ( dialer N.Dialer err error diff --git a/common/dialer/router.go b/common/dialer/router.go index 1d5586546e..2531607753 100644 --- a/common/dialer/router.go +++ b/common/dialer/router.go @@ -18,11 +18,19 @@ func NewRouter(router adapter.Router) N.Dialer { } func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - return d.router.DefaultOutbound(network).DialContext(ctx, network, destination) + dialer, err := d.router.DefaultOutbound(network) + if err != nil { + return nil, err + } + return dialer.DialContext(ctx, network, destination) } func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return d.router.DefaultOutbound(N.NetworkUDP).ListenPacket(ctx, destination) + dialer, err := d.router.DefaultOutbound(N.NetworkUDP) + if err != nil { + return nil, err + } + return dialer.ListenPacket(ctx, destination) } func (d *RouterDialer) Upstream() any { diff --git a/common/dialer/wireguard.go b/common/dialer/wireguard.go new file mode 100644 index 0000000000..195133c650 --- /dev/null +++ b/common/dialer/wireguard.go @@ -0,0 +1,9 @@ +package dialer + +import ( + "net" +) + +type WireGuardListener interface { + ListenPacketCompat(network, address string) (net.PacketConn, error) +} diff --git a/common/dialer/wireguard_control.go b/common/dialer/wireguard_control.go new file mode 100644 index 0000000000..def86411a5 --- /dev/null +++ b/common/dialer/wireguard_control.go @@ -0,0 +1,11 @@ +//go:build with_wireguard + +package dialer + +import ( + "github.com/sagernet/wireguard-go/conn" +) + +var _ WireGuardListener = (conn.Listener)(nil) + +var wgControlFns = conn.ControlFns diff --git a/common/dialer/wiregurad_stub.go b/common/dialer/wiregurad_stub.go new file mode 100644 index 0000000000..d30c223a95 --- /dev/null +++ b/common/dialer/wiregurad_stub.go @@ -0,0 +1,9 @@ +//go:build !with_wireguard + +package dialer + +import ( + "github.com/sagernet/sing/common/control" +) + +var wgControlFns []control.Func diff --git a/common/json/comment.go b/common/json/comment.go deleted file mode 100644 index 6f3be26233..0000000000 --- a/common/json/comment.go +++ /dev/null @@ -1,128 +0,0 @@ -package json - -import ( - "bufio" - "io" -) - -// kanged from v2ray - -type commentFilterState = byte - -const ( - commentFilterStateContent commentFilterState = iota - commentFilterStateEscape - commentFilterStateDoubleQuote - commentFilterStateDoubleQuoteEscape - commentFilterStateSingleQuote - commentFilterStateSingleQuoteEscape - commentFilterStateComment - commentFilterStateSlash - commentFilterStateMultilineComment - commentFilterStateMultilineCommentStar -) - -type CommentFilter struct { - br *bufio.Reader - state commentFilterState -} - -func NewCommentFilter(reader io.Reader) io.Reader { - return &CommentFilter{br: bufio.NewReader(reader)} -} - -func (v *CommentFilter) Read(b []byte) (int, error) { - p := b[:0] - for len(p) < len(b)-2 { - x, err := v.br.ReadByte() - if err != nil { - if len(p) == 0 { - return 0, err - } - return len(p), nil - } - switch v.state { - case commentFilterStateContent: - switch x { - case '"': - v.state = commentFilterStateDoubleQuote - p = append(p, x) - case '\'': - v.state = commentFilterStateSingleQuote - p = append(p, x) - case '\\': - v.state = commentFilterStateEscape - case '#': - v.state = commentFilterStateComment - case '/': - v.state = commentFilterStateSlash - default: - p = append(p, x) - } - case commentFilterStateEscape: - p = append(p, '\\', x) - v.state = commentFilterStateContent - case commentFilterStateDoubleQuote: - switch x { - case '"': - v.state = commentFilterStateContent - p = append(p, x) - case '\\': - v.state = commentFilterStateDoubleQuoteEscape - default: - p = append(p, x) - } - case commentFilterStateDoubleQuoteEscape: - p = append(p, '\\', x) - v.state = commentFilterStateDoubleQuote - case commentFilterStateSingleQuote: - switch x { - case '\'': - v.state = commentFilterStateContent - p = append(p, x) - case '\\': - v.state = commentFilterStateSingleQuoteEscape - default: - p = append(p, x) - } - case commentFilterStateSingleQuoteEscape: - p = append(p, '\\', x) - v.state = commentFilterStateSingleQuote - case commentFilterStateComment: - if x == '\n' { - v.state = commentFilterStateContent - p = append(p, '\n') - } - case commentFilterStateSlash: - switch x { - case '/': - v.state = commentFilterStateComment - case '*': - v.state = commentFilterStateMultilineComment - default: - p = append(p, '/', x) - } - case commentFilterStateMultilineComment: - switch x { - case '*': - v.state = commentFilterStateMultilineCommentStar - case '\n': - p = append(p, '\n') - } - case commentFilterStateMultilineCommentStar: - switch x { - case '/': - v.state = commentFilterStateContent - case '*': - // Stay - case '\n': - p = append(p, '\n') - default: - v.state = commentFilterStateMultilineComment - } - default: - panic("Unknown state.") - } - } - return len(p), nil -} diff --git a/common/json/std.go b/common/json/std.go deleted file mode 100644 index edc3502bb7..0000000000 --- a/common/json/std.go +++ /dev/null @@ -1,18 +0,0 @@ -package json - -import "encoding/json" - -var ( - Marshal = json.Marshal - Unmarshal = json.Unmarshal - NewEncoder = json.NewEncoder - NewDecoder = json.NewDecoder -) - -type ( - Encoder = json.Encoder - Decoder = json.Decoder - Token = json.Token - Delim = json.Delim - SyntaxError = json.SyntaxError -) diff --git a/common/srs/binary.go b/common/srs/binary.go new file mode 100644 index 0000000000..faf4cd17b2 --- /dev/null +++ b/common/srs/binary.go @@ -0,0 +1,487 @@ +package srs + +import ( + "compress/zlib" + "encoding/binary" + "io" + "net/netip" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/rw" + + "go4.org/netipx" +) + +var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS + +const ( + ruleItemQueryType uint8 = iota + ruleItemNetwork + ruleItemDomain + ruleItemDomainKeyword + ruleItemDomainRegex + ruleItemSourceIPCIDR + ruleItemIPCIDR + ruleItemSourcePort + ruleItemSourcePortRange + ruleItemPort + ruleItemPortRange + ruleItemProcessName + ruleItemProcessPath + ruleItemPackageName + ruleItemWIFISSID + ruleItemWIFIBSSID + ruleItemFinal uint8 = 0xFF +) + +func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) { + var magicBytes [3]byte + _, err = io.ReadFull(reader, magicBytes[:]) + if err != nil { + return + } + if magicBytes != MagicBytes { + err = E.New("invalid sing-box rule set file") + return + } + var version uint8 + err = binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return ruleSet, err + } + if version != 1 { + return ruleSet, E.New("unsupported version: ", version) + } + zReader, err := zlib.NewReader(reader) + if err != nil { + return + } + length, err := rw.ReadUVariant(zReader) + if err != nil { + return + } + ruleSet.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + ruleSet.Rules[i], err = readRule(zReader, recovery) + if err != nil { + err = E.Cause(err, "read rule[", i, "]") + return + } + } + return +} + +func Write(writer io.Writer, ruleSet option.PlainRuleSet) error { + _, err := writer.Write(MagicBytes[:]) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) + if err != nil { + return err + } + err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules))) + if err != nil { + return err + } + for _, rule := range ruleSet.Rules { + err = writeRule(zWriter, rule) + if err != nil { + return err + } + } + return zWriter.Close() +} + +func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) { + var ruleType uint8 + err = binary.Read(reader, binary.BigEndian, &ruleType) + if err != nil { + return + } + switch ruleType { + case 0: + rule.Type = C.RuleTypeDefault + rule.DefaultOptions, err = readDefaultRule(reader, recovery) + case 1: + rule.Type = C.RuleTypeLogical + rule.LogicalOptions, err = readLogicalRule(reader, recovery) + default: + err = E.New("unknown rule type: ", ruleType) + } + return +} + +func writeRule(writer io.Writer, rule option.HeadlessRule) error { + switch rule.Type { + case C.RuleTypeDefault: + return writeDefaultRule(writer, rule.DefaultOptions) + case C.RuleTypeLogical: + return writeLogicalRule(writer, rule.LogicalOptions) + default: + panic("unknown rule type: " + rule.Type) + } +} + +func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) { + var lastItemType uint8 + for { + var itemType uint8 + err = binary.Read(reader, binary.BigEndian, &itemType) + if err != nil { + return + } + switch itemType { + case ruleItemQueryType: + var rawQueryType []uint16 + rawQueryType, err = readRuleItemUint16(reader) + if err != nil { + return + } + rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType { + return option.DNSQueryType(it) + }) + case ruleItemNetwork: + rule.Network, err = readRuleItemString(reader) + case ruleItemDomain: + var matcher *domain.Matcher + matcher, err = domain.ReadMatcher(reader) + if err != nil { + return + } + rule.DomainMatcher = matcher + case ruleItemDomainKeyword: + rule.DomainKeyword, err = readRuleItemString(reader) + case ruleItemDomainRegex: + rule.DomainRegex, err = readRuleItemString(reader) + case ruleItemSourceIPCIDR: + rule.SourceIPSet, err = readIPSet(reader) + if err != nil { + return + } + if recovery { + rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemIPCIDR: + rule.IPSet, err = readIPSet(reader) + if err != nil { + return + } + if recovery { + rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String) + } + case ruleItemSourcePort: + rule.SourcePort, err = readRuleItemUint16(reader) + case ruleItemSourcePortRange: + rule.SourcePortRange, err = readRuleItemString(reader) + case ruleItemPort: + rule.Port, err = readRuleItemUint16(reader) + case ruleItemPortRange: + rule.PortRange, err = readRuleItemString(reader) + case ruleItemProcessName: + rule.ProcessName, err = readRuleItemString(reader) + case ruleItemProcessPath: + rule.ProcessPath, err = readRuleItemString(reader) + case ruleItemPackageName: + rule.PackageName, err = readRuleItemString(reader) + case ruleItemWIFISSID: + rule.WIFISSID, err = readRuleItemString(reader) + case ruleItemWIFIBSSID: + rule.WIFIBSSID, err = readRuleItemString(reader) + case ruleItemFinal: + err = binary.Read(reader, binary.BigEndian, &rule.Invert) + return + default: + err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType) + } + if err != nil { + return + } + lastItemType = itemType + } +} + +func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error { + err := binary.Write(writer, binary.BigEndian, uint8(0)) + if err != nil { + return err + } + if len(rule.QueryType) > 0 { + err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 { + return uint16(it) + })) + if err != nil { + return err + } + } + if len(rule.Network) > 0 { + err = writeRuleItemString(writer, ruleItemNetwork, rule.Network) + if err != nil { + return err + } + } + if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 { + err = binary.Write(writer, binary.BigEndian, ruleItemDomain) + if err != nil { + return err + } + err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer) + if err != nil { + return err + } + } + if len(rule.DomainKeyword) > 0 { + err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword) + if err != nil { + return err + } + } + if len(rule.DomainRegex) > 0 { + err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex) + if err != nil { + return err + } + } + if len(rule.SourceIPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR) + if err != nil { + return E.Cause(err, "source_ip_cidr") + } + } + if len(rule.IPCIDR) > 0 { + err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR) + if err != nil { + return E.Cause(err, "ipcidr") + } + } + if len(rule.SourcePort) > 0 { + err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort) + if err != nil { + return err + } + } + if len(rule.SourcePortRange) > 0 { + err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange) + if err != nil { + return err + } + } + if len(rule.Port) > 0 { + err = writeRuleItemUint16(writer, ruleItemPort, rule.Port) + if err != nil { + return err + } + } + if len(rule.PortRange) > 0 { + err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange) + if err != nil { + return err + } + } + if len(rule.ProcessName) > 0 { + err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName) + if err != nil { + return err + } + } + if len(rule.ProcessPath) > 0 { + err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath) + if err != nil { + return err + } + } + if len(rule.PackageName) > 0 { + err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName) + if err != nil { + return err + } + } + if len(rule.WIFISSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) + if err != nil { + return err + } + } + if len(rule.WIFIBSSID) > 0 { + err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, ruleItemFinal) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, rule.Invert) + if err != nil { + return err + } + return nil +} + +func readRuleItemString(reader io.Reader) ([]string, error) { + length, err := rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + value := make([]string, length) + for i := uint64(0); i < length; i++ { + value[i], err = rw.ReadVString(reader) + if err != nil { + return nil, err + } + } + return value, nil +} + +func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error { + err := binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(value))) + if err != nil { + return err + } + for _, item := range value { + err = rw.WriteVString(writer, item) + if err != nil { + return err + } + } + return nil +} + +func readRuleItemUint16(reader io.Reader) ([]uint16, error) { + length, err := rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + value := make([]uint16, length) + for i := uint64(0); i < length; i++ { + err = binary.Read(reader, binary.BigEndian, &value[i]) + if err != nil { + return nil, err + } + } + return value, nil +} + +func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error { + err := binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(value))) + if err != nil { + return err + } + for _, item := range value { + err = binary.Write(writer, binary.BigEndian, item) + if err != nil { + return err + } + } + return nil +} + +func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error { + var builder netipx.IPSetBuilder + for i, prefixString := range value { + prefix, err := netip.ParsePrefix(prefixString) + if err == nil { + builder.AddPrefix(prefix) + continue + } + addr, addrErr := netip.ParseAddr(prefixString) + if addrErr == nil { + builder.Add(addr) + continue + } + return E.Cause(err, "parse [", i, "]") + } + ipSet, err := builder.IPSet() + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, itemType) + if err != nil { + return err + } + return writeIPSet(writer, ipSet) +} + +func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { + var mode uint8 + err = binary.Read(reader, binary.BigEndian, &mode) + if err != nil { + return + } + switch mode { + case 0: + logicalRule.Mode = C.LogicalTypeAnd + case 1: + logicalRule.Mode = C.LogicalTypeOr + default: + err = E.New("unknown logical mode: ", mode) + return + } + length, err := rw.ReadUVariant(reader) + if err != nil { + return + } + logicalRule.Rules = make([]option.HeadlessRule, length) + for i := uint64(0); i < length; i++ { + logicalRule.Rules[i], err = readRule(reader, recovery) + if err != nil { + err = E.Cause(err, "read logical rule [", i, "]") + return + } + } + err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert) + if err != nil { + return + } + return +} + +func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + switch logicalRule.Mode { + case C.LogicalTypeAnd: + err = binary.Write(writer, binary.BigEndian, uint8(0)) + case C.LogicalTypeOr: + err = binary.Write(writer, binary.BigEndian, uint8(1)) + default: + panic("unknown logical mode: " + logicalRule.Mode) + } + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules))) + if err != nil { + return err + } + for _, rule := range logicalRule.Rules { + err = writeRule(writer, rule) + if err != nil { + return err + } + } + err = binary.Write(writer, binary.BigEndian, logicalRule.Invert) + if err != nil { + return err + } + return nil +} diff --git a/common/srs/ip_set.go b/common/srs/ip_set.go new file mode 100644 index 0000000000..b346da26f6 --- /dev/null +++ b/common/srs/ip_set.go @@ -0,0 +1,116 @@ +package srs + +import ( + "encoding/binary" + "io" + "net/netip" + "unsafe" + + "github.com/sagernet/sing/common/rw" + + "go4.org/netipx" +) + +type myIPSet struct { + rr []myIPRange +} + +type myIPRange struct { + from netip.Addr + to netip.Addr +} + +func readIPSet(reader io.Reader) (*netipx.IPSet, error) { + var version uint8 + err := binary.Read(reader, binary.BigEndian, &version) + if err != nil { + return nil, err + } + var length uint64 + err = binary.Read(reader, binary.BigEndian, &length) + if err != nil { + return nil, err + } + mySet := &myIPSet{ + rr: make([]myIPRange, length), + } + for i := uint64(0); i < length; i++ { + var ( + fromLen uint64 + toLen uint64 + fromAddr netip.Addr + toAddr netip.Addr + ) + fromLen, err = rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + fromBytes := make([]byte, fromLen) + _, err = io.ReadFull(reader, fromBytes) + if err != nil { + return nil, err + } + err = fromAddr.UnmarshalBinary(fromBytes) + if err != nil { + return nil, err + } + toLen, err = rw.ReadUVariant(reader) + if err != nil { + return nil, err + } + toBytes := make([]byte, toLen) + _, err = io.ReadFull(reader, toBytes) + if err != nil { + return nil, err + } + err = toAddr.UnmarshalBinary(toBytes) + if err != nil { + return nil, err + } + mySet.rr[i] = myIPRange{fromAddr, toAddr} + } + return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil +} + +func writeIPSet(writer io.Writer, set *netipx.IPSet) error { + err := binary.Write(writer, binary.BigEndian, uint8(1)) + if err != nil { + return err + } + mySet := (*myIPSet)(unsafe.Pointer(set)) + err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr))) + if err != nil { + return err + } + for _, rr := range mySet.rr { + var ( + fromBinary []byte + toBinary []byte + ) + fromBinary, err = rr.from.MarshalBinary() + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(fromBinary))) + if err != nil { + return err + } + _, err = writer.Write(fromBinary) + if err != nil { + return err + } + toBinary, err = rr.to.MarshalBinary() + if err != nil { + return err + } + err = rw.WriteUVariant(writer, uint64(len(toBinary))) + if err != nil { + return err + } + _, err = writer.Write(toBinary) + if err != nil { + return err + } + } + return nil +} diff --git a/common/taskmonitor/monitor.go b/common/taskmonitor/monitor.go new file mode 100644 index 0000000000..a23990fa79 --- /dev/null +++ b/common/taskmonitor/monitor.go @@ -0,0 +1,31 @@ +package taskmonitor + +import ( + "time" + + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/logger" +) + +type Monitor struct { + logger logger.Logger + timeout time.Duration + timer *time.Timer +} + +func New(logger logger.Logger, timeout time.Duration) *Monitor { + return &Monitor{ + logger: logger, + timeout: timeout, + } +} + +func (m *Monitor) Start(taskName ...any) { + m.timer = time.AfterFunc(m.timeout, func() { + m.logger.Warn(F.ToString(taskName...), " take too much time to finish!") + }) +} + +func (m *Monitor) Finish() { + m.timer.Stop() +} diff --git a/common/tls/client.go b/common/tls/client.go index d1c9475a41..4d6b0c540e 100644 --- a/common/tls/client.go +++ b/common/tls/client.go @@ -6,6 +6,7 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/badtls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" M "github.com/sagernet/sing/common/metadata" @@ -42,7 +43,17 @@ func NewClient(ctx context.Context, serverAddress string, options option.Outboun func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) { ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout) defer cancel() - return aTLS.ClientHandshake(ctx, conn, config) + tlsConn, err := aTLS.ClientHandshake(ctx, conn, config) + if err != nil { + return nil, err + } + readWaitConn, err := badtls.NewReadWaitConn(tlsConn) + if err == nil { + return readWaitConn, nil + } else if err != os.ErrInvalid { + return nil, err + } + return tlsConn, nil } type Dialer struct { diff --git a/common/tls/reality_client.go b/common/tls/reality_client.go index afbd3e3ee5..59ecf86003 100644 --- a/common/tls/reality_client.go +++ b/common/tls/reality_client.go @@ -7,6 +7,7 @@ import ( "context" "crypto/aes" "crypto/cipher" + "crypto/ecdh" "crypto/ed25519" "crypto/hmac" "crypto/sha256" @@ -137,12 +138,21 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn hello.SessionId[2] = 1 binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix())) copy(hello.SessionId[8:], e.shortID[:]) - if debug.Enabled { fmt.Printf("REALITY hello.sessionId[:16]: %v\n", hello.SessionId[:16]) } - - authKey := uConn.HandshakeState.State13.EcdheParams.SharedKey(e.publicKey) + publicKey, err := ecdh.X25519().NewPublicKey(e.publicKey) + if err != nil { + return nil, err + } + ecdheKey := uConn.HandshakeState.State13.EcdheKey + if ecdheKey == nil { + return nil, E.New("nil ecdhe_key") + } + authKey, err := ecdheKey.ECDH(publicKey) + if err != nil { + return nil, err + } if authKey == nil { return nil, E.New("nil auth_key") } diff --git a/common/tls/server.go b/common/tls/server.go index ac6d0a2e3e..6afd89d64d 100644 --- a/common/tls/server.go +++ b/common/tls/server.go @@ -3,7 +3,9 @@ package tls import ( "context" "net" + "os" + "github.com/sagernet/sing-box/common/badtls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -26,5 +28,15 @@ func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLS func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) { ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout) defer cancel() - return aTLS.ServerHandshake(ctx, conn, config) + tlsConn, err := aTLS.ServerHandshake(ctx, conn, config) + if err != nil { + return nil, err + } + readWaitConn, err := badtls.NewReadWaitConn(tlsConn) + if err == nil { + return readWaitConn, nil + } else if err != os.ErrInvalid { + return nil, err + } + return tlsConn, nil } diff --git a/common/tls/utls_client.go b/common/tls/utls_client.go index 267168003f..689dcd7a9b 100644 --- a/common/tls/utls_client.go +++ b/common/tls/utls_client.go @@ -253,6 +253,16 @@ func uTLSClientHelloID(name string) (utls.ClientHelloID, error) { switch name { 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/constant/rule.go b/constant/rule.go index 3c741995f8..5a8eaf127f 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -9,3 +9,11 @@ const ( LogicalTypeAnd = "and" LogicalTypeOr = "or" ) + +const ( + RuleSetTypeLocal = "local" + RuleSetTypeRemote = "remote" + RuleSetVersion1 = 1 + RuleSetFormatSource = "source" + RuleSetFormatBinary = "binary" +) diff --git a/constant/timeout.go b/constant/timeout.go index db0379a447..8d9ffbba7d 100644 --- a/constant/timeout.go +++ b/constant/timeout.go @@ -3,11 +3,15 @@ package constant import "time" const ( - TCPTimeout = 5 * time.Second - ReadPayloadTimeout = 300 * time.Millisecond - DNSTimeout = 10 * time.Second - QUICTimeout = 30 * time.Second - STUNTimeout = 15 * time.Second - UDPTimeout = 5 * time.Minute - DefaultURLTestInterval = 1 * time.Minute + TCPTimeout = 5 * time.Second + ReadPayloadTimeout = 300 * time.Millisecond + DNSTimeout = 10 * time.Second + QUICTimeout = 30 * time.Second + STUNTimeout = 15 * time.Second + UDPTimeout = 5 * time.Minute + DefaultURLTestInterval = 3 * time.Minute + DefaultURLTestIdleTimeout = 30 * time.Minute + DefaultStartTimeout = 10 * time.Second + DefaultStopTimeout = 5 * time.Second + DefaultStopFatalTimeout = 10 * time.Second ) diff --git a/debug_http.go b/debug_http.go index 218efc4f66..df356c2ed4 100644 --- a/debug_http.go +++ b/debug_http.go @@ -7,12 +7,12 @@ import ( "runtime/debug" "strings" - "github.com/sagernet/sing-box/common/badjson" "github.com/sagernet/sing-box/common/humanize" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" "github.com/go-chi/chi/v5" ) diff --git a/docs/changelog.md b/docs/changelog.md index b6160e4966..5192843c8d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,12 +2,111 @@ icon: material/alert-decagram --- -# ChangeLog +#### 1.8.2 + +* Fixes and improvements + +#### 1.8.1 + +* Fixes and improvements + +#### 1.8.0 + +* Fixes and improvements + +Important changes since 1.7: + +* Migrate cache file from Clash API to independent options **1** +* Introducing [Rule Set](/configuration/rule-set/) **2** +* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** +* Allow nested logical rules **4** +* Independent `source_ip_is_private` and `ip_is_private` rules **5** +* Add context to JSON decode error message **6** +* Reject internal fake-ip queries **7** +* Add GSO support for TUN and WireGuard system interface **8** +* Add `idle_timeout` for URLTest outbound **9** +* Add simple loopback detect +* Optimize memory usage of idle connections +* Update uTLS to 1.5.4 **10** +* Update dependencies **11** + +**1**: + +See [Cache File](/configuration/experimental/cache-file/) and +[Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options). + +**2**: + +Rule set is independent collections of rules that can be compiled into binaries to improve performance. +Compared to legacy GeoIP and Geosite resources, +it can include more types of rules, load faster, +use less memory, and update automatically. + +See [Route#rule_set](/configuration/route/#rule_set), +[Route Rule](/configuration/route/rule/), +[DNS Rule](/configuration/dns/rule/), +[Rule Set](/configuration/rule-set/), +[Source Format](/configuration/rule-set/source-format/) and +[Headless Rule](/configuration/rule-set/headless-rule/). + +For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and +[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets). + +**3**: + +New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets. + +**4**: + +Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules. + +**5**: + +The `private` GeoIP country never existed and was actually implemented inside V2Ray. +Since GeoIP was deprecated, we made this rule independent, see [Migration](/migration/#migrate-geoip-to-rule-sets). + +**6**: + +JSON parse errors will now include the current key path. +Only takes effect when compiled with Go 1.21+. + +**7**: + +All internal DNS queries now skip DNS rules with `server` type `fakeip`, +and the default DNS server can no longer be `fakeip`. + +This change is intended to break incorrect usage and essentially requires no action. + +**8**: + +See [TUN](/configuration/inbound/tun/) inbound and [WireGuard](/configuration/outbound/wireguard/) outbound. + +**9**: + +When URLTest is idle for a certain period of time, the scheduled delay test will be paused. + +**10**: + +Added some new [fingerprints](/configuration/shared/tls#utls). +Also, starting with this release, uTLS requires at least Go 1.20. + +**11**: + +Updated `cloudflare-tls`, `gomobile`, `smux`, `tfo-go` and `wireguard-go` to latest, `quic-go` to `0.40.1` and `gvisor` to `20231204.0` + + +#### 1.8.0-rc.11 + +* Fixes and improvements #### 1.7.8 * Fixes and improvements +#### 1.8.0-rc.10 + +* Fixes and improvements + #### 1.7.7 * Fix V2Ray transport `path` validation behavior **1** @@ -17,29 +116,191 @@ icon: material/alert-decagram See [V2Ray transport](/configuration/shared/v2ray-transport/). +#### 1.8.0-rc.7 + +* Fixes and improvements + +#### 1.8.0-rc.3 + +* Fix V2Ray transport `path` validation behavior **1** +* Fixes and improvements + +**1**: + +See [V2Ray transport](/configuration/shared/v2ray-transport/). + #### 1.7.6 * Fixes and improvements +#### 1.8.0-rc.1 + +* Fixes and improvements + +#### 1.8.0-beta.9 + +* Add simple loopback detect +* Fixes and improvements + #### 1.7.5 * Fixes and improvements +#### 1.8.0-alpha.17 + +* Add GSO support for TUN and WireGuard system interface **1** +* Update uTLS to 1.5.4 **2** +* Update dependencies **3** +* Fixes and improvements + +**1**: + +See [TUN](/configuration/inbound/tun/) inbound and [WireGuard](/configuration/outbound/wireguard/) outbound. + +**2**: + +Added some new [fingerprints](/configuration/shared/tls#utls). +Also, starting with this release, uTLS requires at least Go 1.20. + +**3**: + +Updated `cloudflare-tls`, `gomobile`, `smux`, `tfo-go` and `wireguard-go` to latest, and `gvisor` to `20231204.0` + +This may break something, good luck! + #### 1.7.4 * Fixes and improvements -_Due to the long waiting time, this version is no longer waiting for approval +_Due to the long waiting time, this version is no longer waiting for approval by the Apple App Store, so updates to Apple Platforms will be delayed._ +#### 1.8.0-alpha.16 + +* Fixes and improvements + +#### 1.8.0-alpha.15 + +* Some chaotic changes **1** +* Fixes and improvements + +**1**: + +Designed to optimize memory usage of idle connections, may take effect on the following protocols: + +| Protocol | TCP | UDP | +|------------------------------------------------------|------------------|------------------| +| HTTP proxy server | :material-check: | / | +| SOCKS5 | :material-close: | :material-check: | +| Shadowsocks none/AEAD/AEAD2022 | :material-check: | :material-check: | +| Trojan | / | :material-check: | +| TUIC/Hysteria/Hysteria2 | :material-close: | :material-check: | +| Multiplex | :material-close: | :material-check: | +| Plain TLS (Trojan/VLESS without extra sub-protocols) | :material-check: | / | +| Other protocols | :material-close: | :material-close: | + +At the same time, everything existing may be broken, please actively report problems with this version. + +#### 1.8.0-alpha.13 + +* Fixes and improvements + +#### 1.8.0-alpha.10 + +* Add `idle_timeout` for URLTest outbound **1** +* Fixes and improvements + +**1**: + +When URLTest is idle for a certain period of time, the scheduled delay test will be paused. + #### 1.7.2 * Fixes and improvements +#### 1.8.0-alpha.8 + +* Add context to JSON decode error message **1** +* Reject internal fake-ip queries **2** +* Fixes and improvements + +**1**: + +JSON parse errors will now include the current key path. +Only takes effect when compiled with Go 1.21+. + +**2**: + +All internal DNS queries now skip DNS rules with `server` type `fakeip`, +and the default DNS server can no longer be `fakeip`. + +This change is intended to break incorrect usage and essentially requires no action. + +#### 1.8.0-alpha.7 + +* Fixes and improvements + #### 1.7.1 * Fixes and improvements +#### 1.8.0-alpha.6 + +* Fix rule-set matching logic **1** +* Fixes and improvements + +**1**: + +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 + +* Parallel rule-set initialization +* Independent `source_ip_is_private` and `ip_is_private` rules **1** + +**1**: + +The `private` GeoIP country never existed and was actually implemented inside V2Ray. +Since GeoIP was deprecated, we made this rule independent, see [Migration](/migration/#migrate-geoip-to-rule-sets). + +#### 1.8.0-alpha.1 + +* Migrate cache file from Clash API to independent options **1** +* Introducing [Rule Set](/configuration/rule-set/) **2** +* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** +* Allow nested logical rules **4** + +**1**: + +See [Cache File](/configuration/experimental/cache-file/) and +[Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options). + +**2**: + +Rule set is independent collections of rules that can be compiled into binaries to improve performance. +Compared to legacy GeoIP and Geosite resources, +it can include more types of rules, load faster, +use less memory, and update automatically. + +See [Route#rule_set](/configuration/route/#rule_set), +[Route Rule](/configuration/route/rule/), +[DNS Rule](/configuration/dns/rule/), +[Rule Set](/configuration/rule-set/), +[Source Format](/configuration/rule-set/source-format/) and +[Headless Rule](/configuration/rule-set/headless-rule/). + +For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and +[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets). + +**3**: + +New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets. + +**4**: + +Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules. + #### 1.7.0 * Fixes and improvements @@ -72,7 +333,8 @@ The new HTTPUpgrade transport has better performance than WebSocket and is bette **3**: Starting in 1.7.0, multiplexing support is no longer enabled by default -and needs to be turned on explicitly in inbound options. +and needs to be turned on explicitly in inbound +options. **4** @@ -254,7 +516,8 @@ When `auto_route` is enabled and `strict_route` is disabled, the device can now **2**: Built using Go 1.20, the last version that will run on -Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave. +Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High +Sierra, 10.14 Mojave. #### 1.6.0-rc.4 @@ -268,7 +531,8 @@ Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave **1**: Built using Go 1.20, the last version that will run on -Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave. +Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High +Sierra, 10.14 Mojave. #### 1.6.0-beta.4 @@ -696,7 +960,8 @@ downloaded through TestFlight. #### 1.3.1-beta.3 -* Introducing our [new iOS](/installation/clients/sfi/) and [macOS](/installation/clients/sfm/) client applications **1** +* Introducing our [new iOS](/installation/clients/sfi/) and [macOS](/installation/clients/sfm/) client applications **1 + ** * Fixes and improvements **1**: diff --git a/docs/clients/android/features.md b/docs/clients/android/features.md index 2702e6fa8c..8fe84add26 100644 --- a/docs/clients/android/features.md +++ b/docs/clients/android/features.md @@ -18,6 +18,7 @@ SFA provides an unprivileged TUN implementation through Android VpnService. | `inet4_address` | :material-check: | / | | `inet6_address` | :material-check: | / | | `mtu` | :material-check: | / | +| `gso` | :material-close: | No permission | | `auto_route` | :material-check: | / | | `strict_route` | :material-close: | Not implemented | | `inet4_route_address` | :material-check: | / | diff --git a/docs/clients/apple/features.md b/docs/clients/apple/features.md index 95143d98f8..7d419103b5 100644 --- a/docs/clients/apple/features.md +++ b/docs/clients/apple/features.md @@ -14,28 +14,29 @@ SFI/SFM/SFT allows you to run sing-box through NetworkExtension with Application SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension. -| TUN inbound option | Available | Note | -|-------------------------------|-----------|-------------------| -| `interface_name` | ✖️ | Managed by Darwin | -| `inet4_address` | ✔️ | / | -| `inet6_address` | ✔️ | / | -| `mtu` | ✔️ | / | -| `auto_route` | ✔️ | / | -| `strict_route` | ✖️ | Not implemented | -| `inet4_route_address` | ✔️ | / | -| `inet6_route_address` | ✔️ | / | -| `inet4_route_exclude_address` | ✔️ | / | -| `inet6_route_exclude_address` | ✔️ | / | -| `endpoint_independent_nat` | ✔️ | / | -| `stack` | ✔️ | / | -| `include_interface` | ✖️ | Not implemented | -| `exclude_interface` | ✖️ | Not implemented | -| `include_uid` | ✖️ | Not implemented | -| `exclude_uid` | ✖️ | Not implemented | -| `include_android_user` | ✖️ | Not implemented | -| `include_package` | ✖️ | Not implemented | -| `exclude_package` | ✖️ | Not implemented | -| `platform` | ✔️ | / | +| TUN inbound option | Available | Note | +|-------------------------------|-------------------|-------------------| +| `interface_name` | :material-close:️ | Managed by Darwin | +| `inet4_address` | :material-check: | / | +| `inet6_address` | :material-check: | / | +| `mtu` | :material-check: | / | +| `gso` | :material-close: | Not implemented | +| `auto_route` | :material-check: | / | +| `strict_route` | :material-close:️ | Not implemented | +| `inet4_route_address` | :material-check: | / | +| `inet6_route_address` | :material-check: | / | +| `inet4_route_exclude_address` | :material-check: | / | +| `inet6_route_exclude_address` | :material-check: | / | +| `endpoint_independent_nat` | :material-check: | / | +| `stack` | :material-check: | / | +| `include_interface` | :material-close:️ | Not implemented | +| `exclude_interface` | :material-close:️ | Not implemented | +| `include_uid` | :material-close:️ | Not implemented | +| `exclude_uid` | :material-close:️ | Not implemented | +| `include_android_user` | :material-close:️ | Not implemented | +| `include_package` | :material-close:️ | Not implemented | +| `exclude_package` | :material-close:️ | Not implemented | +| `platform` | :material-check: | / | | Route/DNS rule option | Available | Note | |-----------------------|------------------|-----------------------| diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 297a29689c..68cc32cfae 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -1,3 +1,14 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -46,6 +57,7 @@ "10.0.0.0/24", "192.168.0.1" ], + "source_ip_is_private": false, "source_port": [ 12345 ], @@ -85,6 +97,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": [ "direct" @@ -118,10 +134,12 @@ The default rule uses the following matching logic: (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) && (`port` || `port_range`) && - (`source_geoip` || `source_ip_cidr`) && + (`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) && (`source_port` || `source_port_range`) && `other fields` + Additionally, included rule sets can be considered merged rather than as a single rule sub-item. + #### inbound Tags of [Inbound](/configuration/inbound/). @@ -166,15 +184,29 @@ Match domain using regular expression. #### geosite +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). + Match geosite. #### source_geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + Match source geoip. #### source_ip_cidr -Match source ip cidr. +Match source IP CIDR. + +#### source_ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public source IP. #### source_port @@ -250,6 +282,12 @@ Match WiFi SSID. Match WiFi BSSID. +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [Rule Set](/configuration/route/#rule_set). + #### invert Invert match result. @@ -286,4 +324,4 @@ Rewrite TTL in DNS responses. #### rules -Included default rules. \ No newline at end of file +Included rules. \ No newline at end of file diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 9cb4e89d82..5b1d75019a 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -1,3 +1,14 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -45,6 +56,7 @@ "source_ip_cidr": [ "10.0.0.0/24" ], + "source_ip_is_private": false, "source_port": [ 12345 ], @@ -84,6 +96,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": [ "direct" @@ -115,10 +131,12 @@ 默认规则使用以下匹配逻辑: (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) && (`port` || `port_range`) && - (`source_geoip` || `source_ip_cidr`) && + (`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) && (`source_port` || `source_port_range`) && `other fields` + 另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。 + #### inbound [入站](/zh/configuration/inbound/) 标签. @@ -163,16 +181,30 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### geosite -匹配 GeoSite。 +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。 + +匹配 Geosite。 #### source_geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。 + 匹配源 GeoIP。 #### source_ip_cidr 匹配源 IP CIDR。 +#### source_ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开源 IP。 + #### source_port 匹配源端口。 @@ -245,6 +277,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配 WiFi BSSID。 +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + #### invert 反选匹配结果。 @@ -281,4 +319,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### rules -包括的默认规则。 \ No newline at end of file +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/dns/server.md b/docs/configuration/dns/server.md index e408b804b9..545810bf9e 100644 --- a/docs/configuration/dns/server.md +++ b/docs/configuration/dns/server.md @@ -45,20 +45,12 @@ The address of the dns server. !!! warning "" - To ensure that system DNS is in effect, rather than Go's built-in default resolver, enable CGO at compile time. - -!!! warning "" - - QUIC and HTTP3 transport is not included by default, see [Installation](./#installation). + To ensure that Android system DNS is in effect, rather than Go's built-in default resolver, enable CGO at compile time. !!! info "" the RCode transport is often used to block queries. Use with rules and the `disable_cache` rule option. -!!! warning "" - - DHCP transport is not included by default, see [Installation](./#installation). - | RCode | Description | |-------------------|-----------------------| | `success` | `No error` | diff --git a/docs/configuration/dns/server.zh.md b/docs/configuration/dns/server.zh.md index 4ce8c01d8d..36bcde5d3c 100644 --- a/docs/configuration/dns/server.zh.md +++ b/docs/configuration/dns/server.zh.md @@ -45,20 +45,12 @@ DNS 服务器的地址。 !!! warning "" - 为了确保系统 DNS 生效,而不是 Go 的内置默认解析器,请在编译时启用 CGO。 - -!!! warning "" - - 默认安装不包含 QUIC 和 HTTP3 传输层,请参阅 [安装](/zh/#_2)。 + 为了确保 Android 系统 DNS 生效,而不是 Go 的内置默认解析器,请在编译时启用 CGO。 !!! info "" RCode 传输层传输层常用于屏蔽请求. 与 DNS 规则和 `disable_cache` 规则选项一起使用。 -!!! warning "" - - 默认安装不包含 DHCP 传输层,请参阅 [安装](/zh/#_2)。 - | RCode | 描述 | |-------------------|----------| | `success` | `无错误` | diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md new file mode 100644 index 0000000000..66e30ef9b0 --- /dev/null +++ b/docs/configuration/experimental/cache-file.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "enabled": true, + "path": "", + "cache_id": "", + "store_fakeip": false +} +``` + +### Fields + +#### enabled + +Enable cache file. + +#### path + +Path to the cache file. + +`cache.db` will be used if empty. + +#### cache_id + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. diff --git a/docs/configuration/experimental/cache-file.zh.md b/docs/configuration/experimental/cache-file.zh.md new file mode 100644 index 0000000000..f4417ede45 --- /dev/null +++ b/docs/configuration/experimental/cache-file.zh.md @@ -0,0 +1,32 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.8.0 起" + +### 结构 + +```json +{ + "enabled": true, + "path": "", + "cache_id": "", + "store_fakeip": false +} +``` + +### 字段 + +#### enabled + +启用缓存文件。 + +#### path + +缓存文件路径,默认使用`cache.db`。 + +#### cache_id + +缓存文件中的标识符。 + +如果不为空,配置特定的数据将使用由其键控的单独存储。 diff --git a/docs/configuration/experimental/clash-api.md b/docs/configuration/experimental/clash-api.md new file mode 100644 index 0000000000..0525d14d64 --- /dev/null +++ b/docs/configuration/experimental/clash-api.md @@ -0,0 +1,114 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-delete-alert: [store_mode](#store_mode) + :material-delete-alert: [store_selected](#store_selected) + :material-delete-alert: [store_fakeip](#store_fakeip) + :material-delete-alert: [cache_file](#cache_file) + :material-delete-alert: [cache_id](#cache_id) + +### Structure + +```json +{ + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" +} +``` + +### Fields + +#### external_controller + +RESTful web API listening address. Clash API will be disabled if empty. + +#### external_ui + +A relative path to the configuration directory or an absolute path to a +directory in which you put some static web resource. sing-box will then +serve it at `http://{{external-controller}}/ui`. + +#### external_ui_download_url + +ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. + +`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. + +#### external_ui_download_detour + +The tag of the outbound to download the external UI. + +Default outbound will be used if empty. + +#### secret + +Secret for the RESTful API (optional) +Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` +ALWAYS set a secret if RESTful API is listening on 0.0.0.0 + +#### default_mode + +Default mode in clash, `Rule` will be used if empty. + +This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. + +#### store_mode + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_mode` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +Store Clash mode in cache file. + +#### store_selected + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and enabled by default if `cache_file.enabled`. + +!!! note "" + + The tag must be set for target outbounds. + +Store selected outbound for the `Selector` outbound in cache file. + +#### store_fakeip + +!!! failure "Deprecated in sing-box 1.8.0" + + `store_selected` is deprecated in Clash API and migrated to `cache_file.store_fakeip`. + +Store fakeip in cache file. + +#### cache_file + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_file` is deprecated in Clash API and migrated to `cache_file.enabled` and `cache_file.path`. + +Cache file path, `cache.db` will be used if empty. + +#### cache_id + +!!! failure "Deprecated in sing-box 1.8.0" + + `cache_id` is deprecated in Clash API and migrated to `cache_file.cache_id`. + +Identifier in cache file. + +If not empty, configuration specified data will use a separate store keyed by it. diff --git a/docs/configuration/experimental/clash-api.zh.md b/docs/configuration/experimental/clash-api.zh.md new file mode 100644 index 0000000000..5a490e587b --- /dev/null +++ b/docs/configuration/experimental/clash-api.zh.md @@ -0,0 +1,112 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-delete-alert: [store_mode](#store_mode) + :material-delete-alert: [store_selected](#store_selected) + :material-delete-alert: [store_fakeip](#store_fakeip) + :material-delete-alert: [cache_file](#cache_file) + :material-delete-alert: [cache_id](#cache_id) + +### 结构 + +```json +{ + "external_controller": "127.0.0.1:9090", + "external_ui": "", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "", + + // Deprecated + + "store_mode": false, + "store_selected": false, + "store_fakeip": false, + "cache_file": "", + "cache_id": "" +} +``` + +### Fields + +#### external_controller + +RESTful web API 监听地址。如果为空,则禁用 Clash API。 + +#### external_ui + +到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。 + +#### external_ui_download_url + +静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。 + +默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。 + +#### external_ui_download_detour + +用于下载静态网页资源的出站的标签。 + +如果为空,将使用默认出站。 + +#### secret + +RESTful API 的密钥(可选) +通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证 +如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。 + +#### default_mode + +Clash 中的默认模式,默认使用 `Rule`。 + +此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。 + +#### store_mode + +!!! failure "已在 sing-box 1.8.0 废弃" + + `store_mode` 已在 Clash API 中废弃,且默认启用当 `cache_file.enabled`。 + +将 Clash 模式存储在缓存文件中。 + +#### store_selected + +!!! failure "已在 sing-box 1.8.0 废弃" + + `store_selected` 已在 Clash API 中废弃,且默认启用当 `cache_file.enabled`。 + +!!! note "" + + 必须为目标出站设置标签。 + +将 `Selector` 中出站的选定的目标出站存储在缓存文件中。 + +#### store_fakeip + +!!! failure "已在 sing-box 1.8.0 废弃" + + `store_selected` 已在 Clash API 中废弃,且已迁移到 `cache_file.store_fakeip`。 + +将 fakeip 存储在缓存文件中。 + +#### cache_file + +!!! failure "已在 sing-box 1.8.0 废弃" + + `cache_file` 已在 Clash API 中废弃,且已迁移到 `cache_file.enabled` 和 `cache_file.path`。 + +缓存文件路径,默认使用`cache.db`。 + +#### cache_id + +!!! failure "已在 sing-box 1.8.0 废弃" + + `cache_id` 已在 Clash API 中废弃,且已迁移到 `cache_file.cache_id`。 + +缓存 ID。 + +如果不为空,配置特定的数据将使用由其键控的单独存储。 diff --git a/docs/configuration/experimental/index.md b/docs/configuration/experimental/index.md index 308e851c3b..4ddcc41af7 100644 --- a/docs/configuration/experimental/index.md +++ b/docs/configuration/experimental/index.md @@ -1,139 +1,30 @@ +--- +icon: material/alert-decagram +--- + # Experimental +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [cache_file](#cache_file) + :material-alert-decagram: [clash_api](#clash_api) + ### Structure ```json { "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" - }, - "v2ray_api": { - "listen": "127.0.0.1:8080", - "stats": { - "enabled": true, - "inbounds": [ - "socks-in" - ], - "outbounds": [ - "proxy", - "direct" - ], - "users": [ - "sekai" - ] - } - } + "cache_file": {}, + "clash_api": {}, + "v2ray_api": {} } } ``` -!!! note "" - - Traffic statistics and connection management can degrade performance. - -### Clash API Fields - -!!! quote "" - - Clash API is not included by default, see [Installation](./#installation). - -#### external_controller - -RESTful web API listening address. Clash API will be disabled if empty. - -#### external_ui - -A relative path to the configuration directory or an absolute path to a -directory in which you put some static web resource. sing-box will then -serve it at `http://{{external-controller}}/ui`. - -#### external_ui_download_url - -ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. - -`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. - -#### external_ui_download_detour - -The tag of the outbound to download the external UI. - -Default outbound will be used if empty. - -#### secret - -Secret for the RESTful API (optional) -Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` -ALWAYS set a secret if RESTful API is listening on 0.0.0.0 - -#### default_mode - -Default mode in clash, `Rule` will be used if empty. - -This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. - -#### store_mode - -Store Clash mode in cache file. - -#### store_selected - -!!! note "" - - The tag must be set for target outbounds. - -Store selected outbound for the `Selector` outbound in cache file. - -#### store_fakeip - -Store fakeip in cache file. - -#### cache_file - -Cache file path, `cache.db` will be used if empty. - -#### cache_id - -Cache ID. - -If not empty, `store_selected` will use a separate store keyed by it. - -### V2Ray API Fields - -!!! quote "" - - V2Ray API is not included by default, see [Installation](./#installation). - -#### listen - -gRPC API listening address. V2Ray API will be disabled if empty. - -#### stats - -Traffic statistics service settings. - -#### stats.enabled - -Enable statistics service. - -#### stats.inbounds - -Inbound list to count traffic. - -#### stats.outbounds - -Outbound list to count traffic. - -#### stats.users +### Fields -User list to count traffic. \ No newline at end of file +| Key | Format | +|--------------|----------------------------| +| `cache_file` | [Cache File](./cache-file/) | +| `clash_api` | [Clash API](./clash-api/) | +| `v2ray_api` | [V2Ray API](./v2ray-api/) | \ No newline at end of file diff --git a/docs/configuration/experimental/index.zh.md b/docs/configuration/experimental/index.zh.md index 88a95852c9..4be70aa7d4 100644 --- a/docs/configuration/experimental/index.zh.md +++ b/docs/configuration/experimental/index.zh.md @@ -1,137 +1,30 @@ +--- +icon: material/alert-decagram +--- + # 实验性 +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [cache_file](#cache_file) + :material-alert-decagram: [clash_api](#clash_api) + ### 结构 ```json { "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "", - "external_ui_download_url": "", - "external_ui_download_detour": "", - "secret": "", - "default_mode": "", - "store_mode": false, - "store_selected": false, - "store_fakeip": false, - "cache_file": "", - "cache_id": "" - }, - "v2ray_api": { - "listen": "127.0.0.1:8080", - "stats": { - "enabled": true, - "inbounds": [ - "socks-in" - ], - "outbounds": [ - "proxy", - "direct" - ], - "users": [ - "sekai" - ] - } - } + "cache_file": {}, + "clash_api": {}, + "v2ray_api": {} } } ``` -!!! note "" - - 流量统计和连接管理会降低性能。 - -### Clash API 字段 - -!!! quote "" - - 默认安装不包含 Clash API,参阅 [安装](/zh/#_2)。 - -#### external_controller - -RESTful web API 监听地址。如果为空,则禁用 Clash API。 - -#### external_ui - -到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。 - -#### external_ui_download_url - -静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。 - -默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。 - -#### external_ui_download_detour - -用于下载静态网页资源的出站的标签。 - -如果为空,将使用默认出站。 - -#### secret - -RESTful API 的密钥(可选) -通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证 -如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。 - -#### default_mode - -Clash 中的默认模式,默认使用 `Rule`。 - -此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。 - -#### store_mode - -将 Clash 模式存储在缓存文件中。 - -#### store_selected - -!!! note "" - - 必须为目标出站设置标签。 - -将 `Selector` 中出站的选定的目标出站存储在缓存文件中。 - -#### store_fakeip - -将 fakeip 存储在缓存文件中。 - -#### cache_file - -缓存文件路径,默认使用`cache.db`。 - -#### cache_id - -缓存 ID。 - -如果不为空,`store_selected` 将会使用以此为键的独立存储。 - -### V2Ray API 字段 - -!!! quote "" - - 默认安装不包含 V2Ray API,参阅 [安装](/zh/#_2)。 - -#### listen - -gRPC API 监听地址。如果为空,则禁用 V2Ray API。 - -#### stats - -流量统计服务设置。 - -#### stats.enabled - -启用统计服务。 - -#### stats.inbounds - -统计流量的入站列表。 - -#### stats.outbounds - -统计流量的出站列表。 - -#### stats.users +### 字段 -统计流量的用户列表。 \ No newline at end of file +| 键 | 格式 | +|--------------|--------------------------| +| `cache_file` | [缓存文件](./cache-file/) | +| `clash_api` | [Clash API](./clash-api/) | +| `v2ray_api` | [V2Ray API](./v2ray-api/) | \ No newline at end of file diff --git a/docs/configuration/experimental/v2ray-api.md b/docs/configuration/experimental/v2ray-api.md new file mode 100644 index 0000000000..4e23dea92d --- /dev/null +++ b/docs/configuration/experimental/v2ray-api.md @@ -0,0 +1,50 @@ +!!! quote "" + + V2Ray API is not included by default, see [Installation](/installation/build-from-source/#build-tags). + +### Structure + +```json +{ + "listen": "127.0.0.1:8080", + "stats": { + "enabled": true, + "inbounds": [ + "socks-in" + ], + "outbounds": [ + "proxy", + "direct" + ], + "users": [ + "sekai" + ] + } +} +``` + +### Fields + +#### listen + +gRPC API listening address. V2Ray API will be disabled if empty. + +#### stats + +Traffic statistics service settings. + +#### stats.enabled + +Enable statistics service. + +#### stats.inbounds + +Inbound list to count traffic. + +#### stats.outbounds + +Outbound list to count traffic. + +#### stats.users + +User list to count traffic. \ No newline at end of file diff --git a/docs/configuration/experimental/v2ray-api.zh.md b/docs/configuration/experimental/v2ray-api.zh.md new file mode 100644 index 0000000000..81fc842787 --- /dev/null +++ b/docs/configuration/experimental/v2ray-api.zh.md @@ -0,0 +1,50 @@ +!!! quote "" + + 默认安装不包含 V2Ray API,参阅 [安装](/zh/installation/build-from-source/#_5)。 + +### 结构 + +```json +{ + "listen": "127.0.0.1:8080", + "stats": { + "enabled": true, + "inbounds": [ + "socks-in" + ], + "outbounds": [ + "proxy", + "direct" + ], + "users": [ + "sekai" + ] + } +} +``` + +### 字段 + +#### listen + +gRPC API 监听地址。如果为空,则禁用 V2Ray API。 + +#### stats + +流量统计服务设置。 + +#### stats.enabled + +启用统计服务。 + +#### stats.inbounds + +统计流量的入站列表。 + +#### stats.outbounds + +统计流量的出站列表。 + +#### stats.users + +统计流量的用户列表。 \ No newline at end of file diff --git a/docs/configuration/inbound/hysteria.md b/docs/configuration/inbound/hysteria.md index a0b24866d6..4725aafcea 100644 --- a/docs/configuration/inbound/hysteria.md +++ b/docs/configuration/inbound/hysteria.md @@ -29,10 +29,6 @@ } ``` -!!! warning "" - - QUIC, which is required by hysteria is not included by default, see [Installation](./#installation). - ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. diff --git a/docs/configuration/inbound/hysteria.zh.md b/docs/configuration/inbound/hysteria.zh.md index b7534058a9..b75660523e 100644 --- a/docs/configuration/inbound/hysteria.zh.md +++ b/docs/configuration/inbound/hysteria.zh.md @@ -29,10 +29,6 @@ } ``` -!!! warning "" - - 默认安装不包含被 Hysteria 依赖的 QUIC,参阅 [安装](/zh/#_2)。 - ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md index c0ae52b772..7c611e6491 100644 --- a/docs/configuration/inbound/hysteria2.md +++ b/docs/configuration/inbound/hysteria2.md @@ -26,10 +26,6 @@ } ``` -!!! warning "" - - QUIC, which is required by Hysteria2 is not included by default, see [Installation](./#installation). - !!! warning "Difference from official Hysteria2" The official program supports an authentication method called **userpass**, diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index 4d5a94157c..c936aae8f8 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -26,10 +26,6 @@ } ``` -!!! warning "" - - 默认安装不包含被 Hysteria2 依赖的 QUIC,参阅 [安装](/zh/#_2)。 - !!! warning "与官方 Hysteria2 的区别" 官方程序支持一种名为 **userpass** 的验证方式, diff --git a/docs/configuration/inbound/naive.md b/docs/configuration/inbound/naive.md index 0e4b8cea29..0b4ff4b776 100644 --- a/docs/configuration/inbound/naive.md +++ b/docs/configuration/inbound/naive.md @@ -18,10 +18,6 @@ } ``` -!!! warning "" - - HTTP3 transport is not included by default, see [Installation](./#installation). - ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. diff --git a/docs/configuration/inbound/naive.zh.md b/docs/configuration/inbound/naive.zh.md index 083d64296f..5707e65361 100644 --- a/docs/configuration/inbound/naive.zh.md +++ b/docs/configuration/inbound/naive.zh.md @@ -18,10 +18,6 @@ } ``` -!!! warning "" - - 默认安装不包含 HTTP3 传输层, 参阅 [安装](/zh/#_2)。 - ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 diff --git a/docs/configuration/inbound/tuic.md b/docs/configuration/inbound/tuic.md index 4ed3dca95e..8a2d8c7e06 100644 --- a/docs/configuration/inbound/tuic.md +++ b/docs/configuration/inbound/tuic.md @@ -22,10 +22,6 @@ } ``` -!!! warning "" - - QUIC, which is required by TUIC is not included by default, see [Installation](./#installation). - ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. diff --git a/docs/configuration/inbound/tuic.zh.md b/docs/configuration/inbound/tuic.zh.md index 60a7ccf6fd..99252056e1 100644 --- a/docs/configuration/inbound/tuic.zh.md +++ b/docs/configuration/inbound/tuic.zh.md @@ -22,10 +22,6 @@ } ``` -!!! warning "" - - 默认安装不包含被 TUI 依赖的 QUIC,参阅 [安装](/zh/#_2)。 - ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index d954de6be3..002c690a4b 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -1,3 +1,12 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [gso](#gso) + :material-alert-decagram: [stack](#stack) + !!! quote "" Only supported on Linux, Windows and macOS. @@ -12,6 +21,7 @@ "inet4_address": "172.19.0.1/30", "inet6_address": "fdfe:dcba:9876::1/126", "mtu": 9000, + "gso": false, "auto_route": true, "strict_route": true, "inet4_route_address": [ @@ -99,6 +109,16 @@ IPv6 prefix for the tun interface. The maximum transmission unit. +#### gso + +!!! question "Since sing-box 1.8.0" + +!!! quote "" + + Only supported on Linux. + +Enable generic segmentation offload. + #### auto_route Set the default route to the Tun. @@ -161,18 +181,19 @@ UDP NAT expiration time in seconds, default is 300 (5 minutes). #### stack -TCP/IP stack. +!!! quote "Changes in sing-box 1.8.0" -| Stack | Description | Status | -|--------|----------------------------------------------------------------------------------|-------------------| -| system | Sometimes better performance | recommended | -| gVisor | Better compatibility, based on [google/gvisor](https://github.com/google/gvisor) | recommended | -| mixed | Mixed `system` TCP stack and `gVisor` UDP stack | recommended | -| LWIP | Based on [eycorsican/go-tun2socks](https://github.com/eycorsican/go-tun2socks) | upstream archived | + :material-delete-alert: The legacy LWIP stack has been deprecated and removed. -!!! warning "" +TCP/IP stack. + +| Stack | Description | +|----------|-------------------------------------------------------------------------------------------------------| +| `system` | Perform L3 to L4 translation using the system network stack | +| `gvisor` | Perform L3 to L4 translation using [gVisor](https://github.com/google/gvisor)'s virtual network stack | +| `mixed` | Mixed `system` TCP stack and `gvisor` UDP stack | - gVisor and LWIP stacks is not included by default, see [Installation](./#installation). +Defaults to the `mixed` stack if the gVisor build tag is enabled, otherwise defaults to the `system` stack. #### include_interface @@ -218,10 +239,10 @@ Exclude users in route, but in range. Limit android users in route. -| Common user | ID | -|--------------|-----| -| Main | 0 | -| Work Profile | 10 | +| Common user | ID | +|--------------|----| +| Main | 0 | +| Work Profile | 10 | #### include_package diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index d0bef4ff90..6a80063487 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -1,3 +1,12 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [gso](#gso) + :material-alert-decagram: [stack](#stack) + !!! quote "" 仅支持 Linux、Windows 和 macOS。 @@ -12,6 +21,7 @@ "inet4_address": "172.19.0.1/30", "inet6_address": "fdfe:dcba:9876::1/126", "mtu": 9000, + "gso": false, "auto_route": true, "strict_route": true, "inet4_route_address": [ @@ -99,6 +109,16 @@ tun 接口的 IPv6 前缀。 最大传输单元。 +#### gso + +!!! question "自 sing-box 1.8.0 起" + +!!! quote "" + + 仅支持 Linux。 + +启用通用分段卸载。 + #### auto_route 设置到 Tun 的默认路由。 @@ -158,17 +178,19 @@ UDP NAT 过期时间,以秒为单位,默认为 300(5 分钟)。 #### stack -TCP/IP 栈。 +!!! quote "sing-box 1.8.0 中的更改" -| 栈 | 描述 | 状态 | -|-------------|--------------------------------------------------------------------------|-------| -| system (默认) | 有时性能更好 | 推荐 | -| gVisor | 兼容性较好,基于 [google/gvisor](https://github.com/google/gvisor) | 推荐 | -| LWIP | 基于 [eycorsican/go-tun2socks](https://github.com/eycorsican/go-tun2socks) | 上游已存档 | + :material-delete-alert: 旧的 LWIP 栈已被弃用并移除。 -!!! warning "" +TCP/IP 栈。 + +| 栈 | 描述 | +|--------|------------------------------------------------------------------| +| system | 基于系统网络栈执行 L3 到 L4 转换 | +| gVisor | 基于 [gVisor](https://github.com/google/gvisor) 虚拟网络栈执行 L3 到 L4 转换 | +| mixed | 混合 `system` TCP 栈与 `gvisor` UDP 栈 | - 默认安装不包含 gVisor 和 LWIP 栈,请参阅 [安装](/zh/#_2)。 +默认使用 `mixed` 栈如果 gVisor 构建标记已启用,否则默认使用 `system` 栈。 #### include_interface @@ -215,8 +237,8 @@ TCP/IP 栈。 限制被路由的 Android 用户。 | 常用用户 | ID | -|--|-----| -| 您 | 0 | +|------|----| +| 您 | 0 | | 工作资料 | 10 | #### include_package diff --git a/docs/configuration/outbound/hysteria.md b/docs/configuration/outbound/hysteria.md index ab9191d722..90190e051f 100644 --- a/docs/configuration/outbound/hysteria.md +++ b/docs/configuration/outbound/hysteria.md @@ -24,10 +24,6 @@ } ``` -!!! warning "" - - QUIC, which is required by hysteria is not included by default, see [Installation](./#installation). - ### Fields #### server diff --git a/docs/configuration/outbound/hysteria.zh.md b/docs/configuration/outbound/hysteria.zh.md index 8f0eb3d986..c6ee2313f0 100644 --- a/docs/configuration/outbound/hysteria.zh.md +++ b/docs/configuration/outbound/hysteria.zh.md @@ -24,10 +24,6 @@ } ``` -!!! warning "" - - 默认安装不包含被 Hysteria 依赖的 QUIC,参阅 [安装](/zh/#_2)。 - ### 字段 #### server diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md index eacb284ec1..ae0b96edda 100644 --- a/docs/configuration/outbound/hysteria2.md +++ b/docs/configuration/outbound/hysteria2.md @@ -22,10 +22,6 @@ } ``` -!!! warning "" - - QUIC, which is required by Hysteria2 is not included by default, see [Installation](./#installation). - !!! warning "Difference from official Hysteria2" The official Hysteria2 supports an authentication method called **userpass**, diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index 5d20802794..7176b9a6f7 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -22,10 +22,6 @@ } ``` -!!! warning "" - - 默认安装不包含被 Hysteria2 依赖的 QUIC,参阅 [安装](/zh/#_2)。 - !!! warning "与官方 Hysteria2 的区别" 官方程序支持一种名为 **userpass** 的验证方式, diff --git a/docs/configuration/outbound/index.md b/docs/configuration/outbound/index.md index 1ce5405f70..c5dc291769 100644 --- a/docs/configuration/outbound/index.md +++ b/docs/configuration/outbound/index.md @@ -15,8 +15,8 @@ ### Fields -| Type | Format | -|----------------|---------------------------------| +| Type | Format | +|----------------|--------------------------------| | `direct` | [Direct](./direct/) | | `block` | [Block](./block/) | | `socks` | [SOCKS](./socks/) | @@ -26,7 +26,6 @@ | `trojan` | [Trojan](./trojan/) | | `wireguard` | [Wireguard](./wireguard/) | | `hysteria` | [Hysteria](./hysteria/) | -| `shadowsocksr` | [ShadowsocksR](./shadowsocksr/) | | `vless` | [VLESS](./vless/) | | `shadowtls` | [ShadowTLS](./shadowtls/) | | `tuic` | [TUIC](./tuic/) | diff --git a/docs/configuration/outbound/index.zh.md b/docs/configuration/outbound/index.zh.md index f1577dd405..c7ee59e91d 100644 --- a/docs/configuration/outbound/index.zh.md +++ b/docs/configuration/outbound/index.zh.md @@ -15,8 +15,8 @@ ### 字段 -| 类型 | 格式 | -|----------------|---------------------------------| +| 类型 | 格式 | +|----------------|--------------------------------| | `direct` | [Direct](./direct/) | | `block` | [Block](./block/) | | `socks` | [SOCKS](./socks/) | @@ -26,7 +26,6 @@ | `trojan` | [Trojan](./trojan/) | | `wireguard` | [Wireguard](./wireguard/) | | `hysteria` | [Hysteria](./hysteria/) | -| `shadowsocksr` | [ShadowsocksR](./shadowsocksr/) | | `vless` | [VLESS](./vless/) | | `shadowtls` | [ShadowTLS](./shadowtls/) | | `tuic` | [TUIC](./tuic/) | diff --git a/docs/configuration/outbound/shadowsocksr.md b/docs/configuration/outbound/shadowsocksr.md deleted file mode 100644 index 2ac145f361..0000000000 --- a/docs/configuration/outbound/shadowsocksr.md +++ /dev/null @@ -1,106 +0,0 @@ -### Structure - -```json -{ - "type": "shadowsocksr", - "tag": "ssr-out", - - "server": "127.0.0.1", - "server_port": 1080, - "method": "aes-128-cfb", - "password": "8JCsPssfgS8tiRwiMlhARg==", - "obfs": "plain", - "obfs_param": "", - "protocol": "origin", - "protocol_param": "", - "network": "udp", - - ... // Dial Fields -} -``` - -!!! warning "" - - The ShadowsocksR protocol is obsolete and unmaintained. This outbound is provided for compatibility only. - -!!! warning "" - - ShadowsocksR is not included by default, see [Installation](./#installation). - -### Fields - -#### server - -==Required== - -The server address. - -#### server_port - -==Required== - -The server port. - -#### method - -==Required== - -Encryption methods: - -* `aes-128-ctr` -* `aes-192-ctr` -* `aes-256-ctr` -* `aes-128-cfb` -* `aes-192-cfb` -* `aes-256-cfb` -* `rc4-md5` -* `chacha20-ietf` -* `xchacha20` - -#### password - -==Required== - -The shadowsocks password. - -#### obfs - -The ShadowsocksR obfuscate. - -* plain -* http_simple -* http_post -* random_head -* tls1.2_ticket_auth - -#### obfs_param - -The ShadowsocksR obfuscate parameter. - -#### protocol - -The ShadowsocksR protocol. - -* origin -* verify_sha1 -* auth_sha1_v4 -* auth_aes128_md5 -* auth_aes128_sha1 -* auth_chain_a -* auth_chain_b - -#### protocol_param - -The ShadowsocksR protocol parameter. - -#### network - -Enabled network - -One of `tcp` `udp`. - -Both is enabled by default. - -### Dial Fields - -See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/shadowsocksr.zh.md b/docs/configuration/outbound/shadowsocksr.zh.md deleted file mode 100644 index ced3756ad6..0000000000 --- a/docs/configuration/outbound/shadowsocksr.zh.md +++ /dev/null @@ -1,106 +0,0 @@ -### 结构 - -```json -{ - "type": "shadowsocksr", - "tag": "ssr-out", - - "server": "127.0.0.1", - "server_port": 1080, - "method": "aes-128-cfb", - "password": "8JCsPssfgS8tiRwiMlhARg==", - "obfs": "plain", - "obfs_param": "", - "protocol": "origin", - "protocol_param": "", - "network": "udp", - - ... // 拨号字段 -} -``` - -!!! warning "" - - ShadowsocksR 协议已过时且无人维护。 提供此出站仅出于兼容性目的。 - -!!! warning "" - - 默认安装不包含被 ShadowsocksR,参阅 [安装](/zh/#_2)。 - -### 字段 - -#### server - -==必填== - -服务器地址。 - -#### server_port - -==必填== - -服务器端口。 - -#### method - -==必填== - -加密方法: - -* `aes-128-ctr` -* `aes-192-ctr` -* `aes-256-ctr` -* `aes-128-cfb` -* `aes-192-cfb` -* `aes-256-cfb` -* `rc4-md5` -* `chacha20-ietf` -* `xchacha20` - -#### password - -==必填== - -Shadowsocks 密码。 - -#### obfs - -ShadowsocksR 混淆。 - -* plain -* http_simple -* http_post -* random_head -* tls1.2_ticket_auth - -#### obfs_param - -ShadowsocksR 混淆参数。 - -#### protocol - -ShadowsocksR 协议。 - -* origin -* verify_sha1 -* auth_sha1_v4 -* auth_aes128_md5 -* auth_aes128_sha1 -* auth_chain_a -* auth_chain_b - -#### protocol_param - -ShadowsocksR 协议参数。 - -#### network - -启用的网络协议 - -`tcp` 或 `udp`。 - -默认所有。 - -### 拨号字段 - -参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/outbound/tor.md b/docs/configuration/outbound/tor.md index 0c1baf96e3..ac77833509 100644 --- a/docs/configuration/outbound/tor.md +++ b/docs/configuration/outbound/tor.md @@ -18,7 +18,7 @@ !!! info "" - Embedded tor is not included by default, see [Installation](./#installation). + Embedded Tor is not included by default, see [Installation](/installation/build-from-source/#build-tags). ### Fields diff --git a/docs/configuration/outbound/tor.zh.md b/docs/configuration/outbound/tor.zh.md index 2ddf832b8e..be5059642f 100644 --- a/docs/configuration/outbound/tor.zh.md +++ b/docs/configuration/outbound/tor.zh.md @@ -18,7 +18,7 @@ !!! info "" - 默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/#_2)。 + 默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#_5)。 ### 字段 diff --git a/docs/configuration/outbound/tuic.md b/docs/configuration/outbound/tuic.md index 80baf41e9c..4f4ef4850d 100644 --- a/docs/configuration/outbound/tuic.md +++ b/docs/configuration/outbound/tuic.md @@ -21,10 +21,6 @@ } ``` -!!! warning "" - - QUIC, which is required by TUIC is not included by default, see [Installation](./#installation). - ### Fields #### server diff --git a/docs/configuration/outbound/tuic.zh.md b/docs/configuration/outbound/tuic.zh.md index b81ffb52ca..ee8fd15d3f 100644 --- a/docs/configuration/outbound/tuic.zh.md +++ b/docs/configuration/outbound/tuic.zh.md @@ -21,10 +21,6 @@ } ``` -!!! warning "" - - 默认安装不包含被 TUI 依赖的 QUIC,参阅 [安装](/zh/#_2)。 - ### 字段 #### server diff --git a/docs/configuration/outbound/urltest.md b/docs/configuration/outbound/urltest.md index d905068dfe..f4b3b0aa8e 100644 --- a/docs/configuration/outbound/urltest.md +++ b/docs/configuration/outbound/urltest.md @@ -10,9 +10,10 @@ "proxy-b", "proxy-c" ], - "url": "https://www.gstatic.com/generate_204", - "interval": "1m", - "tolerance": 50, + "url": "", + "interval": "", + "tolerance": 0, + "idle_timeout": "", "interrupt_exist_connections": false } ``` @@ -31,12 +32,16 @@ The URL to test. `https://www.gstatic.com/generate_204` will be used if empty. #### interval -The test interval. `1m` will be used if empty. +The test interval. `3m` will be used if empty. #### tolerance The test tolerance in milliseconds. `50` will be used if empty. +#### idle_timeout + +The idle timeout. `30m` will be used if empty. + #### interrupt_exist_connections Interrupt existing connections when the selected outbound has changed. diff --git a/docs/configuration/outbound/urltest.zh.md b/docs/configuration/outbound/urltest.zh.md index 0ad891f6bd..4372298afc 100644 --- a/docs/configuration/outbound/urltest.zh.md +++ b/docs/configuration/outbound/urltest.zh.md @@ -10,9 +10,10 @@ "proxy-b", "proxy-c" ], - "url": "https://www.gstatic.com/generate_204", - "interval": "1m", + "url": "", + "interval": "", "tolerance": 50, + "idle_timeout": "", "interrupt_exist_connections": false } ``` @@ -31,12 +32,16 @@ #### interval -测试间隔。 默认使用 `1m`。 +测试间隔。 默认使用 `3m`。 #### tolerance 以毫秒为单位的测试容差。 默认使用 `50`。 +#### idle_timeout + +空闲超时。默认使用 `30m`。 + #### interrupt_exist_connections 当选定的出站发生更改时,中断现有连接。 diff --git a/docs/configuration/outbound/wireguard.md b/docs/configuration/outbound/wireguard.md index f0d0ab83ab..4cd91d2225 100644 --- a/docs/configuration/outbound/wireguard.md +++ b/docs/configuration/outbound/wireguard.md @@ -1,3 +1,11 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [gso](#gso) + ### Structure ```json @@ -8,6 +16,7 @@ "server": "127.0.0.1", "server_port": 1080, "system_interface": false, + "gso": false, "interface_name": "wg0", "local_address": [ "10.0.0.2/32" @@ -36,14 +45,6 @@ } ``` -!!! warning "" - - WireGuard is not included by default, see [Installation](./#installation). - -!!! warning "" - - gVisor, which is required by the unprivileged WireGuard is not included by default, see [Installation](./#installation). - ### Fields #### server @@ -60,15 +61,25 @@ The server port. #### system_interface -Use system tun support. +Use system interface. -Requires privilege and cannot conflict with system interfaces. +Requires privilege and cannot conflict with exists system interfaces. Forced if gVisor not included in the build. #### interface_name -Custom device name when `system_interface` enabled. +Custom interface name for system interface. + +#### gso + +!!! question "Since sing-box 1.8.0" + +!!! quote "" + + Only supported on Linux. + +Try to enable generic segmentation offload. #### local_address diff --git a/docs/configuration/outbound/wireguard.zh.md b/docs/configuration/outbound/wireguard.zh.md index 2648247b16..e853d72e85 100644 --- a/docs/configuration/outbound/wireguard.zh.md +++ b/docs/configuration/outbound/wireguard.zh.md @@ -1,3 +1,11 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [gso](#gso) + ### 结构 ```json @@ -8,6 +16,7 @@ "server": "127.0.0.1", "server_port": 1080, "system_interface": false, + "gso": false, "interface_name": "wg0", "local_address": [ "10.0.0.2/32" @@ -24,14 +33,6 @@ } ``` -!!! warning "" - - 默认安装不包含 WireGuard, 参阅 [安装](/zh/#_2)。 - -!!! warning "" - - 默认安装不包含被非特权 WireGuard 需要的 gVisor, 参阅 [安装](/zh/#_2)。 - ### 字段 #### server @@ -48,15 +49,25 @@ #### system_interface -使用系统 tun 支持。 +使用系统设备。 -需要特权且不能与系统接口冲突。 +需要特权且不能与已有系统接口冲突。 如果 gVisor 未包含在构建中,则强制执行。 #### interface_name -启用 `system_interface` 时的自定义设备名称。 +为系统接口自定义设备名称。 + +#### gso + +!!! question "自 sing-box 1.8.0 起" + +!!! quote "" + + 仅支持 Linux。 + +尝试启用通用分段卸载。 #### local_address diff --git a/docs/configuration/route/geoip.md b/docs/configuration/route/geoip.md index b966a292a5..8a2ed1d4a1 100644 --- a/docs/configuration/route/geoip.md +++ b/docs/configuration/route/geoip.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + ### Structure ```json diff --git a/docs/configuration/route/geoip.zh.md b/docs/configuration/route/geoip.zh.md index 3ee7042728..fb2481e220 100644 --- a/docs/configuration/route/geoip.zh.md +++ b/docs/configuration/route/geoip.zh.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。 + ### 结构 ```json diff --git a/docs/configuration/route/geosite.md b/docs/configuration/route/geosite.md index db700c6af1..0463057153 100644 --- a/docs/configuration/route/geosite.md +++ b/docs/configuration/route/geosite.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). + ### Structure ```json diff --git a/docs/configuration/route/geosite.zh.md b/docs/configuration/route/geosite.zh.md index bee81fbf71..eeee38ff17 100644 --- a/docs/configuration/route/geosite.zh.md +++ b/docs/configuration/route/geosite.zh.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。 + ### 结构 ```json diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 1ab85a4dbc..5deb44f5b7 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -1,5 +1,15 @@ +--- +icon: material/alert-decagram +--- + # Route +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -8,6 +18,7 @@ "geoip": {}, "geosite": {}, "rules": [], + "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, @@ -19,11 +30,20 @@ ### Fields -| Key | Format | -|-----------|-------------------------------| -| `geoip` | [GeoIP](./geoip/) | -| `geosite` | [Geosite](./geosite/) | -| `rules` | List of [Route Rule](./rule/) | +| Key | Format | +|-----------|----------------------| +| `geoip` | [GeoIP](./geoip/) | +| `geosite` | [Geosite](./geosite/) | + +#### rules + +List of [Route Rule](./rule/) + +#### rule_set + +!!! question "Since sing-box 1.8.0" + +List of [Rule Set](/configuration/rule-set/) #### final diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index 92e98e49c7..290268f4a7 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -1,5 +1,15 @@ +--- +icon: material/alert-decagram +--- + # 路由 +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -7,8 +17,8 @@ "route": { "geoip": {}, "geosite": {}, - "ip_rules": [], "rules": [], + "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, @@ -23,8 +33,17 @@ | 键 | 格式 | |-----------|-----------------------| | `geoip` | [GeoIP](./geoip/) | -| `geosite` | [GeoSite](./geosite/) | -| `rules` | 一组 [路由规则](./rule/) | +| `geosite` | [Geosite](./geosite/) | + +#### rule + +一组 [路由规则](./rule/) 。 + +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +一组 [规则集](/configuration/rule-set/)。 #### final diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 2342ce22fc..9bedef8675 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -1,3 +1,17 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.8.0" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-plus: [ip_is_private](#ip_is_private) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### Structure ```json @@ -46,10 +60,12 @@ "10.0.0.0/24", "192.168.0.1" ], + "source_ip_is_private": false, "ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], + "ip_is_private": false, "source_port": [ 12345 ], @@ -89,6 +105,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": "direct" }, @@ -114,12 +134,14 @@ !!! note "" The default rule uses the following matching logic: - (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr`) && + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr` || `ip_is_private`) && (`port` || `port_range`) && - (`source_geoip` || `source_ip_cidr`) && + (`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) && (`source_port` || `source_port_range`) && `other fields` + Additionally, included rule sets can be considered merged rather than as a single rule sub-item. + #### inbound Tags of [Inbound](/configuration/inbound/). @@ -160,23 +182,47 @@ Match domain using regular expression. #### geosite +!!! failure "Deprecated in sing-box 1.8.0" + + Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). + Match geosite. #### source_geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + Match source geoip. #### geoip +!!! failure "Deprecated in sing-box 1.8.0" + + GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + Match geoip. #### source_ip_cidr -Match source ip cidr. +Match source IP CIDR. + +#### ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public IP. #### ip_cidr -Match ip cidr. +Match IP CIDR. + +#### source_ip_is_private + +!!! question "Since sing-box 1.8.0" + +Match non-public source IP. #### source_port @@ -250,6 +296,18 @@ Match WiFi SSID. Match WiFi BSSID. +#### rule_set + +!!! question "Since sing-box 1.8.0" + +Match [Rule Set](/configuration/route/#rule_set). + +#### rule_set_ipcidr_match_source + +!!! question "Since sing-box 1.8.0" + +Make `ipcidr` in rule sets match the source IP. + #### invert Invert match result. @@ -276,4 +334,4 @@ Tag of the target outbound. ==Required== -Included default rules. +Included rules. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 7c49eb435a..0e6f989604 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -1,3 +1,17 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-plus: [rule_set](#rule_set) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [source_ip_is_private](#source_ip_is_private) + :material-plus: [ip_is_private](#ip_is_private) + :material-delete-clock: [source_geoip](#source_geoip) + :material-delete-clock: [geoip](#geoip) + :material-delete-clock: [geosite](#geosite) + ### 结构 ```json @@ -45,9 +59,11 @@ "source_ip_cidr": [ "10.0.0.0/24" ], + "source_ip_is_private": false, "ip_cidr": [ "10.0.0.0/24" ], + "ip_is_private": false, "source_port": [ 12345 ], @@ -87,6 +103,10 @@ "wifi_bssid": [ "00:00:00:00:00:00" ], + "rule_set": [ + "geoip-cn", + "geosite-cn" + ], "invert": false, "outbound": "direct" }, @@ -112,12 +132,14 @@ !!! note "" 默认规则使用以下匹配逻辑: - (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr`) && + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr` || `ip_is_private`) && (`port` || `port_range`) && - (`source_geoip` || `source_ip_cidr`) && + (`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) && (`source_port` || `source_port_range`) && `other fields` + 另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。 + #### inbound [入站](/zh/configuration/inbound/) 标签。 @@ -158,24 +180,48 @@ #### geosite -匹配 GeoSite。 +!!! failure "已在 sing-box 1.8.0 废弃" + + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。 + +匹配 Geosite。 #### source_geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。 + 匹配源 GeoIP。 #### geoip +!!! failure "已在 sing-box 1.8.0 废弃" + + GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。 + 匹配 GeoIP。 #### source_ip_cidr 匹配源 IP CIDR。 +#### source_ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开源 IP。 + #### ip_cidr 匹配 IP CIDR。 +#### ip_is_private + +!!! question "自 sing-box 1.8.0 起" + +匹配非公开 IP。 + #### source_port 匹配源端口。 @@ -248,6 +294,18 @@ 匹配 WiFi BSSID。 +#### rule_set + +!!! question "自 sing-box 1.8.0 起" + +匹配[规则集](/zh/configuration/route/#rule_set)。 + +#### rule_set_ipcidr_match_source + +!!! question "自 sing-box 1.8.0 起" + +使规则集中的 `ipcidr` 规则匹配源 IP。 + #### invert 反选匹配结果。 @@ -274,4 +332,4 @@ ==必填== -包括的默认规则。 \ No newline at end of file +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md new file mode 100644 index 0000000000..6ab62eb2e3 --- /dev/null +++ b/docs/configuration/rule-set/headless-rule.md @@ -0,0 +1,207 @@ +--- +icon: material/new-box +--- + +### Structure + +!!! question "Since sing-box 1.8.0" + +```json +{ + "rules": [ + { + "query_type": [ + "A", + "HTTPS", + 32768 + ], + "network": [ + "tcp" + ], + "domain": [ + "test.com" + ], + "domain_suffix": [ + ".cn" + ], + "domain_keyword": [ + "test" + ], + "domain_regex": [ + "^stun\\..+" + ], + "source_ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "source_port": [ + 12345 + ], + "source_port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "port": [ + 80, + 443 + ], + "port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "process_name": [ + "curl" + ], + "process_path": [ + "/usr/bin/curl" + ], + "package_name": [ + "com.termux" + ], + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], + "invert": false + }, + { + "type": "logical", + "mode": "and", + "rules": [], + "invert": false + } + ] +} +``` + +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + +### Default Fields + +!!! note "" + + The default rule uses the following matching logic: + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) && + (`port` || `port_range`) && + (`source_port` || `source_port_range`) && + `other fields` + +#### query_type + +DNS query type. Values can be integers or type name strings. + +#### network + +`tcp` or `udp`. + +#### domain + +Match full domain. + +#### domain_suffix + +Match domain suffix. + +#### domain_keyword + +Match domain using keyword. + +#### domain_regex + +Match domain using regular expression. + +#### source_ip_cidr + +Match source IP CIDR. + +#### ip_cidr + +!!! info "" + + `ip_cidr` is an alias for `source_ip_cidr` when the Rule Set is used in DNS rules or `rule_set_ipcidr_match_source` enabled in route rules. + +Match IP CIDR. + +#### source_port + +Match source port. + +#### source_port_range + +Match source port range. + +#### port + +Match port. + +#### port_range + +Match port range. + +#### process_name + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process name. + +#### process_path + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Match process path. + +#### package_name + +Match android package name. + +#### wifi_ssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi SSID. + +#### wifi_bssid + +!!! quote "" + + Only supported in graphical clients on Android and iOS. + +Match WiFi BSSID. + +#### invert + +Invert match result. + +### Logical Fields + +#### type + +`logical` + +#### mode + +==Required== + +`and` or `or` + +#### rules + +==Required== + +Included rules. diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md new file mode 100644 index 0000000000..5aff55b371 --- /dev/null +++ b/docs/configuration/rule-set/index.md @@ -0,0 +1,97 @@ +--- +icon: material/new-box +--- + +# Rule Set + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "type": "", + "tag": "", + "format": "", + + ... // Typed Fields +} +``` + +#### Local Structure + +```json +{ + "type": "local", + + ... + + "path": "" +} +``` + +#### Remote Structure + +!!! info "" + + Remote rule-set will be cached if `experimental.cache_file.enabled`. + +```json +{ + "type": "remote", + + ..., + + "url": "", + "download_detour": "", + "update_interval": "" +} +``` + +### Fields + +#### type + +==Required== + +Type of Rule Set, `local` or `remote`. + +#### tag + +==Required== + +Tag of Rule Set. + +#### format + +==Required== + +Format of Rule Set, `source` or `binary`. + +### Local Fields + +#### path + +==Required== + +File path of Rule Set. + +### Remote Fields + +#### url + +==Required== + +Download URL of Rule Set. + +#### download_detour + +Tag of the outbound to download rule-set. + +Default outbound will be used if empty. + +#### update_interval + +Update interval of Rule Set. + +`1d` will be used if empty. diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md new file mode 100644 index 0000000000..8e1934aec4 --- /dev/null +++ b/docs/configuration/rule-set/source-format.md @@ -0,0 +1,34 @@ +--- +icon: material/new-box +--- + +# Source Format + +!!! question "Since sing-box 1.8.0" + +### Structure + +```json +{ + "version": 1, + "rules": [] +} +``` + +### Compile + +Use `sing-box rule-set compile [--output .srs] .json` to compile source to binary rule-set. + +### Fields + +#### version + +==Required== + +Version of Rule Set, must be `1`. + +#### rules + +==Required== + +List of [Headless Rule](./headless-rule.md/). diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index c28314d9cb..a5c7bec4c2 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -1,3 +1,12 @@ +--- +icon: material/alert-decagram +--- + + +!!! quote "Changes in sing-box 1.8.0" + + :material-alert-decagram: [utls](#utls) + ### Inbound ```json @@ -198,10 +207,6 @@ The path to the server private key, in PEM format. ==Client only== -!!! warning "" - - uTLS is not included by default, see [Installation](./#installation). - !!! note "" uTLS is poorly maintained and the effect may be unproven, use at your own risk. @@ -210,7 +215,20 @@ uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resist Available fingerprint values: +!!! question "Since sing-box 1.8.0" + + :material-plus: chrome_psk + :material-plus: chrome_psk_shuffle + :material-plus: chrome_padding_psk_shuffle + :material-plus: chrome_pq + :material-plus: chrome_pq_psk + * chrome +* chrome_psk +* chrome_psk_shuffle +* chrome_padding_psk_shuffle +* chrome_pq +* chrome_pq_psk * firefox * edge * safari @@ -225,10 +243,6 @@ Chrome fingerprint will be used if empty. ### ECH Fields -!!! warning "" - - ECH is not included by default, see [Installation](./#installation). - ECH (Encrypted Client Hello) is a TLS extension that allows a client to encrypt the first part of its ClientHello message. @@ -277,10 +291,6 @@ If empty, load from DNS will be attempted. ### ACME Fields -!!! warning "" - - ACME is not included by default, see [Installation](./#installation). - #### domain List of domain. @@ -356,14 +366,6 @@ See [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/) for details ### Reality Fields -!!! warning "" - - reality server is not included by default, see [Installation](./#installation). - -!!! warning "" - - uTLS, which is required by reality client is not included by default, see [Installation](./#installation). - #### handshake ==Server only== diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index fcea5ba145..5a75945d15 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -1,3 +1,11 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.8.0 中的更改" + + :material-alert-decagram: [utls](#utls) + ### 入站 ```json @@ -190,10 +198,6 @@ TLS 版本值: ==仅客户端== -!!! warning "" - - 默认安装不包含 uTLS, 参阅 [安装](/zh/#_2)。 - !!! note "" uTLS 维护不善且其效果可能未经证实,使用风险自负。 @@ -202,7 +206,20 @@ uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻 可用的指纹值: +!!! question "自 sing-box 1.8.0 起" + + :material-plus: chrome_psk + :material-plus: chrome_psk_shuffle + :material-plus: chrome_padding_psk_shuffle + :material-plus: chrome_pq + :material-plus: chrome_pq_psk + * chrome +* chrome_psk +* chrome_psk_shuffle +* chrome_padding_psk_shuffle +* chrome_pq +* chrome_pq_psk * firefox * edge * safari @@ -217,14 +234,9 @@ uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻 ## ECH 字段 -!!! warning "" - - 默认安装不包含 ECH, 参阅 [安装](/zh/#_2)。 - ECH (Encrypted Client Hello) 是一个 TLS 扩展,它允许客户端加密其 ClientHello 的第一部分 信息。 - ECH 配置和密钥可以通过 `sing-box generate ech-keypair [--pq-signature-schemes-enabled]` 生成。 #### pq_signature_schemes_enabled @@ -270,10 +282,6 @@ ECH PEM 配置路径 ### ACME 字段 -!!! warning "" - - 默认安装不包含 ACME,参阅 [安装](/zh/#_2)。 - #### domain 一组域名。 @@ -345,14 +353,6 @@ ACME DNS01 验证字段。如果配置,将禁用其他验证方法。 ### Reality 字段 -!!! warning "" - - 默认安装不包含 reality 服务器,参阅 [安装](/zh/#_2)。 - -!!! warning "" - - 默认安装不包含被 reality 客户端需要的 uTLS, 参阅 [安装](/zh/#_2)。 - #### handshake ==仅服务器== diff --git a/docs/configuration/shared/v2ray-transport.md b/docs/configuration/shared/v2ray-transport.md index f8911fdbe7..afc332414b 100644 --- a/docs/configuration/shared/v2ray-transport.md +++ b/docs/configuration/shared/v2ray-transport.md @@ -142,10 +142,6 @@ It needs to be consistent with the server. } ``` -!!! warning "" - - QUIC is not included by default, see [Installation](./#installation). - !!! warning "Difference from v2ray-core" No additional encryption support: @@ -155,7 +151,7 @@ It needs to be consistent with the server. !!! note "" - standard gRPC has good compatibility but poor performance and is not included by default, see [Installation](./#installation). + standard gRPC has good compatibility but poor performance and is not included by default, see [Installation](/installation/build-from-source/#build-tags). ```json { diff --git a/docs/configuration/shared/v2ray-transport.zh.md b/docs/configuration/shared/v2ray-transport.zh.md index 91b740face..e5bd7de72e 100644 --- a/docs/configuration/shared/v2ray-transport.zh.md +++ b/docs/configuration/shared/v2ray-transport.zh.md @@ -135,10 +135,6 @@ HTTP 请求的额外标头 } ``` -!!! warning "" - - 默认安装不包含 QUIC, 参阅 [安装](/zh/#_2)。 - !!! warning "与 v2ray-core 的区别" 没有额外的加密支持: @@ -148,7 +144,7 @@ HTTP 请求的额外标头 !!! note "" - 默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/#_2)。 + 默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/installation/build-from-source/#_5)。 ```json { diff --git a/docs/deprecated.md b/docs/deprecated.md index 91a3b25e58..0613122e86 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -4,7 +4,37 @@ icon: material/delete-alert # Deprecated Feature List -### 1.6.0 +## 1.8.0 + +#### Cache file and related features in Clash API + +`cache_file` and related features in Clash API is migrated to independent `cache_file` options, +check [Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options). + +#### GeoIP + +GeoIP is deprecated and may be removed in the future. + +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, +check [Migration](/migration/#migrate-geoip-to-rule-sets). + +#### Geosite + +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, +check [Migration](/migration/#migrate-geosite-to-rule-sets). + +Geosite,即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案,存在着大量问题,包括缺少维护、规则不准确、管理困难。 + +## 1.6.0 The following features will be marked deprecated in 1.5.0 and removed entirely in 1.6.0. diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index f1125565f9..69ec4bdd7f 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -4,7 +4,34 @@ icon: material/delete-alert # 废弃功能列表 -### 1.6.0 +## 1.8.0 + +#### Clash API 中的 Cache file 及相关功能 + +Clash API 中的 `cache_file` 及相关功能已废弃且已迁移到独立的 `cache_file` 设置, +参阅 [迁移指南](/zh/migration/#clash-api)。 + +#### GeoIP + +GeoIP 已废弃且可能在不久的将来移除。 + +maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量绕过, +且现有的实现均存在内存使用大与管理困难的问题。 + +sing-box 1.8.0 引入了[规则集](/configuration/rule-set/), +可以完全替代 GeoIP, 参阅 [迁移指南](/zh/migration/#geoip)。 + +#### Geosite + +Geosite 已废弃且可能在不久的将来移除。 + +Geosite,即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案, +存在着包括缺少维护、规则不准确和管理困难内的大量问题。 + +sing-box 1.8.0 引入了[规则集](/configuration/rule-set/), +可以完全替代 Geosite,参阅 [迁移指南](/zh/migration/#geosite)。 + +## 1.6.0 下列功能已在 1.5.0 中标记为已弃用,并在 1.6.0 中完全删除。 diff --git a/docs/installation/build-from-source.md b/docs/installation/build-from-source.md index 9c9f6162f7..fc78d87f87 100644 --- a/docs/installation/build-from-source.md +++ b/docs/installation/build-from-source.md @@ -13,7 +13,17 @@ Before sing-box 1.4.0: Since sing-box 1.4.0: * Go 1.18.5 - ~ -* Go 1.20.0 - ~ if `with_quic` tag enabled +* Go 1.20.0 - ~ with tag `with_quic` enabled + +Since sing-box 1.5.0: + +* Go 1.18.5 - ~ +* Go 1.20.0 - ~ with tag `with_quic` or `with_ech` enabled + +Since sing-box 1.8.0: + +* Go 1.18.5 - ~ +* Go 1.20.0 - ~ with tag `with_quic`, `with_ech`, or `with_utls` enabled You can download and install Go from: https://go.dev/doc/install, latest version is recommended. @@ -23,7 +33,7 @@ You can download and install Go from: https://go.dev/doc/install, latest version make ``` -Or build and install binary to `GOBIN`: +Or build and install binary to `$GOBIN`: ```bash make install @@ -45,19 +55,17 @@ go build -tags "tag_a tag_b" ./cmd/sing-box | Build Tag | Enabled by default | Description | |------------------------------------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `with_quic` | ✔ | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server/), [Naive inbound](/configuration/inbound/naive/), [Hysteria Inbound](/configuration/inbound/hysteria/), [Hysteria Outbound](/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). | -| `with_grpc` | ✖️ | Build with standard gRPC support, see [V2Ray Transport#gRPC](/configuration/shared/v2ray-transport#grpc). | -| `with_dhcp` | ✔ | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server/). | -| `with_wireguard` | ✔ | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard/). | -| `with_ech` | ✔ | Build with TLS ECH extension support for TLS outbound, see [TLS](/configuration/shared/tls#ech). | -| `with_utls` | ✔ | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). | -| `with_reality_server` | ✔ | Build with reality TLS server support, see [TLS](/configuration/shared/tls/). | -| `with_acme` | ✔ | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). | -| `with_clash_api` | ✔ | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | -| `with_v2ray_api` | ✖️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | -| `with_gvisor` | ✔ | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). | -| `with_embedded_tor` (CGO required) | ✖️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). | -| `with_lwip` (CGO required) | ✖️ | Build with LWIP Tun stack support, see [Tun inbound](/configuration/inbound/tun#stack). | - +| `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server/), [Naive inbound](/configuration/inbound/naive/), [Hysteria Inbound](/configuration/inbound/hysteria/), [Hysteria Outbound](/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). | +| `with_grpc` | :material-close:️ | Build with standard gRPC support, see [V2Ray Transport#gRPC](/configuration/shared/v2ray-transport#grpc). | +| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server/). | +| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard/). | +| `with_ech` | :material-check: | Build with TLS ECH extension support for TLS outbound, see [TLS](/configuration/shared/tls#ech). | +| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). | +| `with_reality_server` | :material-check: | Build with reality TLS server support, see [TLS](/configuration/shared/tls/). | +| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). | +| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | +| `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | +| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). | +| `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). | It is not recommended to change the default build tag list unless you really know what you are adding. diff --git a/docs/installation/build-from-source.zh.md b/docs/installation/build-from-source.zh.md new file mode 100644 index 0000000000..a1a09b9312 --- /dev/null +++ b/docs/installation/build-from-source.zh.md @@ -0,0 +1,71 @@ +--- +icon: material/file-code +--- + +# 从源代码构建 + +## :material-graph: 要求 + +sing-box 1.4.0 前: + +* Go 1.18.5 - 1.20.x + +从 sing-box 1.4.0: + +* Go 1.18.5 - ~ +* Go 1.20.0 - ~ 如果启用构建标记 `with_quic` + +从 sing-box 1.5.0: + +* Go 1.18.5 - ~ +* Go 1.20.0 - ~ 如果启用构建标记 `with_quic` 或 `with_ech` + +从 sing-box 1.8.0: + +* Go 1.18.5 - ~ +* Go 1.20.0 - ~ 如果启用构建标记 `with_quic`、`with_ech` 或 `with_utls` + +您可以从 https://go.dev/doc/install 下载并安装 Go,推荐使用最新版本。 + +## :material-fast-forward: 快速开始 + +```bash +make +``` + +或者构建二进制文件并将其安装到 `$GOBIN`: + +```bash +make install +``` + +## :material-cog: 自定义构建 + +```bash +TAGS="tag_a tag_b" make +``` + +or + +```bash +go build -tags "tag_a tag_b" ./cmd/sing-box +``` + +## :material-folder-settings: 构建标记 + +| 构建标记 | 默认启动 | 说明 | +|------------------------------------|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server/), [Naive inbound](/configuration/inbound/naive/), [Hysteria Inbound](/configuration/inbound/hysteria/), [Hysteria Outbound](/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). | +| `with_grpc` | :material-close:️ | Build with standard gRPC support, see [V2Ray Transport#gRPC](/configuration/shared/v2ray-transport#grpc). | +| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server/). | +| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard/). | +| `with_ech` | :material-check: | Build with TLS ECH extension support for TLS outbound, see [TLS](/configuration/shared/tls#ech). | +| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). | +| `with_reality_server` | :material-check: | Build with reality TLS server support, see [TLS](/configuration/shared/tls/). | +| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). | +| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | +| `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | +| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). | +| `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). | + +除非您确实知道您正在启用什么,否则不建议更改默认构建标签列表。 diff --git a/docs/installation/docker.zh.md b/docs/installation/docker.zh.md new file mode 100644 index 0000000000..ecc66e3a0b --- /dev/null +++ b/docs/installation/docker.zh.md @@ -0,0 +1,31 @@ +--- +icon: material/docker +--- + +# Docker + +## :material-console: 命令 + +```bash +docker run -d \ + -v /etc/sing-box:/etc/sing-box/ \ + --name=sing-box \ + --restart=always \ + ghcr.io/sagernet/sing-box \ + -D /var/lib/sing-box \ + -C /etc/sing-box/ run +``` + +## :material-box-shadow: Compose + +```yaml +version: "3.8" +services: + sing-box: + image: ghcr.io/sagernet/sing-box + container_name: sing-box + restart: always + volumes: + - /etc/sing-box:/etc/sing-box/ + command: -D /var/lib/sing-box -C /etc/sing-box/ run +``` diff --git a/docs/installation/package-manager.md b/docs/installation/package-manager.md index 6ce1669d02..c605ffb0c2 100644 --- a/docs/installation/package-manager.md +++ b/docs/installation/package-manager.md @@ -28,18 +28,18 @@ icon: material/package === ":material-linux: Linux" - | Type | Platform | Link | Command | Actively maintained | - |----------|--------------------|---------------------|------------------------------|---------------------| - | AUR | (Linux) Arch Linux | [sing-box][aur] ᴬᵁᴿ | `? -S sing-box` | :material-check: | - | nixpkgs | (Linux) NixOS | [sing-box][nixpkgs] | `nix-env -iA nixos.sing-box` | :material-check: | - | Homebrew | macOS / Linux | [sing-box][brew] | `brew install sing-box` | :material-check: | - | Alpine | (Linux) Alpine | [sing-box][alpine] | `apk add sing-box` | :material-alert: | + | Type | Platform | Link | Command | Actively maintained | + |----------|---------------|-------------------------|------------------------------|---------------------| + | APK | Alpine | [sing-box][alpine] | `apk add sing-box` | :material-check: | + | AUR | Arch Linux | [sing-box][aur] ᴬᵁᴿ | `? -S sing-box` | :material-check: | + | nixpkgs | NixOS | [sing-box][nixpkgs] | `nix-env -iA nixos.sing-box` | :material-check: | + | Homebrew | macOS / Linux | [sing-box][brew] | `brew install sing-box` | :material-check: | === ":material-apple: macOS" - | Type | Platform | Link | Command | Actively maintained | - |----------|---------------|------------------|-------------------------|---------------------| - | Homebrew | macOS / Linux | [sing-box][brew] | `brew install sing-box` | :material-check: | + | Type | Platform | Link | Command | Actively maintained | + |----------|----------|------------------|-------------------------|---------------------| + | Homebrew | macOS | [sing-box][brew] | `brew install sing-box` | :material-check: | === ":material-microsoft-windows: Windows" @@ -55,6 +55,12 @@ icon: material/package |------------|--------------------|---------------------|------------------------------|---------------------| | Termux | Android | [sing-box][termux] | `pkg add sing-box` | :material-check: | +=== ":material-freebsd: FreeBSD" + + | Type | Platform | Link | Command | Actively maintained | + |------------|----------|-------------------|------------------------|---------------------| + | FreshPorts | FreeBSD | [sing-box][ports] | `pkg install sing-box` | :material-alert: | + ## :material-book-multiple: Service Management For Linux systems with [systemd][systemd], usually the installation already includes a sing-box service, @@ -77,14 +83,20 @@ you can manage the service using the following command: [nixpkgs]: https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/tools/networking/sing-box/default.nix -[termux]: https://github.com/termux/termux-packages/tree/master/packages/sing-box - [brew]: https://formulae.brew.sh/formula/sing-box +[openwrt]: https://github.com/openwrt/packages/tree/master/net/sing-box + +[immortalwrt]: https://github.com/immortalwrt/packages/tree/master/net/sing-box + [choco]: https://chocolatey.org/packages/sing-box [scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/sing-box.json [winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/SagerNet/sing-box -[systemd]: https://systemd.io/ \ No newline at end of file +[termux]: https://github.com/termux/termux-packages/tree/master/packages/sing-box + +[ports]: https://www.freshports.org/net/sing-box + +[systemd]: https://systemd.io/ diff --git a/docs/installation/package-manager.zh.md b/docs/installation/package-manager.zh.md new file mode 100644 index 0000000000..e3c406302d --- /dev/null +++ b/docs/installation/package-manager.zh.md @@ -0,0 +1,98 @@ +--- +icon: material/package +--- + +# 包管理器 + +## :material-download-box: 手动安装 + +=== ":material-debian: Debian / DEB" + + ```bash + bash <(curl -fsSL https://sing-box.app/deb-install.sh) + ``` + +=== ":material-redhat: Redhat / RPM" + + ```bash + bash <(curl -fsSL https://sing-box.app/rpm-install.sh) + ``` + +=== ":simple-archlinux: Archlinux / PKG" + + ```bash + bash <(curl -fsSL https://sing-box.app/arch-install.sh) + ``` + +## :material-book-lock-open: 托管安装 + +=== ":material-linux: Linux" + + | 类型 | 平台 | 链接 | 命令 | 活跃维护 | + |----------|------------|---------------------|------------------------------|------------------| + | Alpine | Alpine | [sing-box][alpine] | `apk add sing-box` | :material-check: | + | AUR | Arch Linux | [sing-box][aur] ᴬᵁᴿ | `? -S sing-box` | :material-check: | + | nixpkgs | NixOS | [sing-box][nixpkgs] | `nix-env -iA nixos.sing-box` | :material-check: | + | Homebrew | Linux | [sing-box][brew] | `brew install sing-box` | :material-check: | + +=== ":material-apple: macOS" + + | 类型 | 平台 | 链接 | 命令 | 活跃维护 | + |----------|-------|------------------|-------------------------|------------------| + | Homebrew | macOS | [sing-box][brew] | `brew install sing-box` | :material-check: | + +=== ":material-microsoft-windows: Windows" + + | 类型 | 平台 | 链接 | 命令 | 活跃维护 | + |------------|---------|--------------------|---------------------------|------------------| + | Scoop | Windows | [sing-box][scoop] | `scoop install sing-box` | :material-check: | + | Chocolatey | Windows | [sing-box][choco] | `choco install sing-box` | :material-check: | + | winget | Windows | [sing-box][winget] | `winget install sing-box` | :material-alert: | + +=== ":material-android: Android" + + | 类型 | 平台 | 链接 | 命令 | 活跃维护 | + |--------|---------|--------------------|--------------------|------------------| + | Termux | Android | [sing-box][termux] | `pkg add sing-box` | :material-check: | + +=== ":material-freebsd: FreeBSD" + + | 类型 | 平台 | 链接 | 命令 | 活跃维护 | + |------------|---------|-------------------|------------------------|------------------| + | FreshPorts | FreeBSD | [sing-box][ports] | `pkg install sing-box` | :material-alert: | + +## :material-book-multiple: 服务管理 + +对于带有 [systemd][systemd] 的 Linux 系统,通常安装已经包含 sing-box 服务, +您可以使用以下命令管理服务: + +| 行动 | 命令 | +|------|-----------------------------------------------| +| 启用 | `sudo systemctl enable sing-box` | +| 禁用 | `sudo systemctl disable sing-box` | +| 启动 | `sudo systemctl start sing-box` | +| 停止 | `sudo systemctl stop sing-box` | +| 强行停止 | `sudo systemctl kill sing-box` | +| 重新启动 | `sudo systemctl restart sing-box` | +| 查看日志 | `sudo journalctl -u sing-box --output cat -e` | +| 实时日志 | `sudo journalctl -u sing-box --output cat -f` | + +[alpine]: https://pkgs.alpinelinux.org/packages?name=sing-box + +[aur]: https://aur.archlinux.org/packages/sing-box + +[nixpkgs]: https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/tools/networking/sing-box/default.nix + +[brew]: https://formulae.brew.sh/formula/sing-box + +[choco]: https://chocolatey.org/packages/sing-box + +[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/sing-box.json + +[winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/SagerNet/sing-box + +[termux]: https://github.com/termux/termux-packages/tree/master/packages/sing-box + +[ports]: https://www.freshports.org/net/sing-box + +[systemd]: https://systemd.io/ diff --git a/docs/manual/proxy/client.md b/docs/manual/proxy/client.md index 675c2af095..4e400e5353 100644 --- a/docs/manual/proxy/client.md +++ b/docs/manual/proxy/client.md @@ -284,4 +284,304 @@ flowchart TB "auto_detect_interface": true } } + ``` + +### Traffic bypass usage for Chinese users + +=== ":material-dns: DNS rules" + + !!! info + + DNS rules are optional if FakeIP is used. + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "address": "tls://8.8.8.8" + }, + { + "tag": "local", + "address": "223.5.5.5", + "detour": "direct" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + }, + { + "clash_mode": "Direct", + "server": "local" + }, + { + "clash_mode": "Global", + "server": "google" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "geosite": "geolocation-!cn", + "invert": true + }, + { + "geosite": "cn", + } + ], + "server": "local" + } + ] + } + } + ``` + +=== ":material-dns: DNS rules (1.8.0+)" + + !!! info + + DNS rules are optional if FakeIP is used. + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "address": "tls://8.8.8.8" + }, + { + "tag": "local", + "address": "223.5.5.5", + "detour": "direct" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + }, + { + "clash_mode": "Direct", + "server": "local" + }, + { + "clash_mode": "Global", + "server": "google" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": "geosite-cn", + } + ], + "server": "local" + } + ] + }, + "route": { + "rule_set": [ + { + "type": "remote", + "tag": "geosite-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-geolocation-!cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" + } + ] + } + } + ``` + +=== ":material-router-network: Route rules" + + ```json + { + "outbounds": [ + { + "type": "direct", + "tag": "direct" + }, + { + "type": "block", + "tag": "block" + } + ], + "route": { + "rules": [ + { + "type": "logical", + "mode": "or", + "rules": [ + { + "protocol": "dns" + }, + { + "port": 53 + } + ], + "outbound": "dns" + }, + { + "geoip": "private", + "outbound": "direct" + }, + { + "clash_mode": "Direct", + "outbound": "direct" + }, + { + "clash_mode": "Global", + "outbound": "default" + }, + { + "type": "logical", + "mode": "or", + "rules": [ + { + "port": 853 + }, + { + "network": "udp", + "port": 443 + }, + { + "protocol": "stun" + } + ], + "outbound": "block" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "geosite": "geolocation-!cn", + "invert": true + }, + { + "geosite": "cn", + "geoip": "cn" + } + ], + "outbound": "direct" + } + ] + } + } + ``` + +=== ":material-router-network: Route rules (1.8.0+)" + + ```json + { + "outbounds": [ + { + "type": "direct", + "tag": "direct" + }, + { + "type": "block", + "tag": "block" + } + ], + "route": { + "rules": [ + { + "type": "logical", + "mode": "or", + "rules": [ + { + "protocol": "dns" + }, + { + "port": 53 + } + ], + "outbound": "dns" + }, + { + "ip_is_private": true, + "outbound": "direct" + }, + { + "clash_mode": "Direct", + "outbound": "direct" + }, + { + "clash_mode": "Global", + "outbound": "default" + }, + { + "type": "logical", + "mode": "or", + "rules": [ + { + "port": 853 + }, + { + "network": "udp", + "port": 443 + }, + { + "protocol": "stun" + } + ], + "outbound": "block" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": [ + "geoip-cn", + "geosite-cn" + ] + } + ], + "outbound": "direct" + } + ], + "rule_set": [ + { + "type": "remote", + "tag": "geoip-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs" + }, + { + "type": "remote", + "tag": "geosite-geolocation-!cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" + } + ] + } + } ``` \ No newline at end of file diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000000..44ddd8337d --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,189 @@ +--- +icon: material/arrange-bring-forward +--- + +## 1.8.0 + +### :material-close-box: Migrate cache file from Clash API to independent options + +!!! info "References" + + [Clash API](/configuration/experimental/clash-api/) / + [Cache File](/configuration/experimental/cache-file/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "experimental": { + "clash_api": { + "cache_file": "cache.db", // default value + "cahce_id": "my_profile2", + "store_mode": true, + "store_selected": true, + "store_fakeip": true + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "experimental" : { + "cache_file": { + "enabled": true, + "path": "cache.db", // default value + "cache_id": "my_profile2", + "store_fakeip": true + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate GeoIP to rule sets + +!!! info "References" + + [GeoIP](/configuration/route/geoip/) / + [Route](/configuration/route/) / + [Route Rule](/configuration/route/rule/) / + [DNS Rule](/configuration/dns/rule/) / + [Rule Set](/configuration/rule-set/) + +!!! tip + + `sing-box geoip` commands can help you convert custom GeoIP into rule sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geoip": "private", + "outbound": "direct" + }, + { + "geoip": "cn", + "outbound": "direct" + }, + { + "source_geoip": "cn", + "outbound": "block" + } + ], + "geoip": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "ip_is_private": true, + "outbound": "direct" + }, + { + "rule_set": "geoip-cn", + "outbound": "direct" + }, + { + "rule_set": "geoip-us", + "rule_set_ipcidr_match_source": true, + "outbound": "block" + } + ], + "rule_set": [ + { + "tag": "geoip-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", + "download_detour": "proxy" + }, + { + "tag": "geoip-us", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` + +### :material-checkbox-intermediate: Migrate Geosite to rule sets + +!!! info "References" + + [Geosite](/configuration/route/geosite/) / + [Route](/configuration/route/) / + [Route Rule](/configuration/route/rule/) / + [DNS Rule](/configuration/dns/rule/) / + [Rule Set](/configuration/rule-set/) + +!!! tip + + `sing-box geosite` commands can help you convert custom Geosite into rule sets. + +=== ":material-card-remove: Deprecated" + + ```json + { + "route": { + "rules": [ + { + "geosite": "cn", + "outbound": "direct" + } + ], + "geosite": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geosite-cn", + "outbound": "direct" + } + ], + "rule_set": [ + { + "tag": "geosite-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` \ No newline at end of file diff --git a/docs/migration.zh.md b/docs/migration.zh.md new file mode 100644 index 0000000000..0422833d04 --- /dev/null +++ b/docs/migration.zh.md @@ -0,0 +1,189 @@ +--- +icon: material/arrange-bring-forward +--- + +## 1.8.0 + +### :material-close-box: 将缓存文件从 Clash API 迁移到独立选项 + +!!! info "参考" + + [Clash API](/zh/configuration/experimental/clash-api/) / + [Cache File](/zh/configuration/experimental/cache-file/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "experimental": { + "clash_api": { + "cache_file": "cache.db", // 默认值 + "cahce_id": "my_profile2", + "store_mode": true, + "store_selected": true, + "store_fakeip": true + } + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "experimental" : { + "cache_file": { + "enabled": true, + "path": "cache.db", // 默认值 + "cache_id": "my_profile2", + "store_fakeip": true + } + } + } + ``` + +### :material-checkbox-intermediate: 迁移 GeoIP 到规则集 + +!!! info "参考" + + [GeoIP](/zh/configuration/route/geoip/) / + [路由](/zh/configuration/route/) / + [路由规则](/zh/configuration/route/rule/) / + [DNS 规则](/zh/configuration/dns/rule/) / + [规则集](/zh/configuration/rule-set/) + +!!! tip + + `sing-box geoip` 命令可以帮助您将自定义 GeoIP 转换为规则集。 + +=== ":material-card-remove: 弃用的" + + ```json + { + "route": { + "rules": [ + { + "geoip": "private", + "outbound": "direct" + }, + { + "geoip": "cn", + "outbound": "direct" + }, + { + "source_geoip": "cn", + "outbound": "block" + } + ], + "geoip": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "route": { + "rules": [ + { + "ip_is_private": true, + "outbound": "direct" + }, + { + "rule_set": "geoip-cn", + "outbound": "direct" + }, + { + "rule_set": "geoip-us", + "rule_set_ipcidr_match_source": true, + "outbound": "block" + } + ], + "rule_set": [ + { + "tag": "geoip-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", + "download_detour": "proxy" + }, + { + "tag": "geoip-us", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` + +### :material-checkbox-intermediate: 迁移 Geosite 到规则集 + +!!! info "参考" + + [Geosite](/zh/configuration/route/geosite/) / + [路由](/zh/configuration/route/) / + [路由规则](/zh/configuration/route/rule/) / + [DNS 规则](/zh/configuration/dns/rule/) / + [规则集](/zh/configuration/rule-set/) + +!!! tip + + `sing-box geosite` 命令可以帮助您将自定义 Geosite 转换为规则集。 + +=== ":material-card-remove: 弃用的" + + ```json + { + "route": { + "rules": [ + { + "geosite": "cn", + "outbound": "direct" + } + ], + "geosite": { + "download_detour": "proxy" + } + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "route": { + "rules": [ + { + "rule_set": "geosite-cn", + "outbound": "direct" + } + ], + "rule_set": [ + { + "tag": "geosite-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", + "download_detour": "proxy" + } + ] + }, + "experimental": { + "cache_file": { + "enabled": true // required to save Rule Set cache + } + } + } + ``` \ No newline at end of file diff --git a/experimental/clashapi/cachefile/cache.go b/experimental/cachefile/cache.go similarity index 71% rename from experimental/clashapi/cachefile/cache.go rename to experimental/cachefile/cache.go index 0911829749..43b8456215 100644 --- a/experimental/clashapi/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/bbolt" bboltErrors "github.com/sagernet/bbolt/errors" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service/filemanager" @@ -21,21 +22,27 @@ var ( bucketSelected = []byte("selected") bucketExpand = []byte("group_expand") bucketMode = []byte("clash_mode") + bucketRuleSet = []byte("rule_set") bucketNameList = []string{ string(bucketSelected), string(bucketExpand), string(bucketMode), + string(bucketRuleSet), } cacheIDDefault = []byte("default") ) -var _ adapter.ClashCacheFile = (*CacheFile)(nil) +var _ adapter.CacheFile = (*CacheFile)(nil) type CacheFile struct { + ctx context.Context + path string + cacheID []byte + storeFakeIP bool + DB *bbolt.DB - cacheID []byte saveAccess sync.RWMutex saveDomain map[netip.Addr]string saveAddress4 map[string]netip.Addr @@ -43,7 +50,29 @@ type CacheFile struct { saveMetadataTimer *time.Timer } -func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) { +func New(ctx context.Context, options option.CacheFileOptions) *CacheFile { + var path string + if options.Path != "" { + path = options.Path + } else { + path = "cache.db" + } + var cacheIDBytes []byte + if options.CacheID != "" { + cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...) + } + return &CacheFile{ + ctx: ctx, + path: filemanager.BasePath(ctx, path), + cacheID: cacheIDBytes, + storeFakeIP: options.StoreFakeIP, + saveDomain: make(map[netip.Addr]string), + saveAddress4: make(map[string]netip.Addr), + saveAddress6: make(map[string]netip.Addr), + } +} + +func (c *CacheFile) start() error { const fileMode = 0o666 options := bbolt.Options{Timeout: time.Second} var ( @@ -51,7 +80,7 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) err error ) for i := 0; i < 10; i++ { - db, err = bbolt.Open(path, fileMode, &options) + db, err = bbolt.Open(c.path, fileMode, &options) if err == nil { break } @@ -59,23 +88,20 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) continue } if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) { - rmErr := os.Remove(path) + rmErr := os.Remove(c.path) if rmErr != nil { - return nil, err + return err } } time.Sleep(100 * time.Millisecond) } if err != nil { - return nil, err + return err } - err = filemanager.Chown(ctx, path) + err = filemanager.Chown(c.ctx, c.path) if err != nil { - return nil, E.Cause(err, "platform chown") - } - var cacheIDBytes []byte - if cacheID != "" { - cacheIDBytes = append([]byte{0}, []byte(cacheID)...) + db.Close() + return E.Cause(err, "platform chown") } err = db.Batch(func(tx *bbolt.Tx) error { return tx.ForEach(func(name []byte, b *bbolt.Bucket) error { @@ -97,15 +123,30 @@ func Open(ctx context.Context, path string, cacheID string) (*CacheFile, error) }) }) if err != nil { - return nil, err + db.Close() + return err } - return &CacheFile{ - DB: db, - cacheID: cacheIDBytes, - saveDomain: make(map[netip.Addr]string), - saveAddress4: make(map[string]netip.Addr), - saveAddress6: make(map[string]netip.Addr), - }, nil + c.DB = db + return nil +} + +func (c *CacheFile) PreStart() error { + return c.start() +} + +func (c *CacheFile) Start() error { + return nil +} + +func (c *CacheFile) Close() error { + if c.DB == nil { + return nil + } + return c.DB.Close() +} + +func (c *CacheFile) StoreFakeIP() bool { + return c.storeFakeIP } func (c *CacheFile) LoadMode() string { @@ -219,6 +260,35 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { }) } -func (c *CacheFile) Close() error { - return c.DB.Close() +func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedRuleSet { + var savedSet adapter.SavedRuleSet + err := c.DB.View(func(t *bbolt.Tx) error { + bucket := c.bucket(t, bucketRuleSet) + if bucket == nil { + return os.ErrNotExist + } + setBinary := bucket.Get([]byte(tag)) + if len(setBinary) == 0 { + return os.ErrInvalid + } + return savedSet.UnmarshalBinary(setBinary) + }) + if err != nil { + return nil + } + return &savedSet +} + +func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedRuleSet) error { + return c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := c.createBucket(t, bucketRuleSet) + if err != nil { + return err + } + setBinary, err := set.MarshalBinary() + if err != nil { + return err + } + return bucket.Put([]byte(tag), setBinary) + }) } diff --git a/experimental/clashapi/cachefile/fakeip.go b/experimental/cachefile/fakeip.go similarity index 99% rename from experimental/clashapi/cachefile/fakeip.go rename to experimental/cachefile/fakeip.go index 2242342a36..e998ebb859 100644 --- a/experimental/clashapi/cachefile/fakeip.go +++ b/experimental/cachefile/fakeip.go @@ -25,7 +25,7 @@ func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata { err := c.DB.Batch(func(tx *bbolt.Tx) error { bucket := tx.Bucket(bucketFakeIP) if bucket == nil { - return nil + return os.ErrNotExist } metadataBinary := bucket.Get(keyMetadata) if len(metadataBinary) == 0 { diff --git a/experimental/clashapi.go b/experimental/clashapi.go index 894d40a74a..805fbd5be7 100644 --- a/experimental/clashapi.go +++ b/experimental/clashapi.go @@ -5,6 +5,7 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" @@ -27,24 +28,37 @@ func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.O func CalculateClashModeList(options option.Options) []string { var clashMode []string - for _, dnsRule := range common.PtrValueOrDefault(options.DNS).Rules { - if dnsRule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, dnsRule.DefaultOptions.ClashMode) { - clashMode = append(clashMode, dnsRule.DefaultOptions.ClashMode) - } - for _, defaultRule := range dnsRule.LogicalOptions.Rules { - if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) { - clashMode = append(clashMode, defaultRule.ClashMode) + clashMode = append(clashMode, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...) + clashMode = append(clashMode, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...) + clashMode = common.FilterNotDefault(common.Uniq(clashMode)) + return clashMode +} + +func extraClashModeFromRule(rules []option.Rule) []string { + var clashMode []string + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if rule.DefaultOptions.ClashMode != "" { + clashMode = append(clashMode, rule.DefaultOptions.ClashMode) } + case C.RuleTypeLogical: + clashMode = append(clashMode, extraClashModeFromRule(rule.LogicalOptions.Rules)...) } } - for _, rule := range common.PtrValueOrDefault(options.Route).Rules { - if rule.DefaultOptions.ClashMode != "" && !common.Contains(clashMode, rule.DefaultOptions.ClashMode) { - clashMode = append(clashMode, rule.DefaultOptions.ClashMode) - } - for _, defaultRule := range rule.LogicalOptions.Rules { - if defaultRule.ClashMode != "" && !common.Contains(clashMode, defaultRule.ClashMode) { - clashMode = append(clashMode, defaultRule.ClashMode) + return clashMode +} + +func extraClashModeFromDNSRule(rules []option.DNSRule) []string { + var clashMode []string + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if rule.DefaultOptions.ClashMode != "" { + clashMode = append(clashMode, rule.DefaultOptions.ClashMode) } + case C.RuleTypeLogical: + clashMode = append(clashMode, extraClashModeFromDNSRule(rule.LogicalOptions.Rules)...) } } return clashMode diff --git a/experimental/clashapi/api_meta.go b/experimental/clashapi/api_meta.go index 876f98695d..29add8ae94 100644 --- a/experimental/clashapi/api_meta.go +++ b/experimental/clashapi/api_meta.go @@ -6,8 +6,8 @@ import ( "net/http" "time" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" + "github.com/sagernet/sing/common/json" "github.com/sagernet/ws" "github.com/sagernet/ws/wsutil" diff --git a/experimental/clashapi/api_meta_group.go b/experimental/clashapi/api_meta_group.go index 763d3801b7..396dee7f60 100644 --- a/experimental/clashapi/api_meta_group.go +++ b/experimental/clashapi/api_meta_group.go @@ -9,11 +9,11 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/badjson" "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/batch" + "github.com/sagernet/sing/common/json/badjson" "github.com/go-chi/chi/v5" "github.com/go-chi/render" diff --git a/experimental/clashapi/cache.go b/experimental/clashapi/cache.go index 7582fde5ae..9c088a82f7 100644 --- a/experimental/clashapi/cache.go +++ b/experimental/clashapi/cache.go @@ -1,23 +1,26 @@ package clashapi import ( + "context" "net/http" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/service" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) -func cacheRouter(router adapter.Router) http.Handler { +func cacheRouter(ctx context.Context) http.Handler { r := chi.NewRouter() - r.Post("/fakeip/flush", flushFakeip(router)) + r.Post("/fakeip/flush", flushFakeip(ctx)) return r } -func flushFakeip(router adapter.Router) func(w http.ResponseWriter, r *http.Request) { +func flushFakeip(ctx context.Context) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if cacheFile := router.ClashServer().CacheFile(); cacheFile != nil { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile != nil { err := cacheFile.FakeIPReset() if err != nil { render.Status(r, http.StatusInternalServerError) diff --git a/experimental/clashapi/connections.go b/experimental/clashapi/connections.go index 042bdd3645..c9471207e8 100644 --- a/experimental/clashapi/connections.go +++ b/experimental/clashapi/connections.go @@ -7,8 +7,8 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" + "github.com/sagernet/sing/common/json" "github.com/sagernet/ws" "github.com/sagernet/ws/wsutil" diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go index 050efd8d17..7a807c1fae 100644 --- a/experimental/clashapi/proxies.go +++ b/experimental/clashapi/proxies.go @@ -9,12 +9,12 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/badjson" "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json/badjson" N "github.com/sagernet/sing/common/network" "github.com/go-chi/chi/v5" @@ -100,8 +100,10 @@ func getProxies(server *Server, router adapter.Router) func(w http.ResponseWrite allProxies = append(allProxies, detour.Tag()) } - defaultTag := router.DefaultOutbound(N.NetworkTCP).Tag() - if defaultTag == "" { + var defaultTag string + if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + defaultTag = defaultOutbound.Tag() + } else { defaultTag = allProxies[0] } diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 6a3d6f66f7..1eec8448af 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -11,17 +11,16 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" - "github.com/sagernet/sing-box/experimental/clashapi/cachefile" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "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" "github.com/sagernet/sing/service/filemanager" @@ -49,12 +48,6 @@ type Server struct { mode string modeList []string modeUpdateHook chan<- struct{} - storeMode bool - storeSelected bool - storeFakeIP bool - cacheFilePath string - cacheID string - cacheFile adapter.ClashCacheFile externalController bool externalUI string @@ -76,9 +69,6 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ trafficManager: trafficManager, modeList: options.ModeList, externalController: options.ExternalController != "", - storeMode: options.StoreMode, - storeSelected: options.StoreSelected, - storeFakeIP: options.StoreFakeIP, externalUIDownloadURL: options.ExternalUIDownloadURL, externalUIDownloadDetour: options.ExternalUIDownloadDetour, } @@ -94,18 +84,10 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ server.modeList = append([]string{defaultMode}, server.modeList...) } server.mode = defaultMode - if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.ExternalController == "" { - cachePath := os.ExpandEnv(options.CacheFile) - if cachePath == "" { - cachePath = "cache.db" - } - if foundPath, loaded := C.FindPath(cachePath); loaded { - cachePath = foundPath - } else { - cachePath = filemanager.BasePath(ctx, cachePath) - } - server.cacheFilePath = cachePath - server.cacheID = options.CacheID + //goland:noinspection GoDeprecation + //nolint:staticcheck + if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" { + return nil, E.New("cache_file and related fields in Clash API is deprecated in sing-box 1.8.0, use experimental.cache_file instead.") } cors := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, @@ -128,7 +110,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/script", scriptRouter()) r.Mount("/profile", profileRouter()) - r.Mount("/cache", cacheRouter(router)) + r.Mount("/cache", cacheRouter(ctx)) r.Mount("/dns", dnsRouter(router)) server.setupMetaAPI(r) @@ -147,19 +129,13 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ } func (s *Server) PreStart() error { - if s.cacheFilePath != "" { - cacheFile, err := cachefile.Open(s.ctx, s.cacheFilePath, s.cacheID) - if err != nil { - return E.Cause(err, "open cache file") - } - s.cacheFile = cacheFile - if s.storeMode { - mode := s.cacheFile.LoadMode() - if common.Any(s.modeList, func(it string) bool { - return strings.EqualFold(it, mode) - }) { - s.mode = mode - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + mode := cacheFile.LoadMode() + if common.Any(s.modeList, func(it string) bool { + return strings.EqualFold(it, mode) + }) { + s.mode = mode } } return nil @@ -187,7 +163,6 @@ func (s *Server) Close() error { return common.Close( common.PtrOrNil(s.httpServer), s.trafficManager, - s.cacheFile, s.urlTestHistory, ) } @@ -224,8 +199,9 @@ func (s *Server) SetMode(newMode string) { } } s.router.ClearDNSCache() - if s.storeMode { - err := s.cacheFile.StoreMode(newMode) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreMode(newMode) if err != nil { s.logger.Error(E.Cause(err, "save mode")) } @@ -233,18 +209,6 @@ func (s *Server) SetMode(newMode string) { s.logger.Info("updated mode: ", newMode) } -func (s *Server) StoreSelected() bool { - return s.storeSelected -} - -func (s *Server) StoreFakeIP() bool { - return s.storeFakeIP -} - -func (s *Server) CacheFile() adapter.ClashCacheFile { - return s.cacheFile -} - func (s *Server) HistoryStorage() *urltest.HistoryStorage { return s.urlTestHistory } diff --git a/experimental/clashapi/server_resources.go b/experimental/clashapi/server_resources.go index ad36641e05..d6d22b5390 100644 --- a/experimental/clashapi/server_resources.go +++ b/experimental/clashapi/server_resources.go @@ -51,7 +51,11 @@ func (s *Server) downloadExternalUI() error { } detour = outbound } else { - detour = s.router.DefaultOutbound(N.NetworkTCP) + outbound, err := s.router.DefaultOutbound(N.NetworkTCP) + if err != nil { + return err + } + detour = outbound } httpClient := &http.Client{ Transport: &http.Transport{ diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go index 3dc5a367e7..4e635d1257 100644 --- a/experimental/clashapi/trafficontrol/tracker.go +++ b/experimental/clashapi/trafficontrol/tracker.go @@ -1,7 +1,6 @@ package trafficontrol import ( - "encoding/json" "net" "net/netip" "time" @@ -10,6 +9,7 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/json" N "github.com/sagernet/sing/common/network" "github.com/gofrs/uuid/v5" @@ -94,7 +94,9 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad var chain []string var next string if rule == nil { - next = router.DefaultOutbound(N.NetworkTCP).Tag() + if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + next = defaultOutbound.Tag() + } } else { next = rule.Outbound() } @@ -181,7 +183,9 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route var chain []string var next string if rule == nil { - next = router.DefaultOutbound(N.NetworkUDP).Tag() + if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil { + next = defaultOutbound.Tag() + } } else { next = rule.Outbound() } diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go index 934820889e..2fc69b98b4 100644 --- a/experimental/libbox/command_group.go +++ b/experimental/libbox/command_group.go @@ -159,11 +159,7 @@ func readGroups(reader io.Reader) (OutboundGroupIterator, error) { func writeGroups(writer io.Writer, boxService *BoxService) error { historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx) - var cacheFile adapter.ClashCacheFile - if clashServer := boxService.instance.Router().ClashServer(); clashServer != nil { - cacheFile = clashServer.CacheFile() - } - + cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx) outbounds := boxService.instance.Router().Outbounds() var iGroups []adapter.OutboundGroup for _, it := range outbounds { @@ -288,16 +284,15 @@ func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return writeError(conn, E.New("service not ready")) } - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - err = cacheFile.StoreGroupExpand(groupTag, isExpand) - if err != nil { - return writeError(conn, err) - } + cacheFile := service.FromContext[adapter.CacheFile](serviceNow.ctx) + if cacheFile != nil { + err = cacheFile.StoreGroupExpand(groupTag, isExpand) + if err != nil { + return writeError(conn, err) } } return writeError(conn, nil) diff --git a/experimental/libbox/command_urltest.go b/experimental/libbox/command_urltest.go index 88e86a8f8c..3563d8c6a3 100644 --- a/experimental/libbox/command_urltest.go +++ b/experimental/libbox/command_urltest.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/service" ) func (c *CommandClient) URLTest(groupTag string) error { @@ -37,11 +38,11 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if err != nil { return err } - service := s.service - if service == nil { + serviceNow := s.service + if serviceNow == nil { return nil } - abstractOutboundGroup, isLoaded := service.instance.Router().Outbound(groupTag) + abstractOutboundGroup, isLoaded := serviceNow.instance.Router().Outbound(groupTag) if !isLoaded { return writeError(conn, E.New("outbound group not found: ", groupTag)) } @@ -53,15 +54,9 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if isURLTest { go urlTest.CheckOutbounds() } else { - var historyStorage *urltest.HistoryStorage - if clashServer := service.instance.Router().ClashServer(); clashServer != nil { - historyStorage = clashServer.HistoryStorage() - } else { - return writeError(conn, E.New("Clash API is required for URLTest on non-URLTest group")) - } - + historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx) outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { - itOutbound, _ := service.instance.Router().Outbound(it) + itOutbound, _ := serviceNow.instance.Router().Outbound(it) return itOutbound }), func(it adapter.Outbound) bool { if it == nil { @@ -73,12 +68,12 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { } return true }) - b, _ := batch.New(service.ctx, batch.WithConcurrencyNum[any](10)) + b, _ := batch.New(serviceNow.ctx, batch.WithConcurrencyNum[any](10)) for _, detour := range outbounds { outboundToTest := detour outboundTag := outboundToTest.Tag() b.Go(outboundTag, func() (any, error) { - t, err := urltest.URLTest(service.ctx, "", outboundToTest) + t, err := urltest.URLTest(serviceNow.ctx, "", outboundToTest) if err != nil { historyStorage.DeleteURLTestHistory(outboundTag) } else { diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index 73ee7e63ed..42fc878f56 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -3,7 +3,6 @@ package libbox import ( "bytes" "context" - "encoding/json" "net/netip" "os" @@ -15,13 +14,13 @@ import ( "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/x/list" ) func parseConfig(configContent string) (option.Options, error) { - var options option.Options - err := options.UnmarshalJSON([]byte(configContent)) + options, err := json.UnmarshalExtended[option.Options]([]byte(configContent)) if err != nil { return option.Options{}, E.Cause(err, "decode config") } diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 3375f7502c..a484c64b23 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -44,8 +44,6 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID) urlTestHistoryStorage := urltest.NewHistoryStorage() ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage) - pauseManager := pause.NewDefaultManager(ctx) - ctx = pause.ContextWithManager(ctx, pauseManager) platformWrapper := &platformInterfaceWrapper{iif: platformInterface, useProcFS: platformInterface.UseProcFS()} instance, err := box.New(box.Options{ Context: ctx, @@ -63,7 +61,7 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box cancel: cancel, instance: instance, urlTestHistoryStorage: urlTestHistoryStorage, - pauseManager: pauseManager, + pauseManager: service.FromContext[pause.Manager](ctx), }, nil } diff --git a/go.mod b/go.mod index b6960d5ac1..b62c284d98 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( berty.tech/go-libtor v1.0.385 github.com/caddyserver/certmagic v0.20.0 - github.com/cloudflare/circl v1.3.6 + 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.11 @@ -24,32 +24,32 @@ require ( github.com/pion/turn/v3 v3.0.1 github.com/pires/go-proxyproto v0.7.0 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a - github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a - github.com/sagernet/gomobile v0.0.0-20230915142329-c6740b6d2950 - github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 - github.com/sagernet/quic-go v0.40.0 + github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 + github.com/sagernet/gomobile v0.1.1 + github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e + github.com/sagernet/quic-go v0.40.1-beta.2 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.2.20 + github.com/sagernet/sing v0.3.0 github.com/sagernet/sing-dns v0.1.12 - github.com/sagernet/sing-mux v0.1.7 - github.com/sagernet/sing-quic v0.1.6 + github.com/sagernet/sing-mux v0.2.0 + github.com/sagernet/sing-quic v0.1.7 github.com/sagernet/sing-shadowsocks v0.2.6 - github.com/sagernet/sing-shadowsocks2 v0.1.5 + github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/sagernet/sing-shadowtls v0.1.4 - github.com/sagernet/sing-tun v0.1.24 + github.com/sagernet/sing-tun v0.2.0 github.com/sagernet/sing-vmess v0.1.8 - github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 - github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 - github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 - github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f + github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 + github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 + github.com/sagernet/utls v1.5.4 + 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 github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.26.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.17.0 - golang.org/x/net v0.19.0 - golang.org/x/sys v0.15.0 + golang.org/x/crypto v0.18.0 + golang.org/x/net v0.20.0 + golang.org/x/sys v0.16.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 google.golang.org/grpc v1.60.1 google.golang.org/protobuf v1.32.0 @@ -60,8 +60,9 @@ require ( require ( github.com/ajg/form v1.5.1 // indirect - github.com/andybalholm/brotli v1.0.5 // indirect + github.com/andybalholm/brotli v1.0.6 // indirect github.com/davecgh/go-spew v1.1.1 // 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 @@ -72,7 +73,7 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/klauspost/compress v1.15.15 // indirect + github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/libdns/libdns v0.2.1 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect @@ -86,7 +87,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/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -94,10 +94,10 @@ require ( github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect + golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.4.0 // indirect + golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.16.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect diff --git a/go.sum b/go.sum index e15d4f4d1f..0c3691d9a9 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,12 @@ berty.tech/go-libtor v1.0.385 h1:RWK94C3hZj6Z2GdvePpHJLnWYobFr3bY/OdUJ5aoEXw= berty.tech/go-libtor v1.0.385/go.mod h1:9swOOQVb+kmvuAlsgWUK/4c52pm69AdbJsxLzk+fJEw= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= -github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= +github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg= -github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg= -github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= @@ -17,6 +17,8 @@ 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.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= @@ -53,8 +55,8 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= -github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= -github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -110,48 +112,45 @@ github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= -github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a h1:wZHruBxZCsQLXHAozWpnJBL3wJ/XufDpz0qKtgpSnA4= -github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a/go.mod h1:dNV1ZP9y3qx5ltULeKaQZTZWTLHflgW5DES+Ses7cMI= -github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA= -github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms= -github.com/sagernet/gomobile v0.0.0-20230915142329-c6740b6d2950 h1:hUz/2mJLgi7l2H36JGpDY+jou9FmI6kAm0ZkU+xPpgE= -github.com/sagernet/gomobile v0.0.0-20230915142329-c6740b6d2950/go.mod h1:5YE39YkJkCcMsfq1jMKkjsrM2GfBoF9JVWnvU89hmvU= -github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 h1:dSPgjIw0CT6ISLeEh8Q20dZMBMFCcEceo23+LncRcNQ= -github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930/go.mod h1:JpKHkOYgh4wLwrX2BhH3ZIvCvazCkTnPeEcmigZJfHY= +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/gomobile v0.1.1 h1:3vihRGyUfFTToHMeeak0UK6/ldt2MV2bcWKFi2VyECU= +github.com/sagernet/gomobile v0.1.1/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E= +github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e h1:DOkjByVeAR56dkszjnMZke4wr7yM/1xHaJF3G9olkEE= +github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e/go.mod h1:fLxq/gtp0qzkaEwywlRRiGmjOK5ES/xUzyIKIFP2Asw= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= -github.com/sagernet/quic-go v0.40.0 h1:DvQNPb72lzvNQDe9tcUyHTw8eRv6PLtM2mNYmdlzUMo= -github.com/sagernet/quic-go v0.40.0/go.mod h1:VqtdhlbkeeG5Okhb3eDMb/9o0EoglReHunNT9ukrJAI= +github.com/sagernet/quic-go v0.40.1-beta.2 h1:USRwm36XuAFdcrmv4vDRD+YUOO08DfvLNruXThrVHZU= +github.com/sagernet/quic-go v0.40.1-beta.2/go.mod h1:CcKTpzTAISxrM4PA5M20/wYuz9Tj6Tx4DwGbNl9UQrU= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= -github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= -github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= -github.com/sagernet/sing v0.2.20 h1:ckcCB/5xu8G8wElNeH74IF6Soac5xWN+eQUXRuonjPQ= -github.com/sagernet/sing v0.2.20/go.mod h1:Ce5LNojQOgOiWhiD8pPD6E9H7e2KgtOe3Zxx4Ou5u80= +github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= +github.com/sagernet/sing v0.3.0 h1:PIDVFZHnQAAYRL1UYqNM+0k5s8f/tb1lUW6UDcQiOc8= +github.com/sagernet/sing v0.3.0/go.mod h1:9pfuAH6mZfgnz/YjP6xu5sxx882rfyjpcrTdUpd6w3g= github.com/sagernet/sing-dns v0.1.12 h1:1HqZ+ln+Rezx/aJMStaS0d7oPeX2EobSV1NT537kyj4= github.com/sagernet/sing-dns v0.1.12/go.mod h1:rx/DTOisneQpCgNQ4jbFU/JNEtnz0lYcHXenlVzpjEU= -github.com/sagernet/sing-mux v0.1.7 h1:+48spVReBwIrv6ZdUujiRFCCnblZFwxmbPgrs5zezlI= -github.com/sagernet/sing-mux v0.1.7/go.mod h1:UmcVSPrVjsOGe95jDXmGgOyKKIXOcjz6FKbFy+0LeDU= -github.com/sagernet/sing-quic v0.1.6 h1:yNkZiNOlmEGpS+A7I4/Zavhe/fRrLz7yCO/dVMZzt+k= -github.com/sagernet/sing-quic v0.1.6/go.mod h1:g1Ogcy2KSwKvC7eDXEUu9AnHbjotC+2xsSP+A1i/VOA= +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.1.7 h1:SC45rAnvQ9BuyO0V186OdDScMBitmZo0XcM9LBYRUW8= +github.com/sagernet/sing-quic v0.1.7/go.mod h1:wUg1Z6AGKeJguruZo0lhrie3dqPiRCKaidLAbK2ttEs= github.com/sagernet/sing-shadowsocks v0.2.6 h1:xr7ylAS/q1cQYS8oxKKajhuQcchd5VJJ4K4UZrrpp0s= github.com/sagernet/sing-shadowsocks v0.2.6/go.mod h1:j2YZBIpWIuElPFL/5sJAj470bcn/3QQ5lxZUNKLDNAM= -github.com/sagernet/sing-shadowsocks2 v0.1.5 h1:JDeAJ4ZWlYZ7F6qEVdDKPhQEangxKw/JtmU+i/YfCYE= -github.com/sagernet/sing-shadowsocks2 v0.1.5/go.mod h1:KF65y8lI5PGHyMgRZGYXYsH9ilgRc/yr+NYbSNGuBm4= +github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg= +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.1.24 h1:cxn8lr8uHMLB1tLU0SzBPE1Q04pG0Fb71GyeeCuic5Q= -github.com/sagernet/sing-tun v0.1.24/go.mod h1:Mnd7+8iGNb9uGnMAh3bp0ZA+nPFBZNaMHZPMEGdAQJM= +github.com/sagernet/sing-tun v0.2.0 h1:/WloeRTWvwKIuiIvY+HVJaaZsTGb3XqJXZUbn6wVhz4= +github.com/sagernet/sing-tun v0.2.0/go.mod h1:vJHzPAbwFUHxdFHUFQlH+Fb4rT3K4/SHODdMLU1rrQI= github.com/sagernet/sing-vmess v0.1.8 h1:XVWad1RpTy9b5tPxdm5MCU8cGfrTGdR8qCq6HV2aCNc= github.com/sagernet/sing-vmess v0.1.8/go.mod h1:vhx32UNzTDUkNwOyIjcZQohre1CaytquC5mPplId8uA= -github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 h1:HuE6xSwco/Xed8ajZ+coeYLmioq0Qp1/Z2zczFaV8as= -github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37/go.mod h1:3skNSftZDJWTGVtVaM2jfbce8qHnmH/AGDRe62iNOg0= -github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 h1:Px+hN4Vzgx+iCGVnWH5A8eR7JhNnIV3rGQmBxA7cw6Q= -github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6/go.mod h1:zovq6vTvEM6ECiqE3Eeb9rpIylPpamPcmrJ9tv0Bt0M= -github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 h1:kDUqhc9Vsk5HJuhfIATJ8oQwBmpOZJuozQG7Vk88lL4= -github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM= -github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f h1:Kvo8w8Y9lzFGB/7z09MJ3TR99TFtfI/IuY87Ygcycho= -github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f/go.mod h1:mySs0abhpc/gLlvhoq7HP1RzOaRmIXVeZGCh++zoApk= +github.com/sagernet/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/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 h1:z3SJQhVyU63FT26Wn/UByW6b7q8QKB0ZkPqsyqcz2PI= +github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6/go.mod h1:73xRZuxwkFk4aiLw28hG8W6o9cr2UPrGL9pdY2UTbvY= +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/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= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg= @@ -192,30 +191,17 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/W golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= -golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= +golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -225,24 +211,14 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= 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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -251,8 +227,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= -golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/inbound/http.go b/inbound/http.go index fa6c3d5867..b466331995 100644 --- a/inbound/http.go +++ b/inbound/http.go @@ -26,7 +26,7 @@ var ( type HTTP struct { myInboundAdapter - authenticator auth.Authenticator + authenticator *auth.Authenticator tlsConfig tls.ServerConfig } diff --git a/inbound/mixed.go b/inbound/mixed.go index 8c3bf3b4f9..982842efeb 100644 --- a/inbound/mixed.go +++ b/inbound/mixed.go @@ -29,7 +29,7 @@ var ( type Mixed struct { myInboundAdapter - authenticator auth.Authenticator + authenticator *auth.Authenticator } func NewMixed(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) *Mixed { diff --git a/inbound/naive.go b/inbound/naive.go index 1ff159d100..36bda492d8 100644 --- a/inbound/naive.go +++ b/inbound/naive.go @@ -32,7 +32,7 @@ var _ adapter.Inbound = (*Naive)(nil) type Naive struct { myInboundAdapter - authenticator auth.Authenticator + authenticator *auth.Authenticator tlsConfig tls.ServerConfig httpServer *http.Server h3Server any diff --git a/inbound/socks.go b/inbound/socks.go index 65dfb1e651..7c5183c9f7 100644 --- a/inbound/socks.go +++ b/inbound/socks.go @@ -22,7 +22,7 @@ var ( type Socks struct { myInboundAdapter - authenticator auth.Authenticator + authenticator *auth.Authenticator } func NewSocks(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SocksInboundOptions) *Socks { diff --git a/inbound/tun.go b/inbound/tun.go index 9582ab3fc5..7a08b1ec86 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -8,6 +8,7 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/taskmonitor" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/log" @@ -74,6 +75,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger tunOptions: tun.Options{ Name: options.InterfaceName, MTU: tunMTU, + GSO: options.GSO, Inet4Address: options.Inet4Address, Inet6Address: options.Inet6Address, AutoRoute: options.AutoRoute, @@ -142,7 +144,6 @@ func (t *Tun) Tag() string { func (t *Tun) Start() error { if C.IsAndroid && t.platformInterface == nil { - t.logger.Trace("building android rules") t.tunOptions.BuildAndroidRules(t.router.PackageManager(), t) } if t.tunOptions.Name == "" { @@ -152,12 +153,14 @@ func (t *Tun) Start() error { tunInterface tun.Tun err error ) - t.logger.Trace("opening interface") + monitor := taskmonitor.New(t.logger, C.DefaultStartTimeout) + monitor.Start("open tun interface") if t.platformInterface != nil { tunInterface, err = t.platformInterface.OpenTun(&t.tunOptions, t.platformOptions) } else { tunInterface, err = tun.New(t.tunOptions) } + monitor.Finish() if err != nil { return E.Cause(err, "configure tun interface") } @@ -166,10 +169,7 @@ func (t *Tun) Start() error { t.tunStack, err = tun.NewStack(t.stack, tun.StackOptions{ Context: t.ctx, Tun: tunInterface, - MTU: t.tunOptions.MTU, - Name: t.tunOptions.Name, - Inet4Address: t.tunOptions.Inet4Address, - Inet6Address: t.tunOptions.Inet6Address, + TunOptions: t.tunOptions, EndpointIndependentNat: t.endpointIndependentNat, UDPTimeout: t.udpTimeout, Handler: t, @@ -180,8 +180,9 @@ func (t *Tun) Start() error { if err != nil { return err } - t.logger.Trace("starting stack") + monitor.Start("initiating tun stack") err = t.tunStack.Start() + monitor.Finish() if err != nil { return err } diff --git a/log/default.go b/log/default.go deleted file mode 100644 index c2e1c7452e..0000000000 --- a/log/default.go +++ /dev/null @@ -1,137 +0,0 @@ -package log - -import ( - "context" - "io" - "os" - "time" - - C "github.com/sagernet/sing-box/constant" - F "github.com/sagernet/sing/common/format" -) - -var _ Factory = (*simpleFactory)(nil) - -type simpleFactory struct { - formatter Formatter - platformFormatter Formatter - writer io.Writer - platformWriter PlatformWriter - level Level -} - -func NewFactory(formatter Formatter, writer io.Writer, platformWriter PlatformWriter) Factory { - return &simpleFactory{ - formatter: formatter, - platformFormatter: Formatter{ - BaseTime: formatter.BaseTime, - DisableColors: C.IsDarwin || C.IsIos, - DisableLineBreak: true, - }, - writer: writer, - platformWriter: platformWriter, - level: LevelTrace, - } -} - -func (f *simpleFactory) Level() Level { - return f.level -} - -func (f *simpleFactory) SetLevel(level Level) { - f.level = level -} - -func (f *simpleFactory) Logger() ContextLogger { - return f.NewLogger("") -} - -func (f *simpleFactory) NewLogger(tag string) ContextLogger { - return &simpleLogger{f, tag} -} - -func (f *simpleFactory) Close() error { - return nil -} - -var _ ContextLogger = (*simpleLogger)(nil) - -type simpleLogger struct { - *simpleFactory - tag string -} - -func (l *simpleLogger) Log(ctx context.Context, level Level, args []any) { - level = OverrideLevelFromContext(level, ctx) - if level > l.level { - return - } - nowTime := time.Now() - message := l.formatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime) - if level == LevelPanic { - panic(message) - } - l.writer.Write([]byte(message)) - if level == LevelFatal { - os.Exit(1) - } - if l.platformWriter != nil { - l.platformWriter.WriteMessage(level, l.platformFormatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime)) - } -} - -func (l *simpleLogger) Trace(args ...any) { - l.TraceContext(context.Background(), args...) -} - -func (l *simpleLogger) Debug(args ...any) { - l.DebugContext(context.Background(), args...) -} - -func (l *simpleLogger) Info(args ...any) { - l.InfoContext(context.Background(), args...) -} - -func (l *simpleLogger) Warn(args ...any) { - l.WarnContext(context.Background(), args...) -} - -func (l *simpleLogger) Error(args ...any) { - l.ErrorContext(context.Background(), args...) -} - -func (l *simpleLogger) Fatal(args ...any) { - l.FatalContext(context.Background(), args...) -} - -func (l *simpleLogger) Panic(args ...any) { - l.PanicContext(context.Background(), args...) -} - -func (l *simpleLogger) TraceContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelTrace, args) -} - -func (l *simpleLogger) DebugContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelDebug, args) -} - -func (l *simpleLogger) InfoContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelInfo, args) -} - -func (l *simpleLogger) WarnContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelWarn, args) -} - -func (l *simpleLogger) ErrorContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelError, args) -} - -func (l *simpleLogger) FatalContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelFatal, args) -} - -func (l *simpleLogger) PanicContext(ctx context.Context, args ...any) { - l.Log(ctx, LevelPanic, args) -} diff --git a/log/export.go b/log/export.go index 743fce93ec..60a0abbbfd 100644 --- a/log/export.go +++ b/log/export.go @@ -9,7 +9,14 @@ import ( var std ContextLogger func init() { - std = NewFactory(Formatter{BaseTime: time.Now()}, os.Stderr, nil).Logger() + std = NewDefaultFactory( + context.Background(), + Formatter{BaseTime: time.Now()}, + os.Stderr, + "", + nil, + false, + ).Logger() } func StdLogger() ContextLogger { diff --git a/log/factory.go b/log/factory.go index 739aa0f2d7..54a88228fc 100644 --- a/log/factory.go +++ b/log/factory.go @@ -11,11 +11,12 @@ type ( ) type Factory interface { + Start() error + Close() error Level() Level SetLevel(level Level) Logger() ContextLogger NewLogger(tag string) ContextLogger - Close() error } type ObservableFactory interface { diff --git a/log/log.go b/log/log.go index 19dbbbd912..7b8f284350 100644 --- a/log/log.go +++ b/log/log.go @@ -7,35 +7,9 @@ import ( "time" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/service/filemanager" ) -type factoryWithFile struct { - Factory - file *os.File -} - -func (f *factoryWithFile) Close() error { - return common.Close( - f.Factory, - common.PtrOrNil(f.file), - ) -} - -type observableFactoryWithFile struct { - ObservableFactory - file *os.File -} - -func (f *observableFactoryWithFile) Close() error { - return common.Close( - f.ObservableFactory, - common.PtrOrNil(f.file), - ) -} - type Options struct { Context context.Context Options option.LogOptions @@ -52,8 +26,8 @@ func New(options Options) (Factory, error) { return NewNOPFactory(), nil } - var logFile *os.File var logWriter io.Writer + var logFilePath string switch logOptions.Output { case "": @@ -66,26 +40,23 @@ func New(options Options) (Factory, error) { case "stdout": logWriter = os.Stdout default: - var err error - logFile, err = filemanager.OpenFile(options.Context, logOptions.Output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return nil, err - } - logWriter = logFile + logFilePath = logOptions.Output } logFormatter := Formatter{ BaseTime: options.BaseTime, - DisableColors: logOptions.DisableColor || logFile != nil, - DisableTimestamp: !logOptions.Timestamp && logFile != nil, + DisableColors: logOptions.DisableColor || logFilePath != "", + DisableTimestamp: !logOptions.Timestamp && logFilePath != "", FullTimestamp: logOptions.Timestamp, TimestampFormat: "-0700 2006-01-02 15:04:05", } - var factory Factory - if options.Observable { - factory = NewObservableFactory(logFormatter, logWriter, options.PlatformWriter) - } else { - factory = NewFactory(logFormatter, logWriter, options.PlatformWriter) - } + factory := NewDefaultFactory( + options.Context, + logFormatter, + logWriter, + logFilePath, + options.PlatformWriter, + options.Observable, + ) if logOptions.Level != "" { logLevel, err := ParseLevel(logOptions.Level) if err != nil { @@ -95,18 +66,5 @@ func New(options Options) (Factory, error) { } else { factory.SetLevel(LevelTrace) } - if logFile != nil { - if options.Observable { - factory = &observableFactoryWithFile{ - ObservableFactory: factory.(ObservableFactory), - file: logFile, - } - } else { - factory = &factoryWithFile{ - Factory: factory, - file: logFile, - } - } - } return factory, nil } diff --git a/log/nop.go b/log/nop.go index 06f0b87241..6369e99b1c 100644 --- a/log/nop.go +++ b/log/nop.go @@ -15,6 +15,14 @@ func NewNOPFactory() ObservableFactory { return (*nopFactory)(nil) } +func (f *nopFactory) Start() error { + return nil +} + +func (f *nopFactory) Close() error { + return nil +} + func (f *nopFactory) Level() Level { return LevelTrace } @@ -72,10 +80,6 @@ func (f *nopFactory) FatalContext(ctx context.Context, args ...any) { func (f *nopFactory) PanicContext(ctx context.Context, args ...any) { } -func (f *nopFactory) Close() error { - return nil -} - func (f *nopFactory) Subscribe() (subscription observable.Subscription[Entry], done <-chan struct{}, err error) { return nil, nil, os.ErrInvalid } diff --git a/log/observable.go b/log/observable.go index c12cbbbe24..859d45b469 100644 --- a/log/observable.go +++ b/log/observable.go @@ -9,29 +9,44 @@ import ( "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/observable" + "github.com/sagernet/sing/service/filemanager" ) -var _ Factory = (*observableFactory)(nil) +var _ Factory = (*defaultFactory)(nil) -type observableFactory struct { +type defaultFactory struct { + ctx context.Context formatter Formatter platformFormatter Formatter writer io.Writer + file *os.File + filePath string platformWriter PlatformWriter + needObservable bool level Level subscriber *observable.Subscriber[Entry] observer *observable.Observer[Entry] } -func NewObservableFactory(formatter Formatter, writer io.Writer, platformWriter PlatformWriter) ObservableFactory { - factory := &observableFactory{ +func NewDefaultFactory( + ctx context.Context, + formatter Formatter, + writer io.Writer, + filePath string, + platformWriter PlatformWriter, + needObservable bool, +) ObservableFactory { + factory := &defaultFactory{ + ctx: ctx, formatter: formatter, platformFormatter: Formatter{ BaseTime: formatter.BaseTime, DisableLineBreak: true, }, writer: writer, + filePath: filePath, platformWriter: platformWriter, + needObservable: needObservable, level: LevelTrace, subscriber: observable.NewSubscriber[Entry](128), } @@ -42,40 +57,53 @@ func NewObservableFactory(formatter Formatter, writer io.Writer, platformWriter return factory } -func (f *observableFactory) Level() Level { +func (f *defaultFactory) Start() error { + if f.filePath != "" { + logFile, err := filemanager.OpenFile(f.ctx, f.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + f.writer = logFile + f.file = logFile + } + return nil +} + +func (f *defaultFactory) Close() error { + return common.Close( + common.PtrOrNil(f.file), + f.observer, + ) +} + +func (f *defaultFactory) Level() Level { return f.level } -func (f *observableFactory) SetLevel(level Level) { +func (f *defaultFactory) SetLevel(level Level) { f.level = level } -func (f *observableFactory) Logger() ContextLogger { +func (f *defaultFactory) Logger() ContextLogger { return f.NewLogger("") } -func (f *observableFactory) NewLogger(tag string) ContextLogger { +func (f *defaultFactory) NewLogger(tag string) ContextLogger { return &observableLogger{f, tag} } -func (f *observableFactory) Subscribe() (subscription observable.Subscription[Entry], done <-chan struct{}, err error) { +func (f *defaultFactory) Subscribe() (subscription observable.Subscription[Entry], done <-chan struct{}, err error) { return f.observer.Subscribe() } -func (f *observableFactory) UnSubscribe(sub observable.Subscription[Entry]) { +func (f *defaultFactory) UnSubscribe(sub observable.Subscription[Entry]) { f.observer.UnSubscribe(sub) } -func (f *observableFactory) Close() error { - return common.Close( - f.observer, - ) -} - var _ ContextLogger = (*observableLogger)(nil) type observableLogger struct { - *observableFactory + *defaultFactory tag string } @@ -85,15 +113,26 @@ func (l *observableLogger) Log(ctx context.Context, level Level, args []any) { return } nowTime := time.Now() - message, messageSimple := l.formatter.FormatWithSimple(ctx, level, l.tag, F.ToString(args...), nowTime) - if level == LevelPanic { - panic(message) - } - l.writer.Write([]byte(message)) - if level == LevelFatal { - os.Exit(1) + if l.needObservable { + message, messageSimple := l.formatter.FormatWithSimple(ctx, level, l.tag, F.ToString(args...), nowTime) + if level == LevelPanic { + panic(message) + } + l.writer.Write([]byte(message)) + if level == LevelFatal { + os.Exit(1) + } + l.subscriber.Emit(Entry{level, messageSimple}) + } else { + message := l.formatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime) + if level == LevelPanic { + panic(message) + } + l.writer.Write([]byte(message)) + if level == LevelFatal { + os.Exit(1) + } } - l.subscriber.Emit(Entry{level, messageSimple}) if l.platformWriter != nil { l.platformWriter.WriteMessage(level, l.platformFormatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime)) } diff --git a/mkdocs.yml b/mkdocs.yml index 1d4b1d8b0e..e786cfdefb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,12 +11,14 @@ theme: logo: assets/icon.svg favicon: assets/icon.svg palette: - - scheme: default + - media: "(prefers-color-scheme: light)" + scheme: default primary: white toggle: icon: material/brightness-7 name: Switch to dark mode - - scheme: slate + - media: "(prefers-color-scheme: dark)" + scheme: slate primary: black toggle: icon: material/brightness-4 @@ -32,12 +34,16 @@ theme: - content.code.copy - content.code.select - content.code.annotate + icon: + admonition: + question: material/new-box nav: - Home: - index.md + - Change Log: changelog.md + - Migration: migration.md - Deprecated: deprecated.md - Support: support.md - - Change Log: changelog.md - Installation: - Package Manager: installation/package-manager.md - Docker: installation/docker.md @@ -56,7 +62,7 @@ nav: - Proxy: - Server: manual/proxy/server.md - Client: manual/proxy/client.md -# - TUN: manual/proxy/tun.md + # - TUN: manual/proxy/tun.md - Proxy Protocol: - Shadowsocks: manual/proxy-protocol/shadowsocks.md - Trojan: manual/proxy-protocol/trojan.md @@ -79,8 +85,15 @@ nav: - Geosite: configuration/route/geosite.md - Route Rule: configuration/route/rule.md - Protocol Sniff: configuration/route/sniff.md + - Rule Set: + - configuration/rule-set/index.md + - Source Format: configuration/rule-set/source-format.md + - Headless Rule: configuration/rule-set/headless-rule.md - Experimental: - configuration/experimental/index.md + - Cache File: configuration/experimental/cache-file.md + - Clash API: configuration/experimental/clash-api.md + - V2Ray API: configuration/experimental/v2ray-api.md - Shared: - Listen Fields: configuration/shared/listen.md - Dial Fields: configuration/shared/dial.md @@ -180,9 +193,10 @@ plugins: name: 简体中文 nav_translations: Home: 开始 + Change Log: 更新日志 + Migration: 迁移指南 Deprecated: 废弃功能列表 Support: 支持 - Change Log: 更新日志 Installation: 安装 Package Manager: 包管理器 @@ -203,7 +217,12 @@ plugins: Route Rule: 路由规则 Protocol Sniff: 协议探测 + Rule Set: 规则集 + Source Format: 源文件格式 + Headless Rule: 无头规则 + Experimental: 实验性 + Cache File: 缓存文件 Shared: 通用 Listen Fields: 监听字段 @@ -215,10 +234,6 @@ plugins: Inbound: 入站 Outbound: 出站 - FAQ: 常见问题 - Known Issues: 已知问题 - Examples: 示例 - Linux Server Installation: Linux 服务器安装 - DNS Hijack: DNS 劫持 + Manual: 手册 reconfigure_material: true reconfigure_search: true \ No newline at end of file diff --git a/option/clash.go b/option/clash.go deleted file mode 100644 index 63ee2aebdf..0000000000 --- a/option/clash.go +++ /dev/null @@ -1,31 +0,0 @@ -package option - -type ClashAPIOptions struct { - ExternalController string `json:"external_controller,omitempty"` - ExternalUI string `json:"external_ui,omitempty"` - ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` - ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` - Secret string `json:"secret,omitempty"` - DefaultMode string `json:"default_mode,omitempty"` - StoreMode bool `json:"store_mode,omitempty"` - StoreSelected bool `json:"store_selected,omitempty"` - StoreFakeIP bool `json:"store_fakeip,omitempty"` - CacheFile string `json:"cache_file,omitempty"` - CacheID string `json:"cache_id,omitempty"` - - ModeList []string `json:"-"` -} - -type SelectorOutboundOptions struct { - Outbounds []string `json:"outbounds"` - Default string `json:"default,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} - -type URLTestOutboundOptions struct { - Outbounds []string `json:"outbounds"` - URL string `json:"url,omitempty"` - Interval Duration `json:"interval,omitempty"` - Tolerance uint16 `json:"tolerance,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} diff --git a/option/config.go b/option/config.go index ec471112e7..3f5d7602c0 100644 --- a/option/config.go +++ b/option/config.go @@ -2,13 +2,12 @@ package option import ( "bytes" - "strings" - "github.com/sagernet/sing-box/common/json" - E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) type _Options struct { + RawMessage json.RawMessage `json:"-"` Schema string `json:"$schema,omitempty"` Log *LogOptions `json:"log,omitempty"` DNS *DNSOptions `json:"dns,omitempty"` @@ -22,19 +21,14 @@ type _Options struct { type Options _Options func (o *Options) UnmarshalJSON(content []byte) error { - decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content))) + decoder := json.NewDecoder(bytes.NewReader(content)) decoder.DisallowUnknownFields() err := decoder.Decode((*_Options)(o)) - if err == nil { - return nil + if err != nil { + return err } - if syntaxError, isSyntaxError := err.(*json.SyntaxError); isSyntaxError { - prefix := string(content[:syntaxError.Offset]) - row := strings.Count(prefix, "\n") + 1 - column := len(prefix) - strings.LastIndex(prefix, "\n") - 1 - return E.Extend(syntaxError, "row ", row, ", column ", column) - } - return err + o.RawMessage = content + return nil } type LogOptions struct { diff --git a/option/debug.go b/option/debug.go index 17fb05d03d..0b0b825a82 100644 --- a/option/debug.go +++ b/option/debug.go @@ -1,9 +1,8 @@ package option import ( - "encoding/json" - "github.com/sagernet/sing-box/common/humanize" + "github.com/sagernet/sing/common/json" ) type DebugOptions struct { diff --git a/option/experimental.go b/option/experimental.go index a5b6acbd32..c685f51f54 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -1,7 +1,48 @@ package option type ExperimentalOptions struct { - ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` - V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` - Debug *DebugOptions `json:"debug,omitempty"` + CacheFile *CacheFileOptions `json:"cache_file,omitempty"` + ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` + V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` + Debug *DebugOptions `json:"debug,omitempty"` +} + +type CacheFileOptions struct { + Enabled bool `json:"enabled,omitempty"` + Path string `json:"path,omitempty"` + CacheID string `json:"cache_id,omitempty"` + StoreFakeIP bool `json:"store_fakeip,omitempty"` +} + +type ClashAPIOptions struct { + ExternalController string `json:"external_controller,omitempty"` + ExternalUI string `json:"external_ui,omitempty"` + ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` + ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` + Secret string `json:"secret,omitempty"` + DefaultMode string `json:"default_mode,omitempty"` + ModeList []string `json:"-"` + + // Deprecated: migrated to global cache file + CacheFile string `json:"cache_file,omitempty"` + // Deprecated: migrated to global cache file + CacheID string `json:"cache_id,omitempty"` + // Deprecated: migrated to global cache file + StoreMode bool `json:"store_mode,omitempty"` + // Deprecated: migrated to global cache file + StoreSelected bool `json:"store_selected,omitempty"` + // Deprecated: migrated to global cache file + StoreFakeIP bool `json:"store_fakeip,omitempty"` +} + +type V2RayAPIOptions struct { + Listen string `json:"listen,omitempty"` + Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` +} + +type V2RayStatsServiceOptions struct { + Enabled bool `json:"enabled,omitempty"` + Inbounds []string `json:"inbounds,omitempty"` + Outbounds []string `json:"outbounds,omitempty"` + Users []string `json:"users,omitempty"` } diff --git a/option/group.go b/option/group.go new file mode 100644 index 0000000000..72a0f63702 --- /dev/null +++ b/option/group.go @@ -0,0 +1,16 @@ +package option + +type SelectorOutboundOptions struct { + Outbounds []string `json:"outbounds"` + Default string `json:"default,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} + +type URLTestOutboundOptions struct { + Outbounds []string `json:"outbounds"` + URL string `json:"url,omitempty"` + Interval Duration `json:"interval,omitempty"` + Tolerance uint16 `json:"tolerance,omitempty"` + IdleTimeout Duration `json:"idle_timeout,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` +} diff --git a/option/hysteria.go b/option/hysteria.go index 7f54a8b76b..7e26185a60 100644 --- a/option/hysteria.go +++ b/option/hysteria.go @@ -1,17 +1,17 @@ package option type HysteriaInboundOptions struct { ListenOptions - Up string `json:"up,omitempty"` - UpMbps int `json:"up_mbps,omitempty"` - Down string `json:"down,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs string `json:"obfs,omitempty"` - Users []HysteriaUser `json:"users,omitempty"` - ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` - ReceiveWindowClient uint64 `json:"recv_window_client,omitempty"` - MaxConnClient int `json:"max_conn_client,omitempty"` - DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` - TLS *InboundTLSOptions `json:"tls,omitempty"` + Up string `json:"up,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + Down string `json:"down,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs string `json:"obfs,omitempty"` + Users []HysteriaUser `json:"users,omitempty"` + ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` + ReceiveWindowClient uint64 `json:"recv_window_client,omitempty"` + MaxConnClient int `json:"max_conn_client,omitempty"` + DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` + InboundTLSOptionsContainer } type HysteriaUser struct { @@ -23,17 +23,17 @@ type HysteriaUser struct { type HysteriaOutboundOptions struct { DialerOptions ServerOptions - Up string `json:"up,omitempty"` - UpMbps int `json:"up_mbps,omitempty"` - Down string `json:"down,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs string `json:"obfs,omitempty"` - Auth []byte `json:"auth,omitempty"` - AuthString string `json:"auth_str,omitempty"` - ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` - ReceiveWindow uint64 `json:"recv_window,omitempty"` - DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` - Network NetworkList `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` + Up string `json:"up,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + Down string `json:"down,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs string `json:"obfs,omitempty"` + Auth []byte `json:"auth,omitempty"` + AuthString string `json:"auth_str,omitempty"` + ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` + ReceiveWindow uint64 `json:"recv_window,omitempty"` + DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` + Network NetworkList `json:"network,omitempty"` + OutboundTLSOptionsContainer TurnRelay *TurnRelayOptions`json:"turn_relay,omitempty"` } diff --git a/option/hysteria2.go b/option/hysteria2.go index f94a59bf37..d3b29ce803 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -2,14 +2,14 @@ package option type Hysteria2InboundOptions struct { ListenOptions - UpMbps int `json:"up_mbps,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs *Hysteria2Obfs `json:"obfs,omitempty"` - Users []Hysteria2User `json:"users,omitempty"` - IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"` - TLS *InboundTLSOptions `json:"tls,omitempty"` - Masquerade string `json:"masquerade,omitempty"` - BrutalDebug bool `json:"brutal_debug,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs *Hysteria2Obfs `json:"obfs,omitempty"` + Users []Hysteria2User `json:"users,omitempty"` + IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"` + InboundTLSOptionsContainer + Masquerade string `json:"masquerade,omitempty"` + BrutalDebug bool `json:"brutal_debug,omitempty"` } type Hysteria2Obfs struct { @@ -25,12 +25,12 @@ type Hysteria2User struct { type Hysteria2OutboundOptions struct { DialerOptions ServerOptions - UpMbps int `json:"up_mbps,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs *Hysteria2Obfs `json:"obfs,omitempty"` - Password string `json:"password,omitempty"` - Network NetworkList `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - BrutalDebug bool `json:"brutal_debug,omitempty"` - TurnRelay *TurnRelayOptions`json:"turn_relay,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs *Hysteria2Obfs `json:"obfs,omitempty"` + Password string `json:"password,omitempty"` + Network NetworkList `json:"network,omitempty"` + OutboundTLSOptionsContainer + BrutalDebug bool `json:"brutal_debug,omitempty"` + TurnRelay *TurnRelayOptions `json:"turn_relay,omitempty"` } diff --git a/option/inbound.go b/option/inbound.go index ede159447a..54e8bab84a 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -3,9 +3,9 @@ package option import ( "time" - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) type _Inbound struct { @@ -31,45 +31,55 @@ type _Inbound struct { type Inbound _Inbound -func (h Inbound) MarshalJSON() ([]byte, error) { - var v any +func (h *Inbound) RawOptions() (any, error) { + var rawOptionsPtr any switch h.Type { case C.TypeTun: - v = h.TunOptions + rawOptionsPtr = &h.TunOptions case C.TypeRedirect: - v = h.RedirectOptions + rawOptionsPtr = &h.RedirectOptions case C.TypeTProxy: - v = h.TProxyOptions + rawOptionsPtr = &h.TProxyOptions case C.TypeDirect: - v = h.DirectOptions + rawOptionsPtr = &h.DirectOptions case C.TypeSOCKS: - v = h.SocksOptions + rawOptionsPtr = &h.SocksOptions case C.TypeHTTP: - v = h.HTTPOptions + rawOptionsPtr = &h.HTTPOptions case C.TypeMixed: - v = h.MixedOptions + rawOptionsPtr = &h.MixedOptions case C.TypeShadowsocks: - v = h.ShadowsocksOptions + rawOptionsPtr = &h.ShadowsocksOptions case C.TypeVMess: - v = h.VMessOptions + rawOptionsPtr = &h.VMessOptions case C.TypeTrojan: - v = h.TrojanOptions + rawOptionsPtr = &h.TrojanOptions case C.TypeNaive: - v = h.NaiveOptions + rawOptionsPtr = &h.NaiveOptions case C.TypeHysteria: - v = h.HysteriaOptions + rawOptionsPtr = &h.HysteriaOptions case C.TypeShadowTLS: - v = h.ShadowTLSOptions + rawOptionsPtr = &h.ShadowTLSOptions case C.TypeVLESS: - v = h.VLESSOptions + rawOptionsPtr = &h.VLESSOptions case C.TypeTUIC: - v = h.TUICOptions + rawOptionsPtr = &h.TUICOptions case C.TypeHysteria2: - v = h.Hysteria2Options + rawOptionsPtr = &h.Hysteria2Options + case "": + return nil, E.New("missing inbound type") default: return nil, E.New("unknown inbound type: ", h.Type) } - return MarshallObjects((_Inbound)(h), v) + return rawOptionsPtr, nil +} + +func (h Inbound) MarshalJSON() ([]byte, error) { + rawOptions, err := h.RawOptions() + if err != nil { + return nil, err + } + return MarshallObjects((_Inbound)(h), rawOptions) } func (h *Inbound) UnmarshalJSON(bytes []byte) error { @@ -77,46 +87,13 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error { if err != nil { return err } - var v any - switch h.Type { - case C.TypeTun: - v = &h.TunOptions - case C.TypeRedirect: - v = &h.RedirectOptions - case C.TypeTProxy: - v = &h.TProxyOptions - case C.TypeDirect: - v = &h.DirectOptions - case C.TypeSOCKS: - v = &h.SocksOptions - case C.TypeHTTP: - v = &h.HTTPOptions - case C.TypeMixed: - v = &h.MixedOptions - case C.TypeShadowsocks: - v = &h.ShadowsocksOptions - case C.TypeVMess: - v = &h.VMessOptions - case C.TypeTrojan: - v = &h.TrojanOptions - case C.TypeNaive: - v = &h.NaiveOptions - case C.TypeHysteria: - v = &h.HysteriaOptions - case C.TypeShadowTLS: - v = &h.ShadowTLSOptions - case C.TypeVLESS: - v = &h.VLESSOptions - case C.TypeTUIC: - v = &h.TUICOptions - case C.TypeHysteria2: - v = &h.Hysteria2Options - default: - return E.New("unknown inbound type: ", h.Type) + rawOptions, err := h.RawOptions() + if err != nil { + return err } - err = UnmarshallExcluded(bytes, (*_Inbound)(h), v) + err = UnmarshallExcluded(bytes, (*_Inbound)(h), rawOptions) if err != nil { - return E.Cause(err, "inbound options") + return err } return nil } @@ -158,3 +135,16 @@ func (c *UDPTimeoutCompat) UnmarshalJSON(data []byte) error { } return json.Unmarshal(data, (*Duration)(c)) } + +type ListenOptionsWrapper interface { + TakeListenOptions() ListenOptions + ReplaceListenOptions(options ListenOptions) +} + +func (o *ListenOptions) TakeListenOptions() ListenOptions { + return *o +} + +func (o *ListenOptions) ReplaceListenOptions(options ListenOptions) { + *o = options +} diff --git a/option/json.go b/option/json.go index d010da32f1..07580e9f7b 100644 --- a/option/json.go +++ b/option/json.go @@ -3,10 +3,10 @@ package option import ( "bytes" - "github.com/sagernet/sing-box/common/badjson" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" ) func ToMap(v any) (*badjson.JSONObject, error) { diff --git a/option/naive.go b/option/naive.go index e7e1a0b28a..0b19f26473 100644 --- a/option/naive.go +++ b/option/naive.go @@ -4,7 +4,7 @@ import "github.com/sagernet/sing/common/auth" type NaiveInboundOptions struct { ListenOptions - Users []auth.User `json:"users,omitempty"` - Network NetworkList `json:"network,omitempty"` - TLS *InboundTLSOptions `json:"tls,omitempty"` + Users []auth.User `json:"users,omitempty"` + Network NetworkList `json:"network,omitempty"` + InboundTLSOptionsContainer } diff --git a/option/outbound.go b/option/outbound.go index f6adb3d3a4..d2e98ce5c7 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -1,9 +1,9 @@ package option import ( - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" M "github.com/sagernet/sing/common/metadata" ) @@ -31,49 +31,59 @@ type _Outbound struct { type Outbound _Outbound -func (h Outbound) MarshalJSON() ([]byte, error) { - var v any +func (h *Outbound) RawOptions() (any, error) { + var rawOptionsPtr any switch h.Type { case C.TypeDirect: - v = h.DirectOptions + rawOptionsPtr = &h.DirectOptions case C.TypeBlock, C.TypeDNS: - v = nil + rawOptionsPtr = nil case C.TypeSOCKS: - v = h.SocksOptions + rawOptionsPtr = &h.SocksOptions case C.TypeHTTP: - v = h.HTTPOptions + rawOptionsPtr = &h.HTTPOptions case C.TypeShadowsocks: - v = h.ShadowsocksOptions + rawOptionsPtr = &h.ShadowsocksOptions case C.TypeVMess: - v = h.VMessOptions + rawOptionsPtr = &h.VMessOptions case C.TypeTrojan: - v = h.TrojanOptions + rawOptionsPtr = &h.TrojanOptions case C.TypeWireGuard: - v = h.WireGuardOptions + rawOptionsPtr = &h.WireGuardOptions case C.TypeHysteria: - v = h.HysteriaOptions + rawOptionsPtr = &h.HysteriaOptions case C.TypeTor: - v = h.TorOptions + rawOptionsPtr = &h.TorOptions case C.TypeSSH: - v = h.SSHOptions + rawOptionsPtr = &h.SSHOptions case C.TypeShadowTLS: - v = h.ShadowTLSOptions + rawOptionsPtr = &h.ShadowTLSOptions case C.TypeShadowsocksR: - v = h.ShadowsocksROptions + rawOptionsPtr = &h.ShadowsocksROptions case C.TypeVLESS: - v = h.VLESSOptions + rawOptionsPtr = &h.VLESSOptions case C.TypeTUIC: - v = h.TUICOptions + rawOptionsPtr = &h.TUICOptions case C.TypeHysteria2: - v = h.Hysteria2Options + rawOptionsPtr = &h.Hysteria2Options case C.TypeSelector: - v = h.SelectorOptions + rawOptionsPtr = &h.SelectorOptions case C.TypeURLTest: - v = h.URLTestOptions + rawOptionsPtr = &h.URLTestOptions + case "": + return nil, E.New("missing outbound type") default: return nil, E.New("unknown outbound type: ", h.Type) } - return MarshallObjects((_Outbound)(h), v) + return rawOptionsPtr, nil +} + +func (h *Outbound) MarshalJSON() ([]byte, error) { + rawOptions, err := h.RawOptions() + if err != nil { + return nil, err + } + return MarshallObjects((*_Outbound)(h), rawOptions) } func (h *Outbound) UnmarshalJSON(bytes []byte) error { @@ -81,70 +91,52 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error { if err != nil { return err } - var v any - switch h.Type { - case C.TypeDirect: - v = &h.DirectOptions - case C.TypeBlock, C.TypeDNS: - v = nil - case C.TypeSOCKS: - v = &h.SocksOptions - case C.TypeHTTP: - v = &h.HTTPOptions - case C.TypeShadowsocks: - v = &h.ShadowsocksOptions - case C.TypeVMess: - v = &h.VMessOptions - case C.TypeTrojan: - v = &h.TrojanOptions - case C.TypeWireGuard: - v = &h.WireGuardOptions - case C.TypeHysteria: - v = &h.HysteriaOptions - case C.TypeTor: - v = &h.TorOptions - case C.TypeSSH: - v = &h.SSHOptions - case C.TypeShadowTLS: - v = &h.ShadowTLSOptions - case C.TypeShadowsocksR: - v = &h.ShadowsocksROptions - case C.TypeVLESS: - v = &h.VLESSOptions - case C.TypeTUIC: - v = &h.TUICOptions - case C.TypeHysteria2: - v = &h.Hysteria2Options - case C.TypeSelector: - v = &h.SelectorOptions - case C.TypeURLTest: - v = &h.URLTestOptions - default: - return E.New("unknown outbound type: ", h.Type) + rawOptions, err := h.RawOptions() + if err != nil { + return err } - err = UnmarshallExcluded(bytes, (*_Outbound)(h), v) + err = UnmarshallExcluded(bytes, (*_Outbound)(h), rawOptions) if err != nil { - return E.Cause(err, "outbound options") + return err } return nil } +type DialerOptionsWrapper interface { + TakeDialerOptions() DialerOptions + ReplaceDialerOptions(options DialerOptions) +} + type DialerOptions struct { - Detour string `json:"detour,omitempty"` - BindInterface string `json:"bind_interface,omitempty"` - 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"` - ReuseAddr bool `json:"reuse_addr,omitempty"` - ConnectTimeout Duration `json:"connect_timeout,omitempty"` - TCPFastOpen bool `json:"tcp_fast_open,omitempty"` - TCPMultiPath bool `json:"tcp_multi_path,omitempty"` - TLSFragment *TLSFragmentOptions `json:"tls_fragment,omitempty"` - UDPFragment *bool `json:"udp_fragment,omitempty"` - UDPFragmentDefault bool `json:"-"` - DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` - FallbackDelay Duration `json:"fallback_delay,omitempty"` + Detour string `json:"detour,omitempty"` + BindInterface string `json:"bind_interface,omitempty"` + 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"` + ReuseAddr bool `json:"reuse_addr,omitempty"` + ConnectTimeout Duration `json:"connect_timeout,omitempty"` + TCPFastOpen bool `json:"tcp_fast_open,omitempty"` + TCPMultiPath bool `json:"tcp_multi_path,omitempty"` + UDPFragment *bool `json:"udp_fragment,omitempty"` + UDPFragmentDefault bool `json:"-"` + DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` + FallbackDelay Duration `json:"fallback_delay,omitempty"` + IsWireGuardListener bool `json:"-"` + TLSFragment *TLSFragmentOptions `json:"tls_fragment,omitempty"` //hiddify +} + +func (o *DialerOptions) TakeDialerOptions() DialerOptions { + return *o +} + +func (o *DialerOptions) ReplaceDialerOptions(options DialerOptions) { + *o = options +} + +type ServerOptionsWrapper interface { + TakeServerOptions() ServerOptions + ReplaceServerOptions(options ServerOptions) } type ServerOptions struct { @@ -155,3 +147,11 @@ type ServerOptions struct { func (o ServerOptions) Build() M.Socksaddr { return M.ParseSocksaddrHostPort(o.Server, o.ServerPort) } + +func (o *ServerOptions) TakeServerOptions() ServerOptions { + return *o +} + +func (o *ServerOptions) ReplaceServerOptions(options ServerOptions) { + *o = options +} diff --git a/option/platform.go b/option/platform.go index fd3d73f0c5..a43cbf230f 100644 --- a/option/platform.go +++ b/option/platform.go @@ -1,8 +1,8 @@ package option import ( - "github.com/sagernet/sing-box/common/json" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) type OnDemandOptions struct { diff --git a/option/route.go b/option/route.go index 43150576e2..e313fcf242 100644 --- a/option/route.go +++ b/option/route.go @@ -4,6 +4,7 @@ type RouteOptions struct { GeoIP *GeoIPOptions `json:"geoip,omitempty"` Geosite *GeositeOptions `json:"geosite,omitempty"` Rules []Rule `json:"rules,omitempty"` + RuleSet []RuleSet `json:"rule_set,omitempty"` Final string `json:"final,omitempty"` FindProcess bool `json:"find_process,omitempty"` AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` diff --git a/option/rule.go b/option/rule.go index 8caba96e57..0ea133c75f 100644 --- a/option/rule.go +++ b/option/rule.go @@ -3,10 +3,10 @@ package option import ( "reflect" - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) type _Rule struct { @@ -48,40 +48,55 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error { } err = UnmarshallExcluded(bytes, (*_Rule)(r), v) if err != nil { - return E.Cause(err, "route rule") + return err } return nil } +func (r Rule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + type DefaultRule struct { - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - GeoIP Listable[string] `json:"geoip,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - IPCIDR Listable[string] `json:"ip_cidr,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + Network Listable[string] `json:"network,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + Protocol Listable[string] `json:"protocol,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + Geosite Listable[string] `json:"geosite,omitempty"` + SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` + GeoIP Listable[string] `json:"geoip,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + User Listable[string] `json:"user,omitempty"` + UserID Listable[int32] `json:"user_id,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` } func (r DefaultRule) IsValid() bool { @@ -92,12 +107,12 @@ func (r DefaultRule) IsValid() bool { } type LogicalRule struct { - Mode string `json:"mode"` - Rules []DefaultRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Mode string `json:"mode"` + Rules []Rule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` } func (r LogicalRule) IsValid() bool { - return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid) + return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid) } diff --git a/option/rule_dns.go b/option/rule_dns.go index ba572b9aa2..443f931475 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -3,10 +3,10 @@ package option import ( "reflect" - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) type _DNSRule struct { @@ -48,42 +48,55 @@ func (r *DNSRule) UnmarshalJSON(bytes []byte) error { } err = UnmarshallExcluded(bytes, (*_DNSRule)(r), v) if err != nil { - return E.Cause(err, "dns route rule") + return err } return nil } +func (r DNSRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault: + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown DNS rule type: " + r.Type) + } +} + type DefaultDNSRule struct { - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - Outbound Listable[string] `json:"outbound,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` + Network Listable[string] `json:"network,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + Protocol Listable[string] `json:"protocol,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + Geosite Listable[string] `json:"geosite,omitempty"` + SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + User Listable[string] `json:"user,omitempty"` + UserID Listable[int32] `json:"user_id,omitempty"` + Outbound Listable[string] `json:"outbound,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` + Invert bool `json:"invert,omitempty"` + Server string `json:"server,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` } func (r DefaultDNSRule) IsValid() bool { @@ -96,14 +109,14 @@ func (r DefaultDNSRule) IsValid() bool { } type LogicalDNSRule struct { - Mode string `json:"mode"` - Rules []DefaultDNSRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + Mode string `json:"mode"` + Rules []DNSRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Server string `json:"server,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` } func (r LogicalDNSRule) IsValid() bool { - return len(r.Rules) > 0 && common.All(r.Rules, DefaultDNSRule.IsValid) + return len(r.Rules) > 0 && common.All(r.Rules, DNSRule.IsValid) } diff --git a/option/rule_set.go b/option/rule_set.go new file mode 100644 index 0000000000..8e367e69a5 --- /dev/null +++ b/option/rule_set.go @@ -0,0 +1,230 @@ +package option + +import ( + "reflect" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/domain" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json" + + "go4.org/netipx" +) + +type _RuleSet struct { + Type string `json:"type"` + Tag string `json:"tag"` + Format string `json:"format"` + LocalOptions LocalRuleSet `json:"-"` + RemoteOptions RemoteRuleSet `json:"-"` +} + +type RuleSet _RuleSet + +func (r RuleSet) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleSetTypeLocal: + v = r.LocalOptions + case C.RuleSetTypeRemote: + v = r.RemoteOptions + default: + return nil, E.New("unknown rule set type: " + r.Type) + } + return MarshallObjects((_RuleSet)(r), v) +} + +func (r *RuleSet) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_RuleSet)(r)) + if err != nil { + return err + } + if r.Tag == "" { + return E.New("missing 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) + } + var v any + switch r.Type { + 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) + } + err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v) + if err != nil { + return err + } + return nil +} + +type LocalRuleSet struct { + Path string `json:"path,omitempty"` +} + +type RemoteRuleSet struct { + URL string `json:"url"` + DownloadDetour string `json:"download_detour,omitempty"` + UpdateInterval Duration `json:"update_interval,omitempty"` +} + +type _HeadlessRule struct { + Type string `json:"type,omitempty"` + DefaultOptions DefaultHeadlessRule `json:"-"` + LogicalOptions LogicalHeadlessRule `json:"-"` +} + +type HeadlessRule _HeadlessRule + +func (r HeadlessRule) MarshalJSON() ([]byte, error) { + var v any + switch r.Type { + case C.RuleTypeDefault: + r.Type = "" + v = r.DefaultOptions + case C.RuleTypeLogical: + v = r.LogicalOptions + default: + return nil, E.New("unknown rule type: " + r.Type) + } + return MarshallObjects((_HeadlessRule)(r), v) +} + +func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_HeadlessRule)(r)) + if err != nil { + return err + } + var v any + switch r.Type { + case "", C.RuleTypeDefault: + r.Type = C.RuleTypeDefault + v = &r.DefaultOptions + case C.RuleTypeLogical: + v = &r.LogicalOptions + default: + return E.New("unknown rule type: " + r.Type) + } + err = UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v) + if err != nil { + return err + } + return nil +} + +func (r HeadlessRule) IsValid() bool { + switch r.Type { + case C.RuleTypeDefault, "": + return r.DefaultOptions.IsValid() + case C.RuleTypeLogical: + return r.LogicalOptions.IsValid() + default: + panic("unknown rule type: " + r.Type) + } +} + +type DefaultHeadlessRule struct { + QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` + Network Listable[string] `json:"network,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + Invert bool `json:"invert,omitempty"` + + DomainMatcher *domain.Matcher `json:"-"` + SourceIPSet *netipx.IPSet `json:"-"` + IPSet *netipx.IPSet `json:"-"` +} + +func (r DefaultHeadlessRule) IsValid() bool { + var defaultValue DefaultHeadlessRule + defaultValue.Invert = r.Invert + return !reflect.DeepEqual(r, defaultValue) +} + +type LogicalHeadlessRule struct { + Mode string `json:"mode"` + Rules []HeadlessRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` +} + +func (r LogicalHeadlessRule) IsValid() bool { + return len(r.Rules) > 0 && common.All(r.Rules, HeadlessRule.IsValid) +} + +type _PlainRuleSetCompat struct { + Version int `json:"version"` + Options PlainRuleSet `json:"-"` +} + +type PlainRuleSetCompat _PlainRuleSetCompat + +func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { + var v any + switch r.Version { + case C.RuleSetVersion1: + v = r.Options + default: + return nil, E.New("unknown rule set version: ", r.Version) + } + return MarshallObjects((_PlainRuleSetCompat)(r), v) +} + +func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_PlainRuleSetCompat)(r)) + if err != nil { + return err + } + var v any + switch r.Version { + case C.RuleSetVersion1: + v = &r.Options + case 0: + return E.New("missing rule set version") + default: + return E.New("unknown rule set version: ", r.Version) + } + err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) + if err != nil { + return err + } + return nil +} + +func (r PlainRuleSetCompat) Upgrade() PlainRuleSet { + var result PlainRuleSet + switch r.Version { + case C.RuleSetVersion1: + result = r.Options + default: + panic("unknown rule set version: " + F.ToString(r.Version)) + } + return result +} + +type PlainRuleSet struct { + Rules []HeadlessRule `json:"rules,omitempty"` +} diff --git a/option/shadowtls.go b/option/shadowtls.go index 2b288bd8b1..a8be77406c 100644 --- a/option/shadowtls.go +++ b/option/shadowtls.go @@ -23,7 +23,7 @@ type ShadowTLSHandshakeOptions struct { type ShadowTLSOutboundOptions struct { DialerOptions ServerOptions - Version int `json:"version,omitempty"` - Password string `json:"password,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` + Version int `json:"version,omitempty"` + Password string `json:"password,omitempty"` + OutboundTLSOptionsContainer } diff --git a/option/simple.go b/option/simple.go index b8692e0cf1..ba9d6bf190 100644 --- a/option/simple.go +++ b/option/simple.go @@ -9,9 +9,9 @@ type SocksInboundOptions struct { type HTTPMixedInboundOptions struct { ListenOptions - Users []auth.User `json:"users,omitempty"` - SetSystemProxy bool `json:"set_system_proxy,omitempty"` - TLS *InboundTLSOptions `json:"tls,omitempty"` + Users []auth.User `json:"users,omitempty"` + SetSystemProxy bool `json:"set_system_proxy,omitempty"` + InboundTLSOptionsContainer } type SocksOutboundOptions struct { @@ -27,9 +27,9 @@ type SocksOutboundOptions struct { type HTTPOutboundOptions struct { DialerOptions ServerOptions - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - Path string `json:"path,omitempty"` - Headers HTTPHeader `json:"headers,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + OutboundTLSOptionsContainer + Path string `json:"path,omitempty"` + Headers HTTPHeader `json:"headers,omitempty"` } diff --git a/option/time_unit.go b/option/time_unit.go new file mode 100644 index 0000000000..5e531dadf1 --- /dev/null +++ b/option/time_unit.go @@ -0,0 +1,226 @@ +package option + +import ( + "errors" + "time" +) + +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +const durationDay = 24 * time.Hour + +var unitMap = map[string]uint64{ + "ns": uint64(time.Nanosecond), + "us": uint64(time.Microsecond), + "µs": uint64(time.Microsecond), // U+00B5 = micro symbol + "μs": uint64(time.Microsecond), // U+03BC = Greek letter mu + "ms": uint64(time.Millisecond), + "s": uint64(time.Second), + "m": uint64(time.Minute), + "h": uint64(time.Hour), + "d": uint64(durationDay), +} + +// ParseDuration parses a duration string. +// A duration string is a possibly signed sequence of +// decimal numbers, each with optional fraction and a unit suffix, +// such as "300ms", "-1.5h" or "2h45m". +// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". +func ParseDuration(s string) (Duration, error) { + // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ + orig := s + var d uint64 + neg := false + + // Consume [-+]? + if s != "" { + c := s[0] + if c == '-' || c == '+' { + neg = c == '-' + s = s[1:] + } + } + // Special case: if all that is left is "0", this is zero. + if s == "0" { + return 0, nil + } + if s == "" { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + for s != "" { + var ( + v, f uint64 // integers before, after decimal point + scale float64 = 1 // value = v + f/scale + ) + + var err error + + // The next character must be [0-9.] + if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + // Consume [0-9]* + pl := len(s) + v, s, err = leadingInt(s) + if err != nil { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + pre := pl != len(s) // whether we consumed anything before a period + + // Consume (\.[0-9]*)? + post := false + if s != "" && s[0] == '.' { + s = s[1:] + pl := len(s) + f, scale, s = leadingFraction(s) + post = pl != len(s) + } + if !pre && !post { + // no digits (e.g. ".s" or "-.s") + return 0, errors.New("time: invalid duration " + quote(orig)) + } + + // Consume unit. + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c == '.' || '0' <= c && c <= '9' { + break + } + } + if i == 0 { + return 0, errors.New("time: missing unit in duration " + quote(orig)) + } + u := s[:i] + s = s[i:] + unit, ok := unitMap[u] + if !ok { + return 0, errors.New("time: unknown unit " + quote(u) + " in duration " + quote(orig)) + } + if v > 1<<63/unit { + // overflow + return 0, errors.New("time: invalid duration " + quote(orig)) + } + v *= unit + if f > 0 { + // float64 is needed to be nanosecond accurate for fractions of hours. + // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit) + v += uint64(float64(f) * (float64(unit) / scale)) + if v > 1<<63 { + // overflow + return 0, errors.New("time: invalid duration " + quote(orig)) + } + } + d += v + if d > 1<<63 { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + } + if neg { + return -Duration(d), nil + } + if d > 1<<63-1 { + return 0, errors.New("time: invalid duration " + quote(orig)) + } + return Duration(d), nil +} + +var errLeadingInt = errors.New("time: bad [0-9]*") // never printed + +// leadingInt consumes the leading [0-9]* from s. +func leadingInt[bytes []byte | string](s bytes) (x uint64, rem bytes, err error) { + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if x > 1<<63/10 { + // overflow + return 0, rem, errLeadingInt + } + x = x*10 + uint64(c) - '0' + if x > 1<<63 { + // overflow + return 0, rem, errLeadingInt + } + } + return x, s[i:], nil +} + +// leadingFraction consumes the leading [0-9]* from s. +// It is used only for fractions, so does not return an error on overflow, +// it just stops accumulating precision. +func leadingFraction(s string) (x uint64, scale float64, rem string) { + i := 0 + scale = 1 + overflow := false + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if overflow { + continue + } + if x > (1<<63-1)/10 { + // It's possible for overflow to give a positive number, so take care. + overflow = true + continue + } + y := x*10 + uint64(c) - '0' + if y > 1<<63 { + overflow = true + continue + } + x = y + scale *= 10 + } + return x, scale, s[i:] +} + +// These are borrowed from unicode/utf8 and strconv and replicate behavior in +// that package, since we can't take a dependency on either. +const ( + lowerhex = "0123456789abcdef" + runeSelf = 0x80 + runeError = '\uFFFD' +) + +func quote(s string) string { + buf := make([]byte, 1, len(s)+2) // slice will be at least len(s) + quotes + buf[0] = '"' + for i, c := range s { + if c >= runeSelf || c < ' ' { + // This means you are asking us to parse a time.Duration or + // time.Location with unprintable or non-ASCII characters in it. + // We don't expect to hit this case very often. We could try to + // reproduce strconv.Quote's behavior with full fidelity but + // given how rarely we expect to hit these edge cases, speed and + // conciseness are better. + var width int + if c == runeError { + width = 1 + if i+2 < len(s) && s[i:i+3] == string(runeError) { + width = 3 + } + } else { + width = len(string(c)) + } + for j := 0; j < width; j++ { + buf = append(buf, `\x`...) + buf = append(buf, lowerhex[s[i+j]>>4]) + buf = append(buf, lowerhex[s[i+j]&0xF]) + } + } else { + if c == '"' || c == '\\' { + buf = append(buf, '\\') + } + buf = append(buf, string(c)...) + } + } + buf = append(buf, '"') + return string(buf) +} diff --git a/option/tls.go b/option/tls.go index 396f3cae51..85b9d2e8d0 100644 --- a/option/tls.go +++ b/option/tls.go @@ -17,6 +17,23 @@ type InboundTLSOptions struct { Reality *InboundRealityOptions `json:"reality,omitempty"` } +type InboundTLSOptionsContainer struct { + TLS *InboundTLSOptions `json:"tls,omitempty"` +} + +type InboundTLSOptionsWrapper interface { + TakeInboundTLSOptions() *InboundTLSOptions + ReplaceInboundTLSOptions(options *InboundTLSOptions) +} + +func (o *InboundTLSOptionsContainer) TakeInboundTLSOptions() *InboundTLSOptions { + return o.TLS +} + +func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTLSOptions) { + o.TLS = options +} + type OutboundTLSOptions struct { Enabled bool `json:"enabled,omitempty"` DisableSNI bool `json:"disable_sni,omitempty"` @@ -34,6 +51,23 @@ type OutboundTLSOptions struct { TLSTricks *TLSTricksOptions `json:"tls_tricks,omitempty"` } +type OutboundTLSOptionsContainer struct { + TLS *OutboundTLSOptions `json:"tls,omitempty"` +} + +type OutboundTLSOptionsWrapper interface { + TakeOutboundTLSOptions() *OutboundTLSOptions + ReplaceOutboundTLSOptions(options *OutboundTLSOptions) +} + +func (o *OutboundTLSOptionsContainer) TakeOutboundTLSOptions() *OutboundTLSOptions { + return o.TLS +} + +func (o *OutboundTLSOptionsContainer) ReplaceOutboundTLSOptions(options *OutboundTLSOptions) { + o.TLS = options +} + type InboundRealityOptions struct { Enabled bool `json:"enabled,omitempty"` Handshake InboundRealityHandshakeOptions `json:"handshake,omitempty"` diff --git a/option/tls_acme.go b/option/tls_acme.go index 1068237e6b..17d515e20b 100644 --- a/option/tls_acme.go +++ b/option/tls_acme.go @@ -1,9 +1,9 @@ package option import ( - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) type InboundACMEOptions struct { @@ -40,6 +40,8 @@ func (o ACMEDNS01ChallengeOptions) MarshalJSON() ([]byte, error) { v = o.AliDNSOptions case C.DNSProviderCloudflare: v = o.CloudflareOptions + case "": + return nil, E.New("missing provider type") default: return nil, E.New("unknown provider type: " + o.Provider) } @@ -62,7 +64,7 @@ func (o *ACMEDNS01ChallengeOptions) UnmarshalJSON(bytes []byte) error { } err = UnmarshallExcluded(bytes, (*_ACMEDNS01ChallengeOptions)(o), v) if err != nil { - return E.Cause(err, "DNS01 challenge options") + return err } return nil } diff --git a/option/trojan.go b/option/trojan.go index d24ed16aed..7d36c8d205 100644 --- a/option/trojan.go +++ b/option/trojan.go @@ -2,8 +2,8 @@ package option type TrojanInboundOptions struct { ListenOptions - Users []TrojanUser `json:"users,omitempty"` - TLS *InboundTLSOptions `json:"tls,omitempty"` + Users []TrojanUser `json:"users,omitempty"` + InboundTLSOptionsContainer Fallback *ServerOptions `json:"fallback,omitempty"` FallbackForALPN map[string]*ServerOptions `json:"fallback_for_alpn,omitempty"` Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` @@ -18,9 +18,9 @@ type TrojanUser struct { type TrojanOutboundOptions struct { DialerOptions ServerOptions - Password string `json:"password"` - Network NetworkList `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` + Password string `json:"password"` + Network NetworkList `json:"network,omitempty"` + OutboundTLSOptionsContainer Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` Transport *V2RayTransportOptions `json:"transport,omitempty"` } diff --git a/option/tuic.go b/option/tuic.go index 7b26535ba3..b0d5980805 100644 --- a/option/tuic.go +++ b/option/tuic.go @@ -2,12 +2,12 @@ package option type TUICInboundOptions struct { ListenOptions - Users []TUICUser `json:"users,omitempty"` - CongestionControl string `json:"congestion_control,omitempty"` - AuthTimeout Duration `json:"auth_timeout,omitempty"` - ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` - Heartbeat Duration `json:"heartbeat,omitempty"` - TLS *InboundTLSOptions `json:"tls,omitempty"` + Users []TUICUser `json:"users,omitempty"` + CongestionControl string `json:"congestion_control,omitempty"` + AuthTimeout Duration `json:"auth_timeout,omitempty"` + ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` + Heartbeat Duration `json:"heartbeat,omitempty"` + InboundTLSOptionsContainer } type TUICUser struct { @@ -19,14 +19,14 @@ type TUICUser struct { type TUICOutboundOptions struct { DialerOptions ServerOptions - UUID string `json:"uuid,omitempty"` - Password string `json:"password,omitempty"` - CongestionControl string `json:"congestion_control,omitempty"` - UDPRelayMode string `json:"udp_relay_mode,omitempty"` - UDPOverStream bool `json:"udp_over_stream,omitempty"` - ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` - Heartbeat Duration `json:"heartbeat,omitempty"` - Network NetworkList `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - TurnRelay *TurnRelayOptions`json:"turn_relay,omitempty"` + UUID string `json:"uuid,omitempty"` + Password string `json:"password,omitempty"` + CongestionControl string `json:"congestion_control,omitempty"` + UDPRelayMode string `json:"udp_relay_mode,omitempty"` + UDPOverStream bool `json:"udp_over_stream,omitempty"` + ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` + Heartbeat Duration `json:"heartbeat,omitempty"` + Network NetworkList `json:"network,omitempty"` + OutboundTLSOptionsContainer + TurnRelay *TurnRelayOptions`json:"turn_relay,omitempty"` //hiddify } diff --git a/option/tun.go b/option/tun.go index c6b9219dd3..ac66a8061c 100644 --- a/option/tun.go +++ b/option/tun.go @@ -5,6 +5,7 @@ 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"` diff --git a/option/types.go b/option/types.go index f2fed66309..aba445eead 100644 --- a/option/types.go +++ b/option/types.go @@ -6,10 +6,10 @@ import ( "strings" "time" - "github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-dns" 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" mDNS "github.com/miekg/dns" @@ -83,7 +83,7 @@ func (v NetworkList) Build() []string { return strings.Split(string(v), "\n") } -type Listable[T comparable] []T +type Listable[T any] []T func (l Listable[T]) MarshalJSON() ([]byte, error) { arrayList := []T(l) @@ -164,7 +164,7 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error { if err != nil { return err } - duration, err := time.ParseDuration(value) + duration, err := ParseDuration(value) if err != nil { return err } @@ -174,6 +174,14 @@ func (d *Duration) UnmarshalJSON(bytes []byte) error { type DNSQueryType uint16 +func (t DNSQueryType) String() string { + typeName, loaded := mDNS.TypeToString[uint16(t)] + if loaded { + return typeName + } + return F.ToString(uint16(t)) +} + func (t DNSQueryType) MarshalJSON() ([]byte, error) { typeName, loaded := mDNS.TypeToString[uint16(t)] if loaded { diff --git a/option/udp_over_tcp.go b/option/udp_over_tcp.go index 79529624f0..e8a7a9726e 100644 --- a/option/udp_over_tcp.go +++ b/option/udp_over_tcp.go @@ -1,7 +1,7 @@ package option import ( - "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/uot" ) diff --git a/option/v2ray.go b/option/v2ray.go index 37f7b8c4b0..774a651d1d 100644 --- a/option/v2ray.go +++ b/option/v2ray.go @@ -1,13 +1 @@ package option - -type V2RayAPIOptions struct { - Listen string `json:"listen,omitempty"` - Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` -} - -type V2RayStatsServiceOptions struct { - Enabled bool `json:"enabled,omitempty"` - Inbounds []string `json:"inbounds,omitempty"` - Outbounds []string `json:"outbounds,omitempty"` - Users []string `json:"users,omitempty"` -} diff --git a/option/v2ray_transport.go b/option/v2ray_transport.go index 63af28a371..fcd81f9462 100644 --- a/option/v2ray_transport.go +++ b/option/v2ray_transport.go @@ -1,13 +1,13 @@ package option import ( - "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" ) type _V2RayTransportOptions struct { - Type string `json:"type,omitempty"` + Type string `json:"type"` HTTPOptions V2RayHTTPOptions `json:"-"` WebsocketOptions V2RayWebsocketOptions `json:"-"` QUICOptions V2RayQUICOptions `json:"-"` @@ -20,8 +20,6 @@ type V2RayTransportOptions _V2RayTransportOptions func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) { var v any switch o.Type { - case "": - return nil, nil case C.V2RayTransportTypeHTTP: v = o.HTTPOptions case C.V2RayTransportTypeWebsocket: @@ -32,6 +30,8 @@ func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) { v = o.GRPCOptions case C.V2RayTransportTypeHTTPUpgrade: v = o.HTTPUpgradeOptions + case "": + return nil, E.New("missing transport type") default: return nil, E.New("unknown transport type: " + o.Type) } @@ -60,7 +60,7 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error { } err = UnmarshallExcluded(bytes, (*_V2RayTransportOptions)(o), v) if err != nil { - return E.Cause(err, "vmess transport options") + return err } return nil } diff --git a/option/vless.go b/option/vless.go index 09010922e9..5acf2aeed0 100644 --- a/option/vless.go +++ b/option/vless.go @@ -2,8 +2,8 @@ package option type VLESSInboundOptions struct { ListenOptions - Users []VLESSUser `json:"users,omitempty"` - TLS *InboundTLSOptions `json:"tls,omitempty"` + Users []VLESSUser `json:"users,omitempty"` + InboundTLSOptionsContainer Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` Transport *V2RayTransportOptions `json:"transport,omitempty"` } @@ -17,10 +17,10 @@ type VLESSUser struct { type VLESSOutboundOptions struct { DialerOptions ServerOptions - UUID string `json:"uuid"` - Flow string `json:"flow,omitempty"` - Network NetworkList `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` + UUID string `json:"uuid"` + Flow string `json:"flow,omitempty"` + Network NetworkList `json:"network,omitempty"` + OutboundTLSOptionsContainer Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` Transport *V2RayTransportOptions `json:"transport,omitempty"` PacketEncoding *string `json:"packet_encoding,omitempty"` diff --git a/option/vmess.go b/option/vmess.go index 8045e9d6da..8a38fb70c8 100644 --- a/option/vmess.go +++ b/option/vmess.go @@ -2,8 +2,8 @@ package option type VMessInboundOptions struct { ListenOptions - Users []VMessUser `json:"users,omitempty"` - TLS *InboundTLSOptions `json:"tls,omitempty"` + Users []VMessUser `json:"users,omitempty"` + InboundTLSOptionsContainer Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` Transport *V2RayTransportOptions `json:"transport,omitempty"` } @@ -17,14 +17,14 @@ type VMessUser struct { type VMessOutboundOptions struct { DialerOptions ServerOptions - UUID string `json:"uuid"` - Security string `json:"security"` - AlterId int `json:"alter_id,omitempty"` - GlobalPadding bool `json:"global_padding,omitempty"` - AuthenticatedLength bool `json:"authenticated_length,omitempty"` - Network NetworkList `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - PacketEncoding string `json:"packet_encoding,omitempty"` - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` + UUID string `json:"uuid"` + Security string `json:"security"` + AlterId int `json:"alter_id,omitempty"` + GlobalPadding bool `json:"global_padding,omitempty"` + AuthenticatedLength bool `json:"authenticated_length,omitempty"` + Network NetworkList `json:"network,omitempty"` + OutboundTLSOptionsContainer + PacketEncoding string `json:"packet_encoding,omitempty"` + Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` } diff --git a/option/wireguard.go b/option/wireguard.go index 9c43d60d07..84b485474e 100644 --- a/option/wireguard.go +++ b/option/wireguard.go @@ -5,6 +5,7 @@ import "net/netip" type WireGuardOutboundOptions struct { DialerOptions SystemInterface bool `json:"system_interface,omitempty"` + GSO bool `json:"gso,omitempty"` InterfaceName string `json:"interface_name,omitempty"` LocalAddress Listable[netip.Prefix] `json:"local_address"` PrivateKey string `json:"private_key"` diff --git a/outbound/builder.go b/outbound/builder.go index 141758d870..e4d6a80e06 100644 --- a/outbound/builder.go +++ b/outbound/builder.go @@ -56,7 +56,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t case C.TypeHysteria2: return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options) case C.TypeSelector: - return NewSelector(router, logger, tag, options.SelectorOptions) + return NewSelector(ctx, router, logger, tag, options.SelectorOptions) case C.TypeURLTest: return NewURLTest(ctx, router, logger, tag, options.URLTestOptions) default: diff --git a/outbound/dns.go b/outbound/dns.go index 3b2ad5e3ca..fcb67d4527 100644 --- a/outbound/dns.go +++ b/outbound/dns.go @@ -111,6 +111,9 @@ func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metada } } if readWaiter, created := bufio.CreatePacketReadWaiter(reader); created { + readWaiter.InitializeReadWaiter(N.ReadWaitOptions{ + MTU: dns.FixedPacketSize, + }) return d.newPacketConnection(ctx, conn, readWaiter, counters, cachedPackets, metadata) } break @@ -165,6 +168,7 @@ func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metada } timeout.Update() responseBuffer := buf.NewPacket() + responseBuffer.Resize(1024, 0) n, err := response.PackBuffer(responseBuffer.FreeBytes()) if err != nil { cancel(err) @@ -192,17 +196,13 @@ func (d *DNS) newPacketConnection(ctx context.Context, conn N.PacketConn, readWa timeout := canceler.New(fastClose, cancel, C.DNSTimeout) var group task.Group group.Append0(func(ctx context.Context) error { - var buffer *buf.Buffer - readWaiter.InitializeReadWaiter(func() *buf.Buffer { - buffer = buf.NewSize(dns.FixedPacketSize) - buffer.FullReset() - return buffer - }) - defer readWaiter.InitializeReadWaiter(nil) for { - var message mDNS.Msg - var destination M.Socksaddr - var err error + var ( + message mDNS.Msg + destination M.Socksaddr + err error + buffer *buf.Buffer + ) if len(cached) > 0 { packet := cached[0] cached = cached[1:] @@ -217,9 +217,8 @@ func (d *DNS) newPacketConnection(ctx context.Context, conn N.PacketConn, readWa } destination = packet.Destination } else { - destination, err = readWaiter.WaitReadPacket() + buffer, destination, err = readWaiter.WaitReadPacket() if err != nil { - buffer.Release() cancel(err) return err } @@ -243,6 +242,7 @@ func (d *DNS) newPacketConnection(ctx context.Context, conn N.PacketConn, readWa } timeout.Update() responseBuffer := buf.NewPacket() + responseBuffer.Resize(1024, 0) n, err := response.PackBuffer(responseBuffer.FreeBytes()) if err != nil { cancel(err) diff --git a/outbound/proxy.go b/outbound/proxy.go index dae0d9c5db..6127f0f215 100644 --- a/outbound/proxy.go +++ b/outbound/proxy.go @@ -30,7 +30,7 @@ type ProxyListener struct { tcpListener *net.TCPListener username string password string - authenticator auth.Authenticator + authenticator *auth.Authenticator } func NewProxyListener(ctx context.Context, logger log.ContextLogger, dialer N.Dialer) *ProxyListener { diff --git a/outbound/selector.go b/outbound/selector.go index c66591cdee..e801daeadc 100644 --- a/outbound/selector.go +++ b/outbound/selector.go @@ -12,6 +12,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" ) var ( @@ -21,6 +22,7 @@ var ( type Selector struct { myOutboundAdapter + ctx context.Context tags []string defaultTag string outbounds map[string]adapter.Outbound @@ -29,7 +31,7 @@ type Selector struct { interruptExternalConnections bool } -func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { +func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { outbound := &Selector{ myOutboundAdapter: myOutboundAdapter{ protocol: C.TypeSelector, @@ -38,6 +40,7 @@ func NewSelector(router adapter.Router, logger log.ContextLogger, tag string, op tag: tag, dependencies: options.Outbounds, }, + ctx: ctx, tags: options.Outbounds, defaultTag: options.Default, outbounds: make(map[string]adapter.Outbound), @@ -67,8 +70,9 @@ func (s *Selector) Start() error { } if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - selected := clashServer.CacheFile().LoadSelected(s.tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + selected := cacheFile.LoadSelected(s.tag) if selected != "" { detour, loaded := s.outbounds[selected] if loaded { @@ -110,8 +114,9 @@ func (s *Selector) SelectOutbound(tag string) bool { } s.selected = detour if s.tag != "" { - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreSelected() { - err := clashServer.CacheFile().StoreSelected(s.tag, tag) + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err := cacheFile.StoreSelected(s.tag, tag) if err != nil { s.logger.Error("store selected: ", err) } diff --git a/outbound/urltest.go b/outbound/urltest.go index 0d0979695a..ea669b2b91 100644 --- a/outbound/urltest.go +++ b/outbound/urltest.go @@ -35,6 +35,7 @@ type URLTest struct { link string interval time.Duration tolerance uint16 + idleTimeout time.Duration group *URLTestGroup interruptExternalConnections bool } @@ -54,6 +55,7 @@ func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLo link: options.URL, interval: time.Duration(options.Interval), tolerance: options.Tolerance, + idleTimeout: time.Duration(options.IdleTimeout), interruptExternalConnections: options.InterruptExistConnections, } if len(outbound.tags) == 0 { @@ -71,7 +73,21 @@ func (s *URLTest) Start() error { } outbounds = append(outbounds, detour) } - s.group = NewURLTestGroup(s.ctx, s.router, s.logger, outbounds, s.link, s.interval, s.tolerance, s.interruptExternalConnections) + group, err := NewURLTestGroup( + s.ctx, + s.router, + s.logger, + outbounds, + s.link, + s.interval, + s.tolerance, + s.idleTimeout, + s.interruptExternalConnections, + ) + if err != nil { + return err + } + s.group = group return nil } @@ -160,6 +176,7 @@ type URLTestGroup struct { link string interval time.Duration tolerance uint16 + idleTimeout time.Duration history *urltest.HistoryStorage checking atomic.Bool pauseManager pause.Manager @@ -183,14 +200,21 @@ func NewURLTestGroup( link string, interval time.Duration, tolerance uint16, + idleTimeout time.Duration, interruptExternalConnections bool, -) *URLTestGroup { +) (*URLTestGroup, error) { if interval == 0 { interval = C.DefaultURLTestInterval } if tolerance == 0 { tolerance = 50 } + if idleTimeout == 0 { + idleTimeout = C.DefaultURLTestIdleTimeout + } + if interval > idleTimeout { + return nil, E.New("interval must be less or equal than idle_timeout") + } var history *urltest.HistoryStorage if history = service.PtrFromContext[urltest.HistoryStorage](ctx); history != nil { } else if clashServer := router.ClashServer(); clashServer != nil { @@ -206,12 +230,13 @@ func NewURLTestGroup( link: link, interval: interval, tolerance: tolerance, + idleTimeout: idleTimeout, history: history, close: make(chan struct{}), - pauseManager: pause.ManagerFromContext(ctx), + pauseManager: service.FromContext[pause.Manager](ctx), interruptGroup: interrupt.NewGroup(), interruptExternalConnections: interruptExternalConnections, - } + }, nil } func (g *URLTestGroup) PostStart() { @@ -278,6 +303,7 @@ func (g *URLTestGroup) Select(network string) adapter.Outbound { func (g *URLTestGroup) loopCheck() { if time.Now().Sub(g.lastActive.Load()) > g.interval { + g.lastActive.Store(time.Now()) g.CheckOutbounds(false) } for { @@ -286,6 +312,13 @@ func (g *URLTestGroup) loopCheck() { return case <-g.ticker.C: } + if time.Now().Sub(g.lastActive.Load()) > g.idleTimeout { + g.access.Lock() + g.ticker.Stop() + g.ticker = nil + g.access.Unlock() + return + } g.pauseManager.WaitActive() g.CheckOutbounds(false) } diff --git a/outbound/wireguard.go b/outbound/wireguard.go index 0c68da489f..e1f154b5eb 100644 --- a/outbound/wireguard.go +++ b/outbound/wireguard.go @@ -9,6 +9,7 @@ import ( "encoding/hex" "fmt" "net" + "net/netip" "strings" "github.com/sagernet/sing-box/adapter" @@ -19,10 +20,13 @@ import ( "github.com/sagernet/sing-box/transport/wireguard" "github.com/sagernet/sing-dns" "github.com/sagernet/sing-tun" - "github.com/sagernet/sing/common/debug" 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/x/list" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" + "github.com/sagernet/wireguard-go/conn" "github.com/sagernet/wireguard-go/device" "github.com/sagernet/sing-box/outbound/houtbound" ) @@ -34,9 +38,18 @@ var ( type WireGuard struct { myOutboundAdapter - bind *wireguard.ClientBind - device *device.Device - tunDevice wireguard.Device + ctx context.Context + workers int + peers []wireguard.PeerConfig + useStdNetBind bool + listener N.Dialer + ipcConf string + + pauseManager pause.Manager + pauseCallback *list.Element[pause.Callback] + bind conn.Bind + device *device.Device + tunDevice wireguard.Device hforwarder *houtbound.Forwarder } @@ -51,33 +64,31 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context tag: tag, dependencies: withDialerDependency(options.DialerOptions), }, - hforwarder: hforwarder, - } - var reserved [3]uint8 - if len(options.Reserved) > 0 { - if len(options.Reserved) != 3 { - return nil, E.New("invalid reserved value, required 3 bytes, got ", len(options.Reserved)) - } - copy(reserved[:], options.Reserved) - } - var isConnect bool - var connectAddr M.Socksaddr - if len(options.Peers) < 2 { - isConnect = true - if len(options.Peers) == 1 { - connectAddr = options.Peers[0].ServerOptions.Build() - } else { - connectAddr = options.ServerOptions.Build() - } + ctx: ctx, + workers: options.Workers, + pauseManager: service.FromContext[pause.Manager](ctx), + hforwarder: hforwarder,//hiddify } - outboundDialer, err := dialer.New(router, options.DialerOptions) + peers, err := wireguard.ParsePeers(options) if err != nil { return nil, err } - outbound.bind = wireguard.NewClientBind(ctx, outbound, outboundDialer, isConnect, connectAddr, reserved) + outbound.peers = peers if len(options.LocalAddress) == 0 { return nil, E.New("missing local address") } + if options.GSO { + if options.GSO && options.Detour != "" { + return nil, E.New("gso is conflict with detour") + } + options.IsWireGuardListener = true + outbound.useStdNetBind = true + } + listener, err := dialer.New(router, options.DialerOptions) + if err != nil { + return nil, err + } + outbound.listener = listener var privateKey string { bytes, err := base64.StdEncoding.DecodeString(options.PrivateKey) @@ -86,80 +97,7 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context } privateKey = hex.EncodeToString(bytes) } - ipcConf := "private_key=" + privateKey - if len(options.Peers) > 0 { - for i, peer := range options.Peers { - var peerPublicKey, preSharedKey string - { - bytes, err := base64.StdEncoding.DecodeString(peer.PublicKey) - if err != nil { - return nil, E.Cause(err, "decode public key for peer ", i) - } - peerPublicKey = hex.EncodeToString(bytes) - } - if peer.PreSharedKey != "" { - bytes, err := base64.StdEncoding.DecodeString(peer.PreSharedKey) - if err != nil { - return nil, E.Cause(err, "decode pre shared key for peer ", i) - } - preSharedKey = hex.EncodeToString(bytes) - } - destination := peer.ServerOptions.Build() - ipcConf += "\npublic_key=" + peerPublicKey - ipcConf += "\nendpoint=" + destination.String() - if preSharedKey != "" { - ipcConf += "\npreshared_key=" + preSharedKey - } - if len(peer.AllowedIPs) == 0 { - return nil, E.New("missing allowed_ips for peer ", i) - } - for _, allowedIP := range peer.AllowedIPs { - ipcConf += "\nallowed_ip=" + allowedIP - } - if len(peer.Reserved) > 0 { - if len(peer.Reserved) != 3 { - return nil, E.New("invalid reserved value for peer ", i, ", required 3 bytes, got ", len(peer.Reserved)) - } - copy(reserved[:], options.Reserved) - outbound.bind.SetReservedForEndpoint(destination, reserved) - } - } - } else { - var peerPublicKey, preSharedKey string - { - bytes, err := base64.StdEncoding.DecodeString(options.PeerPublicKey) - if err != nil { - return nil, E.Cause(err, "decode peer public key") - } - peerPublicKey = hex.EncodeToString(bytes) - } - if options.PreSharedKey != "" { - bytes, err := base64.StdEncoding.DecodeString(options.PreSharedKey) - if err != nil { - return nil, E.Cause(err, "decode pre shared key") - } - preSharedKey = hex.EncodeToString(bytes) - } - ipcConf += "\npublic_key=" + peerPublicKey - ipcConf += "\nendpoint=" + options.ServerOptions.Build().String() - if preSharedKey != "" { - ipcConf += "\npreshared_key=" + preSharedKey - } - var has4, has6 bool - for _, address := range options.LocalAddress { - if address.Addr().Is4() { - has4 = true - } else { - has6 = true - } - } - if has4 { - ipcConf += "\nallowed_ip=0.0.0.0/0" - } - if has6 { - ipcConf += "\nallowed_ip=::/0" - } - } + outbound.ipcConf = "private_key=" + privateKey mtu := options.MTU if mtu == 0 { mtu = 1408 @@ -168,36 +106,86 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context if !options.SystemInterface && tun.WithGVisor { wireTunDevice, err = wireguard.NewStackDevice(options.LocalAddress, mtu) } else { - wireTunDevice, err = wireguard.NewSystemDevice(router, options.InterfaceName, options.LocalAddress, mtu) + wireTunDevice, err = wireguard.NewSystemDevice(router, options.InterfaceName, options.LocalAddress, mtu, options.GSO) } if err != nil { return nil, E.Cause(err, "create WireGuard device") } - wgDevice := device.NewDevice(ctx, wireTunDevice, outbound.bind, &device.Logger{ + outbound.tunDevice = wireTunDevice + return outbound, nil +} + +func (w *WireGuard) Start() error { + err := wireguard.ResolvePeers(w.ctx, w.router, w.peers) + if err != nil { + return err + } + var bind conn.Bind + if w.useStdNetBind { + bind = conn.NewStdNetBind(w.listener.(dialer.WireGuardListener)) + } else { + var ( + isConnect bool + connectAddr netip.AddrPort + reserved [3]uint8 + ) + peerLen := len(w.peers) + if peerLen == 1 { + isConnect = true + connectAddr = w.peers[0].Endpoint + reserved = w.peers[0].Reserved + } + bind = wireguard.NewClientBind(w.ctx, w, w.listener, isConnect, connectAddr, reserved) + } + wgDevice := device.NewDevice(w.tunDevice, bind, &device.Logger{ Verbosef: func(format string, args ...interface{}) { - logger.Debug(fmt.Sprintf(strings.ToLower(format), args...)) + w.logger.Debug(fmt.Sprintf(strings.ToLower(format), args...)) }, Errorf: func(format string, args ...interface{}) { - logger.Error(fmt.Sprintf(strings.ToLower(format), args...)) + w.logger.Error(fmt.Sprintf(strings.ToLower(format), args...)) }, - }, options.Workers) - if debug.Enabled { - logger.Trace("created wireguard ipc conf: \n", ipcConf) + }, w.workers) + ipcConf := w.ipcConf + for _, peer := range w.peers { + ipcConf += peer.GenerateIpcLines() } err = wgDevice.IpcSet(ipcConf) if err != nil { - return nil, E.Cause(err, "setup wireguard") + return E.Cause(err, "setup wireguard: \n", ipcConf) } - outbound.device = wgDevice - outbound.tunDevice = wireTunDevice - return outbound, nil + w.device = wgDevice + w.pauseCallback = w.pauseManager.RegisterCallback(w.onPauseUpdated) + return w.tunDevice.Start() +} + +func (w *WireGuard) Close() error { + if w.hforwarder != nil {//hiddify + w.hforwarder.Close()//hiddify + }//hiddify + if w.device != nil { + w.device.Close() + } + if w.pauseCallback != nil { + w.pauseManager.UnregisterCallback(w.pauseCallback) + } + w.tunDevice.Close() + return nil } func (w *WireGuard) InterfaceUpdated() { - w.bind.Reset() + w.device.BindUpdate() return } +func (w *WireGuard) onPauseUpdated(event int) { + switch event { + case pause.EventDevicePaused: + w.device.Down() + case pause.EventDeviceWake: + w.device.Up() + } +} + func (w *WireGuard) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { switch network { case N.NetworkTCP: @@ -238,18 +226,3 @@ func (w *WireGuard) NewConnection(ctx context.Context, conn net.Conn, metadata a func (w *WireGuard) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { return NewDirectPacketConnection(ctx, w.router, w, conn, metadata, dns.DomainStrategyAsIS) } - -func (w *WireGuard) Start() error { - return w.tunDevice.Start() -} - -func (w *WireGuard) Close() error { - if w.hforwarder != nil { - w.hforwarder.Close() - } - if w.device != nil { - w.device.Close() - } - w.tunDevice.Close() - return nil -} diff --git a/release/config/sing-box.service b/release/config/sing-box.service index 76ab695f1c..7b7a13a855 100644 --- a/release/config/sing-box.service +++ b/release/config/sing-box.service @@ -1,7 +1,7 @@ [Unit] Description=sing-box service Documentation=https://sing-box.sagernet.org -After=network.target nss-lookup.target +After=network.target nss-lookup.target network-online.target [Service] CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH diff --git a/release/config/sing-box@.service b/release/config/sing-box@.service index 423ec45858..578ebd1cb5 100644 --- a/release/config/sing-box@.service +++ b/release/config/sing-box@.service @@ -1,7 +1,7 @@ [Unit] Description=sing-box service Documentation=https://sing-box.sagernet.org -After=network.target nss-lookup.target +After=network.target nss-lookup.target network-online.target [Service] CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH diff --git a/release/local/sing-box.service b/release/local/sing-box.service index 7ff912539a..7dfd6f7950 100644 --- a/release/local/sing-box.service +++ b/release/local/sing-box.service @@ -1,7 +1,7 @@ [Unit] Description=sing-box service Documentation=https://sing-box.sagernet.org -After=network.target nss-lookup.target +After=network.target nss-lookup.target network-online.target [Service] CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH diff --git a/route/router.go b/route/router.go index a389aaee35..adbdbd2082 100644 --- a/route/router.go +++ b/route/router.go @@ -18,6 +18,7 @@ import ( "github.com/sagernet/sing-box/common/geosite" "github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/common/sniff" + "github.com/sagernet/sing-box/common/taskmonitor" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/log" @@ -39,6 +40,7 @@ import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" serviceNTP "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" @@ -64,9 +66,12 @@ type Router struct { geoIPReader *geoip.Reader geositeReader *geosite.Reader geositeCache map[string]adapter.Rule + needFindProcess bool dnsClient *dns.Client defaultDomainStrategy dns.DomainStrategy dnsRules []adapter.DNSRule + ruleSets []adapter.RuleSet + ruleSetMap map[string]adapter.RuleSet defaultTransport dns.Transport transports []dns.Transport transportMap map[string]dns.Transport @@ -87,6 +92,7 @@ type Router struct { v2rayServer adapter.V2RayServer platformInterface platform.Interface needWIFIState bool + needPackageManager bool wifiState adapter.WIFIState started bool } @@ -107,19 +113,24 @@ func NewRouter( outboundByTag: make(map[string]adapter.Outbound), rules: make([]adapter.Rule, 0, len(options.Rules)), dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)), + ruleSetMap: make(map[string]adapter.RuleSet), needGeoIPDatabase: hasRule(options.Rules, isGeoIPRule) || hasDNSRule(dnsOptions.Rules, isGeoIPDNSRule), needGeositeDatabase: hasRule(options.Rules, isGeositeRule) || hasDNSRule(dnsOptions.Rules, isGeositeDNSRule), geoIPOptions: common.PtrValueOrDefault(options.GeoIP), geositeOptions: common.PtrValueOrDefault(options.Geosite), geositeCache: make(map[string]adapter.Rule), + needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess, defaultDetour: options.Final, defaultDomainStrategy: dns.DomainStrategy(dnsOptions.Strategy), autoDetectInterface: options.AutoDetectInterface, defaultInterface: options.DefaultInterface, defaultMark: options.DefaultMark, - pauseManager: pause.ManagerFromContext(ctx), + pauseManager: service.FromContext[pause.Manager](ctx), platformInterface: platformInterface, needWIFIState: hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule), + needPackageManager: C.IsAndroid && platformInterface == nil && common.Any(inbounds, func(inbound option.Inbound) bool { + return len(inbound.TunOptions.IncludePackage) > 0 || len(inbound.TunOptions.ExcludePackage) > 0 + }), } router.dnsClient = dns.NewClient(dns.ClientOptions{ DisableCache: dnsOptions.DNSClientOptions.DisableCache, @@ -128,19 +139,30 @@ func NewRouter( Logger: router.dnsLogger, }) for i, ruleOptions := range options.Rules { - routeRule, err := NewRule(router, router.logger, ruleOptions) + routeRule, err := NewRule(router, router.logger, ruleOptions, true) if err != nil { return nil, E.Cause(err, "parse rule[", i, "]") } router.rules = append(router.rules, routeRule) } for i, dnsRuleOptions := range dnsOptions.Rules { - dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions) + dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions, true) if err != nil { return nil, E.Cause(err, "parse dns rule[", i, "]") } router.dnsRules = append(router.dnsRules, dnsRule) } + for i, ruleSetOptions := range options.RuleSet { + if _, exists := router.ruleSetMap[ruleSetOptions.Tag]; exists { + return nil, E.New("duplicate rule-set tag: ", ruleSetOptions.Tag) + } + ruleSet, err := NewRuleSet(ctx, router, router.logger, ruleSetOptions) + if err != nil { + return nil, E.Cause(err, "parse rule-set[", i, "]") + } + router.ruleSets = append(router.ruleSets, ruleSet) + router.ruleSetMap[ruleSetOptions.Tag] = ruleSet + } transports := make([]dns.Transport, len(dnsOptions.Servers)) dummyTransportMap := make(map[string]dns.Transport) @@ -244,6 +266,9 @@ func NewRouter( } defaultTransport = transports[0] } + if _, isFakeIP := defaultTransport.(adapter.FakeIPTransport); isFakeIP { + return nil, E.New("default DNS server cannot be fakeip") + } router.defaultTransport = defaultTransport router.transports = transports router.transportMap = transportMap @@ -262,7 +287,7 @@ func NewRouter( if fakeIPOptions.Inet6Range != nil { inet6Range = *fakeIPOptions.Inet6Range } - router.fakeIPStore = fakeip.NewStore(router, router.logger, inet4Range, inet6Range) + router.fakeIPStore = fakeip.NewStore(ctx, router.logger, inet4Range, inet6Range) } usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor() @@ -296,34 +321,6 @@ func NewRouter( router.interfaceMonitor = interfaceMonitor } - needFindProcess := hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess - needPackageManager := C.IsAndroid && platformInterface == nil && (needFindProcess || common.Any(inbounds, func(inbound option.Inbound) bool { - return len(inbound.TunOptions.IncludePackage) > 0 || len(inbound.TunOptions.ExcludePackage) > 0 - })) - if needPackageManager { - packageManager, err := tun.NewPackageManager(router) - if err != nil { - return nil, E.Cause(err, "create package manager") - } - router.packageManager = packageManager - } - if needFindProcess { - if platformInterface != nil { - router.processSearcher = platformInterface - } else { - searcher, err := process.NewSearcher(process.Config{ - Logger: logFactory.NewLogger("router/process"), - PackageManager: router.packageManager, - }) - if err != nil { - if err != os.ErrInvalid { - router.logger.Warn(E.Cause(err, "create process searcher")) - } - } else { - router.processSearcher = searcher - } - } - } if ntpOptions.Enabled { timeService, err := ntp.NewService(ctx, router, logFactory.NewLogger("ntp"), ntpOptions) if err != nil { @@ -332,11 +329,6 @@ func NewRouter( service.ContextWith[serviceNTP.TimeService](ctx, timeService) router.timeService = timeService } - if platformInterface != nil && router.interfaceMonitor != nil && router.needWIFIState { - router.interfaceMonitor.RegisterCallback(func(_ int) { - router.updateWIFIState() - }) - } return router, nil } @@ -426,33 +418,49 @@ func (r *Router) Outbounds() []adapter.Outbound { return r.outbounds } -func (r *Router) Start() error { - if r.needGeoIPDatabase { - err := r.prepareGeoIPDatabase() +func (r *Router) PreStart() error { + monitor := taskmonitor.New(r.logger, C.DefaultStartTimeout) + if r.interfaceMonitor != nil { + monitor.Start("initialize interface monitor") + err := r.interfaceMonitor.Start() + monitor.Finish() if err != nil { return err } } - if r.needGeositeDatabase { - err := r.prepareGeositeDatabase() + if r.networkMonitor != nil { + monitor.Start("initialize network monitor") + err := r.networkMonitor.Start() + monitor.Finish() if err != nil { return err } } - if r.interfaceMonitor != nil { - err := r.interfaceMonitor.Start() + if r.fakeIPStore != nil { + monitor.Start("initialize fakeip store") + err := r.fakeIPStore.Start() + monitor.Finish() if err != nil { return err } } - if r.networkMonitor != nil { - err := r.networkMonitor.Start() + return nil +} + +func (r *Router) Start() error { + monitor := taskmonitor.New(r.logger, C.DefaultStartTimeout) + if r.needGeoIPDatabase { + monitor.Start("initialize geoip database") + err := r.prepareGeoIPDatabase() + monitor.Finish() if err != nil { return err } } - if r.packageManager != nil { - err := r.packageManager.Start() + if r.needGeositeDatabase { + monitor.Start("initialize geosite database") + err := r.prepareGeositeDatabase() + monitor.Finish() if err != nil { return err } @@ -477,35 +485,117 @@ func (r *Router) Start() error { r.geositeCache = nil r.geositeReader = nil } - if r.needWIFIState { + + if len(r.ruleSets) > 0 { + monitor.Start("initialize rule-set") + ruleSetStartContext := NewRuleSetStartContext() + var ruleSetStartGroup task.Group + for i, ruleSet := range r.ruleSets { + ruleSetInPlace := ruleSet + ruleSetStartGroup.Append0(func(ctx context.Context) error { + err := ruleSetInPlace.StartContext(ctx, ruleSetStartContext) + if err != nil { + return E.Cause(err, "initialize rule-set[", i, "]") + } + return nil + }) + } + ruleSetStartGroup.Concurrency(5) + ruleSetStartGroup.FastFail() + err := ruleSetStartGroup.Run(r.ctx) + monitor.Finish() + if err != nil { + return err + } + ruleSetStartContext.Close() + } + var ( + needProcessFromRuleSet bool + needWIFIStateFromRuleSet bool + ) + for _, ruleSet := range r.ruleSets { + metadata := ruleSet.Metadata() + if metadata.ContainsProcessRule { + needProcessFromRuleSet = true + } + if metadata.ContainsWIFIRule { + needWIFIStateFromRuleSet = true + } + } + if needProcessFromRuleSet || r.needFindProcess || r.needPackageManager { + if C.IsAndroid && r.platformInterface == nil { + monitor.Start("initialize package manager") + packageManager, err := tun.NewPackageManager(r) + monitor.Finish() + if err != nil { + return E.Cause(err, "create package manager") + } + monitor.Start("start package manager") + err = packageManager.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "start package manager") + } + r.packageManager = packageManager + } + + if r.platformInterface != nil { + r.processSearcher = r.platformInterface + } else { + monitor.Start("initialize process searcher") + searcher, err := process.NewSearcher(process.Config{ + Logger: r.logger, + PackageManager: r.packageManager, + }) + monitor.Finish() + if err != nil { + if err != os.ErrInvalid { + r.logger.Warn(E.Cause(err, "create process searcher")) + } + } else { + r.processSearcher = searcher + } + } + } + if needWIFIStateFromRuleSet || r.needWIFIState { + monitor.Start("initialize WIFI state") + if r.platformInterface != nil && r.interfaceMonitor != nil { + r.interfaceMonitor.RegisterCallback(func(_ int) { + r.updateWIFIState() + }) + } r.updateWIFIState() + monitor.Finish() } + for i, rule := range r.rules { + monitor.Start("initialize rule[", i, "]") err := rule.Start() + monitor.Finish() if err != nil { return E.Cause(err, "initialize rule[", i, "]") } } for i, rule := range r.dnsRules { + monitor.Start("initialize DNS rule[", i, "]") err := rule.Start() + monitor.Finish() if err != nil { return E.Cause(err, "initialize DNS rule[", i, "]") } } - if r.fakeIPStore != nil { - err := r.fakeIPStore.Start() - if err != nil { - return err - } - } for i, transport := range r.transports { + monitor.Start("initialize DNS transport[", i, "]") err := transport.Start() + monitor.Finish() if err != nil { return E.Cause(err, "initialize DNS server[", i, "]") } } if r.timeService != nil { + monitor.Start("initialize time service") err := r.timeService.Start() + monitor.Finish() if err != nil { return E.Cause(err, "initialize time service") } @@ -514,65 +604,83 @@ func (r *Router) Start() error { } func (r *Router) Close() error { + monitor := taskmonitor.New(r.logger, C.DefaultStopTimeout) var err error for i, rule := range r.rules { - r.logger.Trace("closing rule[", i, "]") + monitor.Start("close rule[", i, "]") err = E.Append(err, rule.Close(), func(err error) error { return E.Cause(err, "close rule[", i, "]") }) + monitor.Finish() } for i, rule := range r.dnsRules { - r.logger.Trace("closing dns rule[", i, "]") + monitor.Start("close dns rule[", i, "]") err = E.Append(err, rule.Close(), func(err error) error { return E.Cause(err, "close dns rule[", i, "]") }) + monitor.Finish() } for i, transport := range r.transports { - r.logger.Trace("closing transport[", i, "] ") + monitor.Start("close dns transport[", i, "]") err = E.Append(err, transport.Close(), func(err error) error { return E.Cause(err, "close dns transport[", i, "]") }) + monitor.Finish() } if r.geoIPReader != nil { - r.logger.Trace("closing geoip reader") + monitor.Start("close geoip reader") err = E.Append(err, r.geoIPReader.Close(), func(err error) error { return E.Cause(err, "close geoip reader") }) + monitor.Finish() } if r.interfaceMonitor != nil { - r.logger.Trace("closing interface monitor") + monitor.Start("close interface monitor") err = E.Append(err, r.interfaceMonitor.Close(), func(err error) error { return E.Cause(err, "close interface monitor") }) + monitor.Finish() } if r.networkMonitor != nil { - r.logger.Trace("closing network monitor") + monitor.Start("close network monitor") err = E.Append(err, r.networkMonitor.Close(), func(err error) error { return E.Cause(err, "close network monitor") }) + monitor.Finish() } if r.packageManager != nil { - r.logger.Trace("closing package manager") + monitor.Start("close package manager") err = E.Append(err, r.packageManager.Close(), func(err error) error { return E.Cause(err, "close package manager") }) + monitor.Finish() } if r.timeService != nil { - r.logger.Trace("closing time service") + monitor.Start("close time service") err = E.Append(err, r.timeService.Close(), func(err error) error { return E.Cause(err, "close time service") }) + monitor.Finish() } if r.fakeIPStore != nil { - r.logger.Trace("closing fakeip store") + monitor.Start("close fakeip store") err = E.Append(err, r.fakeIPStore.Close(), func(err error) error { return E.Cause(err, "close fakeip store") }) + monitor.Finish() } return err } func (r *Router) PostStart() error { + if len(r.ruleSets) > 0 { + for i, ruleSet := range r.ruleSets { + err := ruleSet.PostStart() + if err != nil { + return E.Cause(err, "post start rule-set[", i, "]") + } + } + } r.started = true return nil } @@ -582,11 +690,17 @@ func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { return outbound, loaded } -func (r *Router) DefaultOutbound(network string) adapter.Outbound { +func (r *Router) DefaultOutbound(network string) (adapter.Outbound, error) { if network == N.NetworkTCP { - return r.defaultOutboundForConnection + if r.defaultOutboundForConnection == nil { + return nil, E.New("missing default outbound for TCP connections") + } + return r.defaultOutboundForConnection, nil } else { - return r.defaultOutboundForPacketConnection + if r.defaultOutboundForPacketConnection == nil { + return nil, E.New("missing default outbound for UDP connections") + } + return r.defaultOutboundForPacketConnection, nil } } @@ -594,7 +708,16 @@ func (r *Router) FakeIPStore() adapter.FakeIPStore { return r.fakeIPStore } +func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) { + ruleSet, loaded := r.ruleSetMap[tag] + return ruleSet, loaded +} + func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + if r.pauseManager.IsDevicePaused() { + return E.New("reject connection to ", metadata.Destination, " while device paused") + } + if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { return E.New("routing loop on detour: ", metadata.InboundDetour) @@ -652,7 +775,6 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad if metadata.InboundOptions.SniffEnabled { buffer := buf.NewPacket() - buffer.FullReset() sniffMetadata, err := sniff.PeekStream(ctx, conn, buffer, time.Duration(metadata.InboundOptions.SniffTimeout), sniff.StreamDomainNameQuery, sniff.TLSClientHello, sniff.HTTPHost) if sniffMetadata != nil { metadata.Protocol = sniffMetadata.Protocol @@ -720,6 +842,9 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad } func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + if r.pauseManager.IsDevicePaused() { + return E.New("reject packet connection to ", metadata.Destination, " while device paused") + } if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { return E.New("routing loop on detour: ", metadata.InboundDetour) @@ -768,7 +893,6 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m if metadata.InboundOptions.SniffEnabled || metadata.Destination.Addr.IsUnspecified() { buffer := buf.NewPacket() - buffer.FullReset() destination, err := conn.ReadPacket(buffer) if err != nil { buffer.Release() @@ -884,6 +1008,7 @@ func (r *Router) match0(ctx context.Context, metadata *adapter.InboundContext, d } } for i, rule := range r.rules { + metadata.ResetRuleCache() if rule.Match(metadata) { detour := rule.Outbound() r.logger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour) diff --git a/route/router_dns.go b/route/router_dns.go index 1532df94f8..8ae9171002 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -37,12 +37,13 @@ func (m *DNSReverseMapping) Query(address netip.Addr) (string, bool) { return domain, loaded } -func (r *Router) matchDNS(ctx context.Context) (context.Context, dns.Transport, dns.DomainStrategy) { +func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool) (context.Context, dns.Transport, dns.DomainStrategy) { metadata := adapter.ContextFrom(ctx) if metadata == nil { panic("no context") } for i, rule := range r.dnsRules { + metadata.ResetRuleCache() if rule.Match(metadata) { detour := rule.Outbound() transport, loaded := r.transportMap[detour] @@ -50,7 +51,7 @@ func (r *Router) matchDNS(ctx context.Context) (context.Context, dns.Transport, r.dnsLogger.ErrorContext(ctx, "transport not found: ", detour) continue } - if _, isFakeIP := transport.(adapter.FakeIPTransport); isFakeIP && metadata.FakeIP { + if _, isFakeIP := transport.(adapter.FakeIPTransport); isFakeIP && !allowFakeIP { continue } r.dnsLogger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour) @@ -96,7 +97,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er } metadata.Domain = fqdnToDomain(message.Question[0].Name) } - ctx, transport, strategy := r.matchDNS(ctx) + ctx, transport, strategy := r.matchDNS(ctx, true) ctx, cancel := context.WithTimeout(ctx, C.DNSTimeout) defer cancel() response, err = r.dnsClient.Exchange(ctx, transport, message, strategy) @@ -124,7 +125,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS r.dnsLogger.DebugContext(ctx, "lookup domain ", domain) ctx, metadata := adapter.AppendContext(ctx) metadata.Domain = domain - ctx, transport, transportStrategy := r.matchDNS(ctx) + ctx, transport, transportStrategy := r.matchDNS(ctx, false) if strategy == dns.DomainStrategyAsIS { strategy = transportStrategy } diff --git a/route/router_geo_resources.go b/route/router_geo_resources.go index 8715cf922c..e0a572c92f 100644 --- a/route/router_geo_resources.go +++ b/route/router_geo_resources.go @@ -13,8 +13,6 @@ import ( "github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-box/common/geosite" C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/rw" @@ -243,75 +241,3 @@ func (r *Router) downloadGeositeDatabase(savePath string) error { } return err } - -func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool { - for _, rule := range rules { - switch rule.Type { - case C.RuleTypeDefault: - if cond(rule.DefaultOptions) { - return true - } - case C.RuleTypeLogical: - for _, subRule := range rule.LogicalOptions.Rules { - if cond(subRule) { - return true - } - } - } - } - return false -} - -func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool { - for _, rule := range rules { - switch rule.Type { - case C.RuleTypeDefault: - if cond(rule.DefaultOptions) { - return true - } - case C.RuleTypeLogical: - for _, subRule := range rule.LogicalOptions.Rules { - if cond(subRule) { - return true - } - } - } - } - return false -} - -func isGeoIPRule(rule option.DefaultRule) bool { - return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode) -} - -func isGeoIPDNSRule(rule option.DefaultDNSRule) bool { - return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) -} - -func isGeositeRule(rule option.DefaultRule) bool { - return len(rule.Geosite) > 0 -} - -func isGeositeDNSRule(rule option.DefaultDNSRule) bool { - return len(rule.Geosite) > 0 -} - -func isProcessRule(rule option.DefaultRule) bool { - return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 -} - -func isProcessDNSRule(rule option.DefaultDNSRule) bool { - return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 -} - -func notPrivateNode(code string) bool { - return code != "private" -} - -func isWIFIRule(rule option.DefaultRule) bool { - return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 -} - -func isWIFIDNSRule(rule option.DefaultDNSRule) bool { - return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 -} diff --git a/route/router_rule.go b/route/router_rule.go new file mode 100644 index 0000000000..9850b5bc10 --- /dev/null +++ b/route/router_rule.go @@ -0,0 +1,99 @@ +package route + +import ( + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" +) + +func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool { + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if cond(rule.DefaultOptions) { + return true + } + case C.RuleTypeLogical: + if hasRule(rule.LogicalOptions.Rules, cond) { + return true + } + } + } + return false +} + +func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool { + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if cond(rule.DefaultOptions) { + return true + } + case C.RuleTypeLogical: + if hasDNSRule(rule.LogicalOptions.Rules, cond) { + return true + } + } + } + return false +} + +func hasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool { + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if cond(rule.DefaultOptions) { + return true + } + case C.RuleTypeLogical: + if hasHeadlessRule(rule.LogicalOptions.Rules, cond) { + return true + } + } + } + return false +} + +func isGeoIPRule(rule option.DefaultRule) bool { + return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode) +} + +func isGeoIPDNSRule(rule option.DefaultDNSRule) bool { + return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) +} + +func isGeositeRule(rule option.DefaultRule) bool { + return len(rule.Geosite) > 0 +} + +func isGeositeDNSRule(rule option.DefaultDNSRule) bool { + return len(rule.Geosite) > 0 +} + +func isProcessRule(rule option.DefaultRule) bool { + return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 +} + +func isProcessDNSRule(rule option.DefaultDNSRule) bool { + return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 +} + +func isProcessHeadlessRule(rule option.DefaultHeadlessRule) bool { + return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 +} + +func notPrivateNode(code string) bool { + return code != "private" +} + +func isWIFIRule(rule option.DefaultRule) bool { + return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 +} + +func isWIFIDNSRule(rule option.DefaultDNSRule) bool { + return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 +} + +func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool { + return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 +} diff --git a/route/rule_abstract.go b/route/rule_abstract.go index 38d4d57d41..6decb9f37e 100644 --- a/route/rule_abstract.go +++ b/route/rule_abstract.go @@ -1,6 +1,7 @@ package route import ( + "io" "strings" "github.com/sagernet/sing-box/adapter" @@ -16,6 +17,7 @@ type abstractDefaultRule struct { destinationAddressItems []RuleItem destinationPortItems []RuleItem allItems []RuleItem + ruleSetItem RuleItem invert bool outbound string } @@ -61,64 +63,64 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { return true } - for _, item := range r.items { - if !item.Match(metadata) { - return r.invert - } - } - - if len(r.sourceAddressItems) > 0 { - var sourceAddressMatch bool + if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch { for _, item := range r.sourceAddressItems { if item.Match(metadata) { - sourceAddressMatch = true + metadata.SourceAddressMatch = true break } } - if !sourceAddressMatch { - return r.invert - } } - if len(r.sourcePortItems) > 0 { - var sourcePortMatch bool + if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch { for _, item := range r.sourcePortItems { if item.Match(metadata) { - sourcePortMatch = true + metadata.SourcePortMatch = true break } } - if !sourcePortMatch { - return r.invert - } } - if len(r.destinationAddressItems) > 0 { - var destinationAddressMatch bool + if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch { for _, item := range r.destinationAddressItems { if item.Match(metadata) { - destinationAddressMatch = true + metadata.DestinationAddressMatch = true break } } - if !destinationAddressMatch { - return r.invert - } } - if len(r.destinationPortItems) > 0 { - var destinationPortMatch bool + if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch { for _, item := range r.destinationPortItems { if item.Match(metadata) { - destinationPortMatch = true + metadata.DestinationPortMatch = true break } } - if !destinationPortMatch { + } + + for _, item := range r.items { + if !item.Match(metadata) { return r.invert } } + if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch { + return r.invert + } + + if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch { + return r.invert + } + + if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch { + return r.invert + } + + if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch { + return r.invert + } + return !r.invert } @@ -135,7 +137,7 @@ func (r *abstractDefaultRule) String() string { } type abstractLogicalRule struct { - rules []adapter.Rule + rules []adapter.HeadlessRule mode string invert bool outbound string @@ -146,7 +148,10 @@ func (r *abstractLogicalRule) Type() string { } func (r *abstractLogicalRule) UpdateGeosite() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (adapter.Rule, bool) { + rule, loaded := it.(adapter.Rule) + return rule, loaded + }) { err := rule.UpdateGeosite() if err != nil { return err @@ -156,7 +161,10 @@ func (r *abstractLogicalRule) UpdateGeosite() error { } func (r *abstractLogicalRule) Start() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (common.Starter, bool) { + rule, loaded := it.(common.Starter) + return rule, loaded + }) { err := rule.Start() if err != nil { return err @@ -166,7 +174,10 @@ func (r *abstractLogicalRule) Start() error { } func (r *abstractLogicalRule) Close() error { - for _, rule := range r.rules { + for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (io.Closer, bool) { + rule, loaded := it.(io.Closer) + return rule, loaded + }) { err := rule.Close() if err != nil { return err @@ -177,11 +188,13 @@ func (r *abstractLogicalRule) Close() error { func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { if r.mode == C.LogicalTypeAnd { - return common.All(r.rules, func(it adapter.Rule) bool { + return common.All(r.rules, func(it adapter.HeadlessRule) bool { + metadata.ResetRuleCache() return it.Match(metadata) }) != r.invert } else { - return common.Any(r.rules, func(it adapter.Rule) bool { + return common.Any(r.rules, func(it adapter.HeadlessRule) bool { + metadata.ResetRuleCache() return it.Match(metadata) }) != r.invert } diff --git a/route/rule_default.go b/route/rule_default.go index 2d62f97a46..d2227bb314 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -8,13 +8,13 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule) (adapter.Rule, error) { +func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule, checkOutbound bool) (adapter.Rule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - if options.DefaultOptions.Outbound == "" { + if options.DefaultOptions.Outbound == "" && checkOutbound { return nil, E.New("missing outbound field") } return NewDefaultRule(router, logger, options.DefaultOptions) @@ -22,7 +22,7 @@ func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rul if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - if options.LogicalOptions.Outbound == "" { + if options.LogicalOptions.Outbound == "" && checkOutbound { return nil, E.New("missing outbound field") } return NewLogicalRule(router, logger, options.LogicalOptions) @@ -115,11 +115,16 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt if len(options.SourceIPCIDR) > 0 { item, err := NewIPCIDRItem(true, options.SourceIPCIDR) if err != nil { - return nil, E.Cause(err, "source_ipcidr") + return nil, E.Cause(err, "source_ip_cidr") } rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } + if options.SourceIPIsPrivate { + item := NewIPIsPrivateItem(true) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } if len(options.IPCIDR) > 0 { item, err := NewIPCIDRItem(false, options.IPCIDR) if err != nil { @@ -128,6 +133,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } + if options.IPIsPrivate { + item := NewIPIsPrivateItem(false) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } if len(options.SourcePort) > 0 { item := NewPortItem(true, options.SourcePort) rule.sourcePortItems = append(rule.sourcePortItems, item) @@ -194,6 +204,11 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.RuleSet) > 0 { + item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } @@ -206,7 +221,7 @@ type LogicalRule struct { func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { r := &LogicalRule{ abstractLogicalRule{ - rules: make([]adapter.Rule, len(options.Rules)), + rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, outbound: options.Outbound, }, @@ -220,7 +235,7 @@ func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options opt return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDefaultRule(router, logger, subRule) + rule, err := NewRule(router, logger, subRule, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } diff --git a/route/rule_dns.go b/route/rule_dns.go index 5132f02456..c43f629083 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -8,13 +8,13 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule) (adapter.DNSRule, error) { +func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule, checkServer bool) (adapter.DNSRule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - if options.DefaultOptions.Server == "" { + if options.DefaultOptions.Server == "" && checkServer { return nil, E.New("missing server field") } return NewDefaultDNSRule(router, logger, options.DefaultOptions) @@ -22,7 +22,7 @@ func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option. if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - if options.LogicalOptions.Server == "" { + if options.LogicalOptions.Server == "" && checkServer { return nil, E.New("missing server field") } return NewLogicalDNSRule(router, logger, options.LogicalOptions) @@ -114,11 +114,16 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options if len(options.SourceIPCIDR) > 0 { item, err := NewIPCIDRItem(true, options.SourceIPCIDR) if err != nil { - return nil, E.Cause(err, "source_ipcidr") + return nil, E.Cause(err, "source_ip_cidr") } rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } + if options.SourceIPIsPrivate { + item := NewIPIsPrivateItem(true) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } if len(options.SourcePort) > 0 { item := NewPortItem(true, options.SourcePort) rule.sourcePortItems = append(rule.sourcePortItems, item) @@ -190,6 +195,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.RuleSet) > 0 { + item := NewRuleSetItem(router, options.RuleSet, false) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } @@ -212,7 +222,7 @@ type LogicalDNSRule struct { func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { r := &LogicalDNSRule{ abstractLogicalRule: abstractLogicalRule{ - rules: make([]adapter.Rule, len(options.Rules)), + rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, outbound: options.Server, }, @@ -228,7 +238,7 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDefaultDNSRule(router, logger, subRule) + rule, err := NewDNSRule(router, logger, subRule, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } diff --git a/route/rule_headless.go b/route/rule_headless.go new file mode 100644 index 0000000000..82c07d3102 --- /dev/null +++ b/route/rule_headless.go @@ -0,0 +1,173 @@ +package route + +import ( + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func NewHeadlessRule(router adapter.Router, options option.HeadlessRule) (adapter.HeadlessRule, error) { + switch options.Type { + case "", C.RuleTypeDefault: + if !options.DefaultOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewDefaultHeadlessRule(router, options.DefaultOptions) + case C.RuleTypeLogical: + if !options.LogicalOptions.IsValid() { + return nil, E.New("missing conditions") + } + return NewLogicalHeadlessRule(router, options.LogicalOptions) + default: + return nil, E.New("unknown rule type: ", options.Type) + } +} + +var _ adapter.HeadlessRule = (*DefaultHeadlessRule)(nil) + +type DefaultHeadlessRule struct { + abstractDefaultRule +} + +func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) { + rule := &DefaultHeadlessRule{ + abstractDefaultRule{ + invert: options.Invert, + }, + } + if len(options.Network) > 0 { + item := NewNetworkItem(options.Network) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 { + item := NewDomainItem(options.Domain, options.DomainSuffix) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.DomainMatcher != nil { + item := NewRawDomainItem(options.DomainMatcher) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainKeyword) > 0 { + item := NewDomainKeywordItem(options.DomainKeyword) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DomainRegex) > 0 { + item, err := NewDomainRegexItem(options.DomainRegex) + if err != nil { + return nil, E.Cause(err, "domain_regex") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourceIPCIDR) > 0 { + item, err := NewIPCIDRItem(true, options.SourceIPCIDR) + if err != nil { + return nil, E.Cause(err, "source_ip_cidr") + } + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.SourceIPSet != nil { + item := NewRawIPCIDRItem(true, options.SourceIPSet) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.IPCIDR) > 0 { + item, err := NewIPCIDRItem(false, options.IPCIDR) + if err != nil { + return nil, E.Cause(err, "ipcidr") + } + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } else if options.IPSet != nil { + item := NewRawIPCIDRItem(false, options.IPSet) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePort) > 0 { + item := NewPortItem(true, options.SourcePort) + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourcePortRange) > 0 { + item, err := NewPortRangeItem(true, options.SourcePortRange) + if err != nil { + return nil, E.Cause(err, "source_port_range") + } + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.Port) > 0 { + item := NewPortItem(false, options.Port) + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PortRange) > 0 { + item, err := NewPortRangeItem(false, options.PortRange) + if err != nil { + return nil, E.Cause(err, "port_range") + } + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessName) > 0 { + item := NewProcessItem(options.ProcessName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ProcessPath) > 0 { + item := NewProcessPathItem(options.ProcessPath) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.PackageName) > 0 { + item := NewPackageNameItem(options.PackageName) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(router, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFIBSSID) > 0 { + item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + return rule, nil +} + +var _ adapter.HeadlessRule = (*LogicalHeadlessRule)(nil) + +type LogicalHeadlessRule struct { + abstractLogicalRule +} + +func NewLogicalHeadlessRule(router adapter.Router, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) { + r := &LogicalHeadlessRule{ + abstractLogicalRule{ + rules: make([]adapter.HeadlessRule, len(options.Rules)), + invert: options.Invert, + }, + } + switch options.Mode { + case C.LogicalTypeAnd: + r.mode = C.LogicalTypeAnd + case C.LogicalTypeOr: + r.mode = C.LogicalTypeOr + default: + return nil, E.New("unknown logical mode: ", options.Mode) + } + for i, subRule := range options.Rules { + rule, err := NewHeadlessRule(router, subRule) + if err != nil { + return nil, E.Cause(err, "sub rule[", i, "]") + } + r.rules[i] = rule + } + return r, nil +} diff --git a/route/rule_item_cidr.go b/route/rule_item_cidr.go index b72d1e10b1..0e15e67441 100644 --- a/route/rule_item_cidr.go +++ b/route/rule_item_cidr.go @@ -31,13 +31,13 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { builder.Add(addr) continue } - return nil, E.Cause(err, "parse ip_cidr [", i, "]") + return nil, E.Cause(err, "parse [", i, "]") } var description string if isSource { - description = "source_ipcidr=" + description = "source_ip_cidr=" } else { - description = "ipcidr=" + description = "ip_cidr=" } if dLen := len(prefixStrings); dLen == 1 { description += prefixStrings[0] @@ -57,8 +57,23 @@ func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { }, nil } +func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem { + var description string + if isSource { + description = "source_ip_cidr=" + } else { + description = "ip_cidr=" + } + description += "" + return &IPCIDRItem{ + ipSet: ipSet, + isSource: isSource, + description: description, + } +} + func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool { - if r.isSource { + if r.isSource || metadata.QueryType != 0 || metadata.IPCIDRMatchSource { return r.ipSet.Contains(metadata.Source.Addr) } else { if metadata.Destination.IsIP() { diff --git a/route/rule_item_domain.go b/route/rule_item_domain.go index 6602441deb..d2a11181b0 100644 --- a/route/rule_item_domain.go +++ b/route/rule_item_domain.go @@ -43,6 +43,13 @@ func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem { } } +func NewRawDomainItem(matcher *domain.Matcher) *DomainItem { + return &DomainItem{ + matcher, + "domain/domain_suffix=", + } +} + func (r *DomainItem) Match(metadata *adapter.InboundContext) bool { var domainHost string if metadata.Domain != "" { diff --git a/route/rule_item_ip_is_private.go b/route/rule_item_ip_is_private.go new file mode 100644 index 0000000000..4d511fdfb1 --- /dev/null +++ b/route/rule_item_ip_is_private.go @@ -0,0 +1,44 @@ +package route + +import ( + "net/netip" + + "github.com/sagernet/sing-box/adapter" + N "github.com/sagernet/sing/common/network" +) + +var _ RuleItem = (*IPIsPrivateItem)(nil) + +type IPIsPrivateItem struct { + isSource bool +} + +func NewIPIsPrivateItem(isSource bool) *IPIsPrivateItem { + return &IPIsPrivateItem{isSource} +} + +func (r *IPIsPrivateItem) Match(metadata *adapter.InboundContext) bool { + var destination netip.Addr + if r.isSource { + destination = metadata.Source.Addr + } else { + destination = metadata.Destination.Addr + } + if destination.IsValid() && !N.IsPublicAddr(destination) { + return true + } + for _, destinationAddress := range metadata.DestinationAddresses { + if !N.IsPublicAddr(destinationAddress) { + return true + } + } + return false +} + +func (r *IPIsPrivateItem) String() string { + if r.isSource { + return "source_ip_is_private=true" + } else { + return "ip_is_private=true" + } +} diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go new file mode 100644 index 0000000000..959b2f6110 --- /dev/null +++ b/route/rule_item_rule_set.go @@ -0,0 +1,55 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*RuleSetItem)(nil) + +type RuleSetItem struct { + router adapter.Router + tagList []string + setList []adapter.HeadlessRule + ipcidrMatchSource bool +} + +func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool) *RuleSetItem { + return &RuleSetItem{ + router: router, + tagList: tagList, + ipcidrMatchSource: ipCIDRMatchSource, + } +} + +func (r *RuleSetItem) Start() error { + for _, tag := range r.tagList { + ruleSet, loaded := r.router.RuleSet(tag) + if !loaded { + return E.New("rule-set not found: ", tag) + } + r.setList = append(r.setList, ruleSet) + } + return nil +} + +func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { + metadata.IPCIDRMatchSource = r.ipcidrMatchSource + for _, ruleSet := range r.setList { + if ruleSet.Match(metadata) { + return true + } + } + return false +} + +func (r *RuleSetItem) String() string { + if len(r.tagList) == 1 { + return F.ToString("rule_set=", r.tagList[0]) + } else { + return F.ToString("rule_set=[", strings.Join(r.tagList, " "), "]") + } +} diff --git a/route/rule_set.go b/route/rule_set.go new file mode 100644 index 0000000000..f644fb406f --- /dev/null +++ b/route/rule_set.go @@ -0,0 +1,67 @@ +package route + +import ( + "context" + "net" + "net/http" + "sync" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { + switch options.Type { + case C.RuleSetTypeLocal: + return NewLocalRuleSet(router, options) + case C.RuleSetTypeRemote: + return NewRemoteRuleSet(ctx, router, logger, options), nil + default: + return nil, E.New("unknown rule set type: ", options.Type) + } +} + +var _ adapter.RuleSetStartContext = (*RuleSetStartContext)(nil) + +type RuleSetStartContext struct { + access sync.Mutex + httpClientCache map[string]*http.Client +} + +func NewRuleSetStartContext() *RuleSetStartContext { + return &RuleSetStartContext{ + httpClientCache: make(map[string]*http.Client), + } +} + +func (c *RuleSetStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client { + c.access.Lock() + defer c.access.Unlock() + if httpClient, loaded := c.httpClientCache[detour]; loaded { + return httpClient + } + httpClient := &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + }, + } + c.httpClientCache[detour] = httpClient + return httpClient +} + +func (c *RuleSetStartContext) Close() { + c.access.Lock() + defer c.access.Unlock() + for _, client := range c.httpClientCache { + client.CloseIdleConnections() + } +} diff --git a/route/rule_set_local.go b/route/rule_set_local.go new file mode 100644 index 0000000000..635f22ed01 --- /dev/null +++ b/route/rule_set_local.go @@ -0,0 +1,84 @@ +package route + +import ( + "context" + "os" + + "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" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" +) + +var _ adapter.RuleSet = (*LocalRuleSet)(nil) + +type LocalRuleSet struct { + rules []adapter.HeadlessRule + metadata adapter.RuleSetMetadata +} + +func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { + var plainRuleSet option.PlainRuleSet + switch options.Format { + case C.RuleSetFormatSource, "": + content, err := os.ReadFile(options.LocalOptions.Path) + if err != nil { + return nil, err + } + compat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return nil, err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + setFile, err := os.Open(options.LocalOptions.Path) + if err != nil { + return nil, err + } + plainRuleSet, err = srs.Read(setFile, false) + if err != nil { + return nil, err + } + default: + return nil, E.New("unknown rule set format: ", options.Format) + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + var err error + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(router, ruleOptions) + if err != nil { + return nil, E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + var metadata adapter.RuleSetMetadata + metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) + metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) + return &LocalRuleSet{rules, 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) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error { + return nil +} + +func (s *LocalRuleSet) PostStart() error { + return nil +} + +func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata { + return s.metadata +} + +func (s *LocalRuleSet) Close() error { + return nil +} diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go new file mode 100644 index 0000000000..595e328c5c --- /dev/null +++ b/route/rule_set_remote.go @@ -0,0 +1,262 @@ +package route + +import ( + "bytes" + "context" + "io" + "net" + "net/http" + "runtime" + "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" + E "github.com/sagernet/sing/common/exceptions" + "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/service" + "github.com/sagernet/sing/service/pause" +) + +var _ adapter.RuleSet = (*RemoteRuleSet)(nil) + +type RemoteRuleSet struct { + ctx context.Context + cancel context.CancelFunc + router adapter.Router + logger logger.ContextLogger + options option.RuleSet + metadata adapter.RuleSetMetadata + updateInterval time.Duration + dialer N.Dialer + rules []adapter.HeadlessRule + lastUpdated time.Time + lastEtag string + updateTicker *time.Ticker + pauseManager pause.Manager +} + +func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { + ctx, cancel := context.WithCancel(ctx) + var updateInterval time.Duration + if options.RemoteOptions.UpdateInterval > 0 { + updateInterval = time.Duration(options.RemoteOptions.UpdateInterval) + } else { + updateInterval = 24 * time.Hour + } + return &RemoteRuleSet{ + ctx: ctx, + cancel: cancel, + router: router, + logger: logger, + options: options, + updateInterval: updateInterval, + pauseManager: service.FromContext[pause.Manager](ctx), + } +} + +func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} + +func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error { + var dialer N.Dialer + if s.options.RemoteOptions.DownloadDetour != "" { + outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour) + if !loaded { + return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour) + } + dialer = outbound + } else { + outbound, err := s.router.DefaultOutbound(N.NetworkTCP) + if err != nil { + return err + } + dialer = outbound + } + s.dialer = dialer + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + if savedSet := cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil { + err := s.loadBytes(savedSet.Content) + if err != nil { + return E.Cause(err, "restore cached rule-set") + } + s.lastUpdated = savedSet.LastUpdated + s.lastEtag = savedSet.LastEtag + } + } + if s.lastUpdated.IsZero() { + err := s.fetchOnce(ctx, startContext) + if err != nil { + return E.Cause(err, "initial rule-set: ", s.options.Tag) + } + } + s.updateTicker = time.NewTicker(s.updateInterval) + go s.loopUpdate() + return nil +} + +func (s *RemoteRuleSet) PostStart() error { + if s.lastUpdated.IsZero() { + err := s.fetchOnce(s.ctx, nil) + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } + } + return nil +} + +func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata { + return s.metadata +} + +func (s *RemoteRuleSet) loadBytes(content []byte) error { + var ( + plainRuleSet option.PlainRuleSet + err error + ) + switch s.options.Format { + case C.RuleSetFormatSource: + var compat option.PlainRuleSetCompat + compat, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + plainRuleSet, err = srs.Read(bytes.NewReader(content), false) + if err != nil { + return err + } + default: + return E.New("unknown rule set format: ", s.options.Format) + } + rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) + for i, ruleOptions := range plainRuleSet.Rules { + rules[i], err = NewHeadlessRule(s.router, ruleOptions) + if err != nil { + return E.Cause(err, "parse rule_set.rules.[", i, "]") + } + } + s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) + s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) + s.rules = rules + return nil +} + +func (s *RemoteRuleSet) loopUpdate() { + if time.Since(s.lastUpdated) > s.updateInterval { + err := s.fetchOnce(s.ctx, nil) + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } + } + for { + runtime.GC() + select { + case <-s.ctx.Done(): + return + case <-s.updateTicker.C: + s.pauseManager.WaitActive() + err := s.fetchOnce(s.ctx, nil) + if err != nil { + s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } + } + } +} + +func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext adapter.RuleSetStartContext) error { + s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL) + var httpClient *http.Client + if startContext != nil { + httpClient = startContext.HTTPClient(s.options.RemoteOptions.DownloadDetour, s.dialer) + } else { + httpClient = &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + }, + } + } + request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil) + if err != nil { + return err + } + if s.lastEtag != "" { + request.Header.Set("If-None-Match", s.lastEtag) + } + response, err := httpClient.Do(request.WithContext(ctx)) + if err != nil { + return err + } + switch response.StatusCode { + case http.StatusOK: + case http.StatusNotModified: + s.lastUpdated = time.Now() + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + savedRuleSet := cacheFile.LoadRuleSet(s.options.Tag) + if savedRuleSet != nil { + savedRuleSet.LastUpdated = s.lastUpdated + err = cacheFile.SaveRuleSet(s.options.Tag, savedRuleSet) + if err != nil { + s.logger.Error("save rule-set updated time: ", err) + return nil + } + } + } + s.logger.Info("update rule-set ", s.options.Tag, ": not modified") + return nil + default: + return E.New("unexpected status: ", response.Status) + } + content, err := io.ReadAll(response.Body) + if err != nil { + response.Body.Close() + return err + } + err = s.loadBytes(content) + if err != nil { + response.Body.Close() + return err + } + response.Body.Close() + eTagHeader := response.Header.Get("Etag") + if eTagHeader != "" { + s.lastEtag = eTagHeader + } + s.lastUpdated = time.Now() + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + err = cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedRuleSet{ + LastUpdated: s.lastUpdated, + Content: content, + LastEtag: s.lastEtag, + }) + if err != nil { + s.logger.Error("save rule-set cache: ", err) + } + } + s.logger.Info("updated rule-set ", s.options.Tag) + return nil +} + +func (s *RemoteRuleSet) Close() error { + s.updateTicker.Stop() + s.cancel() + return nil +} diff --git a/test/brutal_test.go b/test/brutal_test.go index 6bfdfcc1b1..bfe4d1fc02 100644 --- a/test/brutal_test.go +++ b/test/brutal_test.go @@ -118,11 +118,13 @@ func TestBrutalTrojan(t *testing.T) { DownMbps: 100, }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -150,10 +152,12 @@ func TestBrutalTrojan(t *testing.T) { DownMbps: 100, }, }, - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, }, }, @@ -275,19 +279,21 @@ func TestBrutalVLESS(t *testing.T) { DownMbps: 100, }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "google.com", - Reality: &option.InboundRealityOptions{ - Enabled: true, - Handshake: option.InboundRealityHandshakeOptions{ - ServerOptions: option.ServerOptions{ - Server: "google.com", - ServerPort: 443, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.InboundRealityOptions{ + Enabled: true, + Handshake: option.InboundRealityHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "google.com", + ServerPort: 443, + }, }, + ShortID: []string{"0123456789abcdef"}, + PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", }, - ShortID: []string{"0123456789abcdef"}, - PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", }, }, }, @@ -306,16 +312,18 @@ func TestBrutalVLESS(t *testing.T) { ServerPort: serverPort, }, UUID: user.String(), - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "google.com", - Reality: &option.OutboundRealityOptions{ - Enabled: true, - ShortID: "0123456789abcdef", - PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", - }, - UTLS: &option.OutboundUTLSOptions{ - Enabled: true, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.OutboundRealityOptions{ + Enabled: true, + ShortID: "0123456789abcdef", + PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", + }, + UTLS: &option.OutboundUTLSOptions{ + Enabled: true, + }, }, }, Multiplex: &option.OutboundMultiplexOptions{ diff --git a/test/domain_inbound_test.go b/test/domain_inbound_test.go index bf43aa98f2..f22fe24982 100644 --- a/test/domain_inbound_test.go +++ b/test/domain_inbound_test.go @@ -38,11 +38,13 @@ func TestTUICDomainUDP(t *testing.T) { Users: []option.TUICUser{{ UUID: uuid.Nil.String(), }}, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -60,10 +62,12 @@ func TestTUICDomainUDP(t *testing.T) { ServerPort: serverPort, }, UUID: uuid.Nil.String(), - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, }, }, diff --git a/test/ech_test.go b/test/ech_test.go index 85e4a43395..35d5d89158 100644 --- a/test/ech_test.go +++ b/test/ech_test.go @@ -40,14 +40,16 @@ func TestECH(t *testing.T) { Password: "password", }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, - ECH: &option.InboundECHOptions{ - Enabled: true, - Key: []string{echKey}, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + ECH: &option.InboundECHOptions{ + Enabled: true, + Key: []string{echKey}, + }, }, }, }, @@ -66,13 +68,15 @@ func TestECH(t *testing.T) { ServerPort: serverPort, }, Password: "password", - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - ECH: &option.OutboundECHOptions{ - Enabled: true, - Config: []string{echConfig}, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + ECH: &option.OutboundECHOptions{ + Enabled: true, + Config: []string{echConfig}, + }, }, }, }, @@ -117,14 +121,16 @@ func TestECHQUIC(t *testing.T) { Users: []option.TUICUser{{ UUID: uuid.Nil.String(), }}, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, - ECH: &option.InboundECHOptions{ - Enabled: true, - Key: []string{echKey}, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + ECH: &option.InboundECHOptions{ + Enabled: true, + Key: []string{echKey}, + }, }, }, }, @@ -143,13 +149,15 @@ func TestECHQUIC(t *testing.T) { ServerPort: serverPort, }, UUID: uuid.Nil.String(), - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - ECH: &option.OutboundECHOptions{ - Enabled: true, - Config: []string{echConfig}, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + ECH: &option.OutboundECHOptions{ + Enabled: true, + Config: []string{echConfig}, + }, }, }, }, @@ -194,14 +202,16 @@ func TestECHHysteria2(t *testing.T) { Users: []option.Hysteria2User{{ Password: "password", }}, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, - ECH: &option.InboundECHOptions{ - Enabled: true, - Key: []string{echKey}, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + ECH: &option.InboundECHOptions{ + Enabled: true, + Key: []string{echKey}, + }, }, }, }, @@ -220,13 +230,15 @@ func TestECHHysteria2(t *testing.T) { ServerPort: serverPort, }, Password: "password", - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - ECH: &option.OutboundECHOptions{ - Enabled: true, - Config: []string{echConfig}, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + ECH: &option.OutboundECHOptions{ + Enabled: true, + Config: []string{echConfig}, + }, }, }, }, diff --git a/test/go.mod b/test/go.mod index 42c9d087b4..339bd6f2df 100644 --- a/test/go.mod +++ b/test/go.mod @@ -10,24 +10,24 @@ require ( github.com/docker/docker v24.0.7+incompatible github.com/docker/go-connections v0.4.0 github.com/gofrs/uuid/v5 v5.0.0 - github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460 - github.com/sagernet/sing v0.2.18-0.20231119032432-6a556bfa50cc + github.com/sagernet/quic-go v0.40.0 + github.com/sagernet/sing v0.2.20-0.20231212123824-8836b6754226 github.com/sagernet/sing-dns v0.1.11 - github.com/sagernet/sing-quic v0.1.4 - github.com/sagernet/sing-shadowsocks v0.2.5 - github.com/sagernet/sing-shadowsocks2 v0.1.5 + github.com/sagernet/sing-quic v0.1.6-0.20231207143711-eb3cbf9ed054 + github.com/sagernet/sing-shadowsocks v0.2.6 + github.com/sagernet/sing-shadowsocks2 v0.1.6-0.20231207143709-50439739601a github.com/spyzhov/ajson v0.9.0 github.com/stretchr/testify v1.8.4 go.uber.org/goleak v1.3.0 - golang.org/x/net v0.18.0 + golang.org/x/net v0.19.0 ) require ( berty.tech/go-libtor v1.0.385 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ajg/form v1.5.1 // indirect - github.com/andybalholm/brotli v1.0.5 // indirect - github.com/caddyserver/certmagic v0.19.2 // indirect + github.com/andybalholm/brotli v1.0.6 // indirect + github.com/caddyserver/certmagic v0.20.0 // indirect github.com/cloudflare/circl v1.3.6 // indirect github.com/cretz/bine v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -35,6 +35,7 @@ require ( github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gaukas/godicttls v0.0.4 // indirect github.com/go-chi/chi/v5 v5.0.10 // indirect github.com/go-chi/cors v1.2.1 // indirect github.com/go-chi/render v1.0.3 // indirect @@ -45,11 +46,11 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.1.2 // indirect - github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect github.com/hashicorp/yamux v0.1.1 // indirect - github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c // indirect + github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/klauspost/compress v1.15.15 // indirect + github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/libdns/alidns v1.0.3 // indirect github.com/libdns/cloudflare v0.1.0 // indirect @@ -59,7 +60,7 @@ require ( github.com/miekg/dns v1.1.57 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect - github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/onsi/ginkgo/v2 v2.9.7 // indirect github.com/ooni/go-libtor v1.1.8 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect @@ -68,36 +69,35 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/qtls-go1-20 v0.3.4 // indirect + github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect - github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a // indirect - github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect - github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 // indirect + github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 // indirect + github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e // indirect github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect - github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 // indirect + github.com/sagernet/sing-mux v0.1.6-0.20231208180947-9053c29513a2 // indirect github.com/sagernet/sing-shadowtls v0.1.4 // indirect - github.com/sagernet/sing-tun v0.1.21-0.20231119035513-f6ea97c5af71 // indirect + github.com/sagernet/sing-tun v0.1.24-0.20231212060935-6a1419aeae11 // indirect github.com/sagernet/sing-vmess v0.1.8 // indirect - github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 // indirect - github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 // indirect - github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 // indirect - github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f // indirect - github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed // indirect + github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect + github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 // indirect + github.com/sagernet/utls v1.5.4 // indirect + github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e // indirect + github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - go4.org/netipx v0.0.0-20230824141953-6213f710f925 // indirect - golang.org/x/crypto v0.15.0 // indirect - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.4.0 // indirect - golang.org/x/tools v0.15.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.16.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.31.0 // indirect diff --git a/test/go.sum b/test/go.sum index c3d155cb45..ccd3672d5f 100644 --- a/test/go.sum +++ b/test/go.sum @@ -5,13 +5,10 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= -github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/caddyserver/certmagic v0.19.2 h1:HZd1AKLx4592MalEGQS39DKs2ZOAJCEM/xYPMQ2/ui0= -github.com/caddyserver/certmagic v0.19.2/go.mod h1:fsL01NomQ6N+kE2j37ZCnig2MFosG+MIO4ztnmG/zz8= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= +github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= +github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg= github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg= github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= @@ -32,6 +29,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 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.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= @@ -58,20 +57,19 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk= +github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c h1:PgxFEySCI41sH0mB7/2XswdXbUykQsRUGod8Rn+NubM= -github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= +github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= +github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= -github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -94,9 +92,9 @@ github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= -github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= +github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= +github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= github.com/ooni/go-libtor v1.1.8 h1:Wo3V3DVTxl5vZdxtQakqYP+DAHx7pPtAFSl1bnAa08w= github.com/ooni/go-libtor v1.1.8/go.mod h1:q1YyLwRD9GeMyeerVvwc0vJ2YgwDLTp2bdVcrh/JXyI= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -113,52 +111,49 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg= -github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= +github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= +github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= -github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a h1:wZHruBxZCsQLXHAozWpnJBL3wJ/XufDpz0qKtgpSnA4= -github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a/go.mod h1:dNV1ZP9y3qx5ltULeKaQZTZWTLHflgW5DES+Ses7cMI= -github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA= -github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms= -github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 h1:dSPgjIw0CT6ISLeEh8Q20dZMBMFCcEceo23+LncRcNQ= -github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930/go.mod h1:JpKHkOYgh4wLwrX2BhH3ZIvCvazCkTnPeEcmigZJfHY= +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/gvisor v0.0.0-20231209105102-8d27a30e436e h1:DOkjByVeAR56dkszjnMZke4wr7yM/1xHaJF3G9olkEE= +github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e/go.mod h1:fLxq/gtp0qzkaEwywlRRiGmjOK5ES/xUzyIKIFP2Asw= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= -github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460 h1:dAe4OIJAtE0nHOzTHhAReQteh3+sa63rvXbuIpbeOTY= -github.com/sagernet/quic-go v0.0.0-20231008035953-32727fef9460/go.mod h1:uJGpmJCOcMQqMlHKc3P1Vz6uygmpz4bPeVIoOhdVQnM= +github.com/sagernet/quic-go v0.40.0 h1:DvQNPb72lzvNQDe9tcUyHTw8eRv6PLtM2mNYmdlzUMo= +github.com/sagernet/quic-go v0.40.0/go.mod h1:VqtdhlbkeeG5Okhb3eDMb/9o0EoglReHunNT9ukrJAI= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= -github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= -github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= -github.com/sagernet/sing v0.2.18-0.20231119032432-6a556bfa50cc h1:dmU0chO0QrBpARo8sqyOc+mvPLW+qux4ca16kb2WIc8= -github.com/sagernet/sing v0.2.18-0.20231119032432-6a556bfa50cc/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= +github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= +github.com/sagernet/sing v0.2.20-0.20231212123824-8836b6754226 h1:rcII71ho6F/7Nyx7n2kESLcnvNMdcU4i8ZUGF2Fi7yA= +github.com/sagernet/sing v0.2.20-0.20231212123824-8836b6754226/go.mod h1:Ce5LNojQOgOiWhiD8pPD6E9H7e2KgtOe3Zxx4Ou5u80= github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE= github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE= -github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM= -github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07/go.mod h1:u/MZf32xPG8jEKe3t+xUV67EBnKtDtCaPhsJQOQGUYU= -github.com/sagernet/sing-quic v0.1.4 h1:F5KRGXMXKQEmP8VrzVollf9HWcRqggcuG9nRCL+5IJ8= -github.com/sagernet/sing-quic v0.1.4/go.mod h1:aXHVP+osF3w5wJzoWZbJSrX3ceJiU9QMd0KPnKV6C/o= -github.com/sagernet/sing-shadowsocks v0.2.5 h1:qxIttos4xu6ii7MTVJYA8EFQR7Q3KG6xMqmLJIFtBaY= -github.com/sagernet/sing-shadowsocks v0.2.5/go.mod h1:MGWGkcU2xW2G2mfArT9/QqpVLOGU+dBaahZCtPHdt7A= -github.com/sagernet/sing-shadowsocks2 v0.1.5 h1:JDeAJ4ZWlYZ7F6qEVdDKPhQEangxKw/JtmU+i/YfCYE= -github.com/sagernet/sing-shadowsocks2 v0.1.5/go.mod h1:KF65y8lI5PGHyMgRZGYXYsH9ilgRc/yr+NYbSNGuBm4= +github.com/sagernet/sing-mux v0.1.6-0.20231208180947-9053c29513a2 h1:rRlYQPbMKmzKX+43XC04gEQvxc45/AxfteRWfcl2/rw= +github.com/sagernet/sing-mux v0.1.6-0.20231208180947-9053c29513a2/go.mod h1:IdSrwwqBeJTrjLZJRFXE+F8mYXNI/rPAjzlgTFuEVmo= +github.com/sagernet/sing-quic v0.1.6-0.20231207143711-eb3cbf9ed054 h1:Ed7FskwQcep5oQ+QahgVK0F6jPPSV8Nqwjr9MwGatMU= +github.com/sagernet/sing-quic v0.1.6-0.20231207143711-eb3cbf9ed054/go.mod h1:u758WWv3G1OITG365CYblL0NfAruFL1PpLD9DUVTv1o= +github.com/sagernet/sing-shadowsocks v0.2.6 h1:xr7ylAS/q1cQYS8oxKKajhuQcchd5VJJ4K4UZrrpp0s= +github.com/sagernet/sing-shadowsocks v0.2.6/go.mod h1:j2YZBIpWIuElPFL/5sJAj470bcn/3QQ5lxZUNKLDNAM= +github.com/sagernet/sing-shadowsocks2 v0.1.6-0.20231207143709-50439739601a h1:uYIKfpE1/EJpa+1Bja7b006VixeRuVduOpeuesMk2lU= +github.com/sagernet/sing-shadowsocks2 v0.1.6-0.20231207143709-50439739601a/go.mod h1:pjeylQ4ApvpEH7B4PUBrdyJf4xmQkg8BaIzT5fI2fR0= github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= -github.com/sagernet/sing-tun v0.1.21-0.20231119035513-f6ea97c5af71 h1:WQi0TwhjbSNFFbxybIgAUSjVvo7uWSsLD28ldoM2avY= -github.com/sagernet/sing-tun v0.1.21-0.20231119035513-f6ea97c5af71/go.mod h1:hyzA4gDWbeg2SXklqPDswBKa//QcjlZqKw9aPcNdQ9A= +github.com/sagernet/sing-tun v0.1.24-0.20231212060935-6a1419aeae11 h1:crTOVPJGOGWOW+Q2a0FQiiS/G2+W6uCLKtOofFMisQc= +github.com/sagernet/sing-tun v0.1.24-0.20231212060935-6a1419aeae11/go.mod h1:DgXPnBqtqWrZj37Mun/W61dW0Q56eLqTZYhcuNLaCtY= github.com/sagernet/sing-vmess v0.1.8 h1:XVWad1RpTy9b5tPxdm5MCU8cGfrTGdR8qCq6HV2aCNc= github.com/sagernet/sing-vmess v0.1.8/go.mod h1:vhx32UNzTDUkNwOyIjcZQohre1CaytquC5mPplId8uA= -github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 h1:HuE6xSwco/Xed8ajZ+coeYLmioq0Qp1/Z2zczFaV8as= -github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37/go.mod h1:3skNSftZDJWTGVtVaM2jfbce8qHnmH/AGDRe62iNOg0= -github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 h1:Px+hN4Vzgx+iCGVnWH5A8eR7JhNnIV3rGQmBxA7cw6Q= -github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6/go.mod h1:zovq6vTvEM6ECiqE3Eeb9rpIylPpamPcmrJ9tv0Bt0M= -github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 h1:kDUqhc9Vsk5HJuhfIATJ8oQwBmpOZJuozQG7Vk88lL4= -github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM= -github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f h1:Kvo8w8Y9lzFGB/7z09MJ3TR99TFtfI/IuY87Ygcycho= -github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f/go.mod h1:mySs0abhpc/gLlvhoq7HP1RzOaRmIXVeZGCh++zoApk= -github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed h1:90a510OeE9siSJoYsI8nSjPmA+u5ROMDts/ZkdNsuXY= -github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= +github.com/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/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 h1:z3SJQhVyU63FT26Wn/UByW6b7q8QKB0ZkPqsyqcz2PI= +github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6/go.mod h1:73xRZuxwkFk4aiLw28hG8W6o9cr2UPrGL9pdY2UTbvY= +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/wireguard-go v0.0.0-20231209092712-9a439356a62e h1:iGH0RMv2FzELOFNFQtvsxH7NPmlo7X5JizEK51UCojo= +github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e/go.mod h1:YbL4TKHRR6APYQv3U2RGfwLDpPYSyWz6oUlpISBEzBE= +github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= +github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s= github.com/spyzhov/ajson v0.9.0 h1:tF46gJGOenYVj+k9K1U1XpCxVWhmiyY5PsVCAs1+OJ0= @@ -187,17 +182,17 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -go4.org/netipx v0.0.0-20230824141953-6213f710f925 h1:eeQDDVKFkx0g4Hyy8pHgmZaK0EqB4SD6rvKbUdN3ziQ= -go4.org/netipx v0.0.0-20230824141953-6213f710f925/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= @@ -208,8 +203,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 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.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -217,33 +212,32 @@ golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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= -golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.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.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= -golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/test/hysteria2_test.go b/test/hysteria2_test.go index 04735eeb77..f549442811 100644 --- a/test/hysteria2_test.go +++ b/test/hysteria2_test.go @@ -52,11 +52,13 @@ func testHysteria2Self(t *testing.T, salamanderPassword string) { Users: []option.Hysteria2User{{ Password: "password", }}, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -77,10 +79,12 @@ func testHysteria2Self(t *testing.T, salamanderPassword string) { DownMbps: 100, Obfs: obfs, Password: "password", - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, }, }, @@ -118,11 +122,13 @@ func TestHysteria2Inbound(t *testing.T) { Users: []option.Hysteria2User{{ Password: "password", }}, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -177,10 +183,12 @@ func TestHysteria2Outbound(t *testing.T) { Password: "cry_me_a_r1ver", }, Password: "password", - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, }, }, diff --git a/test/hysteria_test.go b/test/hysteria_test.go index 664e5a7c04..90ff62dd10 100644 --- a/test/hysteria_test.go +++ b/test/hysteria_test.go @@ -35,11 +35,13 @@ func TestHysteriaSelf(t *testing.T) { AuthString: "password", }}, Obfs: "fuck me till the daylight", - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -60,10 +62,12 @@ func TestHysteriaSelf(t *testing.T) { DownMbps: 100, AuthString: "password", Obfs: "fuck me till the daylight", - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, }, }, @@ -99,11 +103,13 @@ func TestHysteriaInbound(t *testing.T) { AuthString: "password", }}, Obfs: "fuck me till the daylight", - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -157,10 +163,12 @@ func TestHysteriaOutbound(t *testing.T) { DownMbps: 100, AuthString: "password", Obfs: "fuck me till the daylight", - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, }, }, diff --git a/test/naive_test.go b/test/naive_test.go index 86f8b329ce..1a1547da17 100644 --- a/test/naive_test.go +++ b/test/naive_test.go @@ -74,11 +74,13 @@ func TestNaiveInbound(t *testing.T) { }, }, Network: network.NetworkTCP, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -116,11 +118,13 @@ func TestNaiveHTTP3Inbound(t *testing.T) { }, }, Network: network.NetworkUDP, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, diff --git a/test/reality_test.go b/test/reality_test.go index b330fe1616..2355aee31d 100644 --- a/test/reality_test.go +++ b/test/reality_test.go @@ -39,19 +39,21 @@ func TestVLESSVisionReality(t *testing.T) { Flow: vless.FlowVision, }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "google.com", - Reality: &option.InboundRealityOptions{ - Enabled: true, - Handshake: option.InboundRealityHandshakeOptions{ - ServerOptions: option.ServerOptions{ - Server: "google.com", - ServerPort: 443, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.InboundRealityOptions{ + Enabled: true, + Handshake: option.InboundRealityHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "google.com", + ServerPort: 443, + }, }, + ShortID: []string{"0123456789abcdef"}, + PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", }, - ShortID: []string{"0123456789abcdef"}, - PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", }, }, }, @@ -70,11 +72,13 @@ func TestVLESSVisionReality(t *testing.T) { Password: userUUID.String(), }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -92,10 +96,12 @@ func TestVLESSVisionReality(t *testing.T) { ServerPort: otherPort, }, Password: userUUID.String(), - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, DialerOptions: option.DialerOptions{ Detour: "vless-out", @@ -112,16 +118,18 @@ func TestVLESSVisionReality(t *testing.T) { }, UUID: userUUID.String(), Flow: vless.FlowVision, - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "google.com", - Reality: &option.OutboundRealityOptions{ - Enabled: true, - ShortID: "0123456789abcdef", - PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", - }, - UTLS: &option.OutboundUTLSOptions{ - Enabled: true, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.OutboundRealityOptions{ + Enabled: true, + ShortID: "0123456789abcdef", + PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", + }, + UTLS: &option.OutboundUTLSOptions{ + Enabled: true, + }, }, }, }, @@ -169,19 +177,21 @@ func TestVLESSVisionRealityPlain(t *testing.T) { Flow: vless.FlowVision, }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "google.com", - Reality: &option.InboundRealityOptions{ - Enabled: true, - Handshake: option.InboundRealityHandshakeOptions{ - ServerOptions: option.ServerOptions{ - Server: "google.com", - ServerPort: 443, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.InboundRealityOptions{ + Enabled: true, + Handshake: option.InboundRealityHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "google.com", + ServerPort: 443, + }, }, + ShortID: []string{"0123456789abcdef"}, + PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", }, - ShortID: []string{"0123456789abcdef"}, - PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", }, }, }, @@ -201,16 +211,18 @@ func TestVLESSVisionRealityPlain(t *testing.T) { }, UUID: userUUID.String(), Flow: vless.FlowVision, - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "google.com", - Reality: &option.OutboundRealityOptions{ - Enabled: true, - ShortID: "0123456789abcdef", - PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", - }, - UTLS: &option.OutboundUTLSOptions{ - Enabled: true, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.OutboundRealityOptions{ + Enabled: true, + ShortID: "0123456789abcdef", + PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", + }, + UTLS: &option.OutboundUTLSOptions{ + Enabled: true, + }, }, }, }, @@ -275,19 +287,21 @@ func testVLESSRealityTransport(t *testing.T, transport *option.V2RayTransportOpt UUID: userUUID.String(), }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "google.com", - Reality: &option.InboundRealityOptions{ - Enabled: true, - Handshake: option.InboundRealityHandshakeOptions{ - ServerOptions: option.ServerOptions{ - Server: "google.com", - ServerPort: 443, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.InboundRealityOptions{ + Enabled: true, + Handshake: option.InboundRealityHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "google.com", + ServerPort: 443, + }, }, + ShortID: []string{"0123456789abcdef"}, + PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", }, - ShortID: []string{"0123456789abcdef"}, - PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", }, }, Transport: transport, @@ -307,16 +321,18 @@ func testVLESSRealityTransport(t *testing.T, transport *option.V2RayTransportOpt ServerPort: serverPort, }, UUID: userUUID.String(), - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "google.com", - Reality: &option.OutboundRealityOptions{ - Enabled: true, - ShortID: "0123456789abcdef", - PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", - }, - UTLS: &option.OutboundUTLSOptions{ - Enabled: true, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + Reality: &option.OutboundRealityOptions{ + Enabled: true, + ShortID: "0123456789abcdef", + PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", + }, + UTLS: &option.OutboundUTLSOptions{ + Enabled: true, + }, }, }, Transport: transport, diff --git a/test/shadowtls_test.go b/test/shadowtls_test.go index 01f48dbeed..2f53d46ada 100644 --- a/test/shadowtls_test.go +++ b/test/shadowtls_test.go @@ -99,11 +99,13 @@ func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool) Server: "127.0.0.1", ServerPort: serverPort, }, - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "google.com", - UTLS: &option.OutboundUTLSOptions{ - Enabled: utlsEanbled, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + UTLS: &option.OutboundUTLSOptions{ + Enabled: utlsEanbled, + }, }, }, Version: version, @@ -301,9 +303,11 @@ func TestShadowTLSOutbound(t *testing.T) { Server: "127.0.0.1", ServerPort: serverPort, }, - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "google.com", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + }, }, Version: 3, Password: "hello", diff --git a/test/tls_test.go b/test/tls_test.go index 954fd7e7c4..da55faf5bf 100644 --- a/test/tls_test.go +++ b/test/tls_test.go @@ -35,11 +35,13 @@ func TestUTLS(t *testing.T) { Password: "password", }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -57,14 +59,15 @@ func TestUTLS(t *testing.T) { ServerPort: serverPort, }, Password: "password", - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - MixedCaseSNI: true, - CertificatePath: certPem, - UTLS: &option.OutboundUTLSOptions{ - Enabled: true, - Fingerprint: "chrome", + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + UTLS: &option.OutboundUTLSOptions{ + Enabled: true, + Fingerprint: "chrome", + }, }, }, }, diff --git a/test/trojan_test.go b/test/trojan_test.go index 4dab0db73e..d8659b2e4f 100644 --- a/test/trojan_test.go +++ b/test/trojan_test.go @@ -40,10 +40,12 @@ func TestTrojanOutbound(t *testing.T) { ServerPort: serverPort, }, Password: "password", - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, }, }, @@ -79,11 +81,13 @@ func TestTrojanSelf(t *testing.T) { Password: "password", }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -101,10 +105,12 @@ func TestTrojanSelf(t *testing.T) { ServerPort: serverPort, }, Password: "password", - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, }, }, diff --git a/test/tuic_test.go b/test/tuic_test.go index cee39d106e..c2b71111f0 100644 --- a/test/tuic_test.go +++ b/test/tuic_test.go @@ -51,11 +51,13 @@ func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) { UUID: uuid.Nil.String(), }}, ZeroRTTHandshake: zeroRTTHandshake, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -75,10 +77,12 @@ func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) { UUID: uuid.Nil.String(), UDPRelayMode: udpRelayMode, ZeroRTTHandshake: zeroRTTHandshake, - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, }, }, @@ -112,11 +116,13 @@ func TestTUICInbound(t *testing.T) { UUID: "FE35D05B-8803-45C4-BAE6-723AD2CD5D3D", Password: "tuic", }}, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -166,10 +172,12 @@ func TestTUICOutbound(t *testing.T) { }, UUID: "FE35D05B-8803-45C4-BAE6-723AD2CD5D3D", Password: "tuic", - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, }, }, diff --git a/test/v2ray_grpc_test.go b/test/v2ray_grpc_test.go index 1be797fd47..fa43f753a1 100644 --- a/test/v2ray_grpc_test.go +++ b/test/v2ray_grpc_test.go @@ -41,11 +41,13 @@ func testV2RayGRPCInbound(t *testing.T, forceLite bool) { UUID: userId.String(), }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, Transport: &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeGRPC, @@ -147,10 +149,12 @@ func testV2RayGRPCOutbound(t *testing.T, forceLite bool) { }, UUID: userId.String(), Security: "zero", - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, Transport: &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeGRPC, diff --git a/test/v2ray_transport_test.go b/test/v2ray_transport_test.go index 124e41846f..2f39d18aed 100644 --- a/test/v2ray_transport_test.go +++ b/test/v2ray_transport_test.go @@ -68,11 +68,13 @@ func testVMessTransportSelf(t *testing.T, server *option.V2RayTransportOptions, UUID: user.String(), }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, Transport: server, }, @@ -92,10 +94,12 @@ func testVMessTransportSelf(t *testing.T, server *option.V2RayTransportOptions, }, UUID: user.String(), Security: "zero", - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, Transport: client, }, @@ -144,11 +148,13 @@ func testTrojanTransportSelf(t *testing.T, server *option.V2RayTransportOptions, Password: user.String(), }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, Transport: server, }, @@ -167,10 +173,12 @@ func testTrojanTransportSelf(t *testing.T, server *option.V2RayTransportOptions, ServerPort: serverPort, }, Password: user.String(), - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, Transport: client, }, @@ -222,11 +230,13 @@ func TestVMessQUICSelf(t *testing.T) { UUID: user.String(), }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, Transport: transport, }, @@ -246,10 +256,12 @@ func TestVMessQUICSelf(t *testing.T) { }, UUID: user.String(), Security: "zero", - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, Transport: transport, }, diff --git a/test/v2ray_ws_test.go b/test/v2ray_ws_test.go index 8bc0b723ae..0e238c28b6 100644 --- a/test/v2ray_ws_test.go +++ b/test/v2ray_ws_test.go @@ -75,11 +75,13 @@ func testV2RayWebsocketInbound(t *testing.T, maxEarlyData uint32, earlyDataHeade UUID: userId.String(), }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, Transport: &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeWebsocket, @@ -179,10 +181,12 @@ func testV2RayWebsocketOutbound(t *testing.T, maxEarlyData uint32, earlyDataHead }, UUID: userId.String(), Security: "zero", - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, Transport: &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeWebsocket, diff --git a/test/vless_test.go b/test/vless_test.go index 7757bc111b..50031dd4d3 100644 --- a/test/vless_test.go +++ b/test/vless_test.go @@ -130,11 +130,13 @@ func testVLESSXrayOutbound(t *testing.T, packetEncoding string, flow string) { Password: userID.String(), }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -148,10 +150,12 @@ func testVLESSXrayOutbound(t *testing.T, packetEncoding string, flow string) { ServerPort: otherPort, }, Password: userID.String(), - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, DialerOptions: option.DialerOptions{ Detour: "vless", @@ -169,10 +173,12 @@ func testVLESSXrayOutbound(t *testing.T, packetEncoding string, flow string) { UUID: userID.String(), Flow: flow, PacketEncoding: &packetEncoding, - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, }, }, @@ -238,11 +244,13 @@ func testVLESSSelf(t *testing.T, flow string) { Flow: flow, }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -261,10 +269,12 @@ func testVLESSSelf(t *testing.T, flow string) { }, UUID: userUUID.String(), Flow: flow, - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, }, }, @@ -313,11 +323,13 @@ func testVLESSSelfTLS(t *testing.T, flow string) { Flow: flow, }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -335,11 +347,13 @@ func testVLESSSelfTLS(t *testing.T, flow string) { Password: userUUID.String(), }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -357,10 +371,12 @@ func testVLESSSelfTLS(t *testing.T, flow string) { ServerPort: otherPort, }, Password: userUUID.String(), - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, DialerOptions: option.DialerOptions{ Detour: "vless-out", @@ -377,10 +393,12 @@ func testVLESSSelfTLS(t *testing.T, flow string) { }, UUID: userUUID.String(), Flow: flow, - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, }, }, @@ -424,11 +442,13 @@ func testVLESSXrayInbound(t *testing.T, flow string) { Flow: flow, }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -446,11 +466,13 @@ func testVLESSXrayInbound(t *testing.T, flow string) { Password: userId.String(), }, }, - TLS: &option.InboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, - KeyPath: keyPem, + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, }, }, }, @@ -480,10 +502,12 @@ func testVLESSXrayInbound(t *testing.T, flow string) { ServerPort: otherPort, }, Password: userId.String(), - TLS: &option.OutboundTLSOptions{ - Enabled: true, - ServerName: "example.org", - CertificatePath: certPem, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, }, DialerOptions: option.DialerOptions{ Detour: "vless-out", diff --git a/test/wrapper_test.go b/test/wrapper_test.go new file mode 100644 index 0000000000..d2b6b9ffc1 --- /dev/null +++ b/test/wrapper_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + + "github.com/stretchr/testify/require" +) + +func TestOptionsWrapper(t *testing.T) { + inbound := option.Inbound{ + Type: C.TypeHTTP, + HTTPOptions: option.HTTPMixedInboundOptions{ + InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ + TLS: &option.InboundTLSOptions{ + Enabled: true, + }, + }, + }, + } + rawOptions, err := inbound.RawOptions() + require.NoError(t, err) + tlsOptionsWrapper, loaded := rawOptions.(option.InboundTLSOptionsWrapper) + require.True(t, loaded, "find inbound tls options") + tlsOptions := tlsOptionsWrapper.TakeInboundTLSOptions() + require.NotNil(t, tlsOptions, "find inbound tls options") + tlsOptions.Enabled = false + tlsOptionsWrapper.ReplaceInboundTLSOptions(tlsOptions) + require.False(t, inbound.HTTPOptions.TLS.Enabled, "replace tls enabled") +} diff --git a/transport/fakeip/packet_wait.go b/transport/fakeip/packet_wait.go index 3e3fd89f5d..9fa4a5bd78 100644 --- a/transport/fakeip/packet_wait.go +++ b/transport/fakeip/packet_wait.go @@ -17,16 +17,16 @@ func (c *NATPacketConn) CreatePacketReadWaiter() (N.PacketReadWaiter, bool) { type waitNATPacketConn struct { *NATPacketConn - waiter N.PacketReadWaiter + readWaiter N.PacketReadWaiter } -func (c *waitNATPacketConn) InitializeReadWaiter(newBuffer func() *buf.Buffer) { - c.waiter.InitializeReadWaiter(newBuffer) +func (c *waitNATPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + return c.readWaiter.InitializeReadWaiter(options) } -func (c *waitNATPacketConn) WaitReadPacket() (destination M.Socksaddr, err error) { - destination, err = c.waiter.WaitReadPacket() - if socksaddrWithoutPort(destination) == c.origin { +func (c *waitNATPacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { + buffer, destination, err = c.readWaiter.WaitReadPacket() + if err == nil && socksaddrWithoutPort(destination) == c.origin { destination = M.Socksaddr{ Addr: c.destination.Addr, Fqdn: c.destination.Fqdn, diff --git a/transport/fakeip/store.go b/transport/fakeip/store.go index 96f6bf031e..83677b0d05 100644 --- a/transport/fakeip/store.go +++ b/transport/fakeip/store.go @@ -1,17 +1,19 @@ package fakeip import ( + "context" "net/netip" "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" ) var _ adapter.FakeIPStore = (*Store)(nil) type Store struct { - router adapter.Router + ctx context.Context logger logger.Logger inet4Range netip.Prefix inet6Range netip.Prefix @@ -20,9 +22,9 @@ type Store struct { inet6Current netip.Addr } -func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { +func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { return &Store{ - router: router, + ctx: ctx, logger: logger, inet4Range: inet4Range, inet6Range: inet6Range, @@ -31,10 +33,9 @@ func NewStore(router adapter.Router, logger logger.Logger, inet4Range netip.Pref func (s *Store) Start() error { var storage adapter.FakeIPStorage - if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreFakeIP() { - if cacheFile := clashServer.CacheFile(); cacheFile != nil { - storage = cacheFile - } + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil && cacheFile.StoreFakeIP() { + storage = cacheFile } if storage == nil { storage = NewMemoryStorage() diff --git a/transport/trojan/mux.go b/transport/trojan/mux.go index 773240008d..13ac1e83a9 100644 --- a/transport/trojan/mux.go +++ b/transport/trojan/mux.go @@ -53,7 +53,7 @@ func newMuxConnection0(ctx context.Context, stream net.Conn, metadata M.Metadata case CommandTCP: return handler.NewConnection(ctx, stream, metadata) case CommandUDP: - return handler.NewPacketConnection(ctx, &PacketConn{stream}, metadata) + return handler.NewPacketConnection(ctx, &PacketConn{Conn: stream}, metadata) default: return E.New("unknown command ", command) } diff --git a/transport/trojan/protocol.go b/transport/trojan/protocol.go index 09e1878251..394ba291a1 100644 --- a/transport/trojan/protocol.go +++ b/transport/trojan/protocol.go @@ -85,9 +85,10 @@ func (c *ClientConn) Upstream() any { type ClientPacketConn struct { net.Conn - access sync.Mutex - key [KeyLength]byte - headerWritten bool + access sync.Mutex + key [KeyLength]byte + headerWritten bool + readWaitOptions N.ReadWaitOptions } func NewClientPacketConn(conn net.Conn, key [KeyLength]byte) *ClientPacketConn { diff --git a/transport/trojan/protocol_wait.go b/transport/trojan/protocol_wait.go new file mode 100644 index 0000000000..c6b4ec06ef --- /dev/null +++ b/transport/trojan/protocol_wait.go @@ -0,0 +1,45 @@ +package trojan + +import ( + "encoding/binary" + + "github.com/sagernet/sing/common/buf" + 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" +) + +var _ N.PacketReadWaiter = (*ClientPacketConn)(nil) + +func (c *ClientPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readWaitOptions = options + return false +} + +func (c *ClientPacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { + destination, err = M.SocksaddrSerializer.ReadAddrPort(c.Conn) + if err != nil { + return nil, M.Socksaddr{}, E.Cause(err, "read destination") + } + + var length uint16 + err = binary.Read(c.Conn, binary.BigEndian, &length) + if err != nil { + return nil, M.Socksaddr{}, E.Cause(err, "read chunk length") + } + + err = rw.SkipN(c.Conn, 2) + if err != nil { + return nil, M.Socksaddr{}, E.Cause(err, "skip crlf") + } + + buffer = c.readWaitOptions.NewPacketBuffer() + _, err = buffer.ReadFullFrom(c.Conn, int(length)) + if err != nil { + buffer.Release() + return + } + c.readWaitOptions.PostReturn(buffer) + return +} diff --git a/transport/trojan/service.go b/transport/trojan/service.go index de6bd7e85d..9078276c73 100644 --- a/transport/trojan/service.go +++ b/transport/trojan/service.go @@ -105,7 +105,7 @@ func (s *Service[K]) NewConnection(ctx context.Context, conn net.Conn, metadata case CommandTCP: return s.handler.NewConnection(ctx, conn, metadata) case CommandUDP: - return s.handler.NewPacketConnection(ctx, &PacketConn{conn}, metadata) + return s.handler.NewPacketConnection(ctx, &PacketConn{Conn: conn}, metadata) // case CommandMux: default: return HandleMuxConnection(ctx, conn, metadata, s.handler) @@ -122,6 +122,7 @@ func (s *Service[K]) fallback(ctx context.Context, conn net.Conn, metadata M.Met type PacketConn struct { net.Conn + readWaitOptions N.ReadWaitOptions } func (c *PacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { diff --git a/transport/trojan/service_wait.go b/transport/trojan/service_wait.go new file mode 100644 index 0000000000..5ec082fedd --- /dev/null +++ b/transport/trojan/service_wait.go @@ -0,0 +1,45 @@ +package trojan + +import ( + "encoding/binary" + + "github.com/sagernet/sing/common/buf" + 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" +) + +var _ N.PacketReadWaiter = (*PacketConn)(nil) + +func (c *PacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readWaitOptions = options + return false +} + +func (c *PacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { + destination, err = M.SocksaddrSerializer.ReadAddrPort(c.Conn) + if err != nil { + return nil, M.Socksaddr{}, E.Cause(err, "read destination") + } + + var length uint16 + err = binary.Read(c.Conn, binary.BigEndian, &length) + if err != nil { + return nil, M.Socksaddr{}, E.Cause(err, "read chunk length") + } + + err = rw.SkipN(c.Conn, 2) + if err != nil { + return nil, M.Socksaddr{}, E.Cause(err, "skip crlf") + } + + buffer = c.readWaitOptions.NewPacketBuffer() + _, err = buffer.ReadFullFrom(c.Conn, int(length)) + if err != nil { + buffer.Release() + return + } + c.readWaitOptions.PostReturn(buffer) + return +} diff --git a/transport/vless/vision.go b/transport/vless/vision.go index 3851f6d52e..5ee2d0dff5 100644 --- a/transport/vless/vision.go +++ b/transport/vless/vision.go @@ -24,7 +24,7 @@ var tlsRegistry []func(conn net.Conn) (loaded bool, netConn net.Conn, reflectTyp func init() { tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer uintptr) { - tlsConn, loaded := conn.(*tls.Conn) + tlsConn, loaded := common.Cast[*tls.Conn](conn) if !loaded { return } @@ -134,7 +134,7 @@ func (c *VisionConn) Read(p []byte) (n int, err error) { buffers = common.Map(buffers, func(it *buf.Buffer) *buf.Buffer { return it.ToOwned() }) - chunkBuffer.FullReset() + chunkBuffer.Reset() } if c.remainingContent == 0 && c.remainingPadding == 0 { if c.currentCommand == commandPaddingEnd { diff --git a/transport/wireguard/client_bind.go b/transport/wireguard/client_bind.go index 2b56f73a3c..4d39120530 100644 --- a/transport/wireguard/client_bind.go +++ b/transport/wireguard/client_bind.go @@ -12,7 +12,6 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/service/pause" "github.com/sagernet/wireguard-go/conn" ) @@ -22,33 +21,27 @@ type ClientBind struct { ctx context.Context errorHandler E.Handler dialer N.Dialer - reservedForEndpoint map[M.Socksaddr][3]uint8 + reservedForEndpoint map[netip.AddrPort][3]uint8 connAccess sync.Mutex conn *wireConn done chan struct{} isConnect bool - connectAddr M.Socksaddr + connectAddr netip.AddrPort reserved [3]uint8 - pauseManager pause.Manager } -func NewClientBind(ctx context.Context, errorHandler E.Handler, dialer N.Dialer, isConnect bool, connectAddr M.Socksaddr, reserved [3]uint8) *ClientBind { +func NewClientBind(ctx context.Context, errorHandler E.Handler, dialer N.Dialer, isConnect bool, connectAddr netip.AddrPort, reserved [3]uint8) *ClientBind { return &ClientBind{ ctx: ctx, errorHandler: errorHandler, dialer: dialer, - reservedForEndpoint: make(map[M.Socksaddr][3]uint8), + reservedForEndpoint: make(map[netip.AddrPort][3]uint8), isConnect: isConnect, connectAddr: connectAddr, reserved: reserved, - pauseManager: pause.ManagerFromContext(ctx), } } -func (c *ClientBind) SetReservedForEndpoint(destination M.Socksaddr, reserved [3]byte) { - c.reservedForEndpoint[destination] = reserved -} - func (c *ClientBind) connect() (*wireConn, error) { serverConn := c.conn if serverConn != nil { @@ -71,16 +64,13 @@ func (c *ClientBind) connect() (*wireConn, error) { } } if c.isConnect { - udpConn, err := c.dialer.DialContext(c.ctx, N.NetworkUDP, c.connectAddr) + udpConn, err := c.dialer.DialContext(c.ctx, N.NetworkUDP, M.SocksaddrFromNetIP(c.connectAddr)) if err != nil { return nil, err } c.conn = &wireConn{ - PacketConn: &bufio.UnbindPacketConn{ - ExtendedConn: bufio.NewExtendedConn(udpConn), - Addr: c.connectAddr, - }, - done: make(chan struct{}), + PacketConn: bufio.NewUnbindPacketConn(udpConn), + done: make(chan struct{}), } } else { udpConn, err := c.dialer.ListenPacket(c.ctx, M.Socksaddr{Addr: netip.IPv4Unspecified()}) @@ -116,7 +106,6 @@ func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint) c.errorHandler.NewError(context.Background(), E.Cause(err, "connect to server")) err = nil time.Sleep(time.Second) - c.pauseManager.WaitActive() return } n, addr, err := udpConn.ReadFrom(packets[0]) @@ -133,11 +122,9 @@ func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint) sizes[0] = n if n > 3 { b := packets[0] - b[1] = 0 - b[2] = 0 - b[3] = 0 + common.ClearArray(b[1:4]) } - eps[0] = Endpoint(M.SocksaddrFromNet(addr)) + eps[0] = Endpoint(M.AddrPortFromNet(addr)) count = 1 return } @@ -170,18 +157,16 @@ func (c *ClientBind) Send(bufs [][]byte, ep conn.Endpoint) error { if err != nil { return err } - destination := M.Socksaddr(ep.(Endpoint)) + destination := netip.AddrPort(ep.(Endpoint)) for _, b := range bufs { if len(b) > 3 { reserved, loaded := c.reservedForEndpoint[destination] if !loaded { reserved = c.reserved } - b[1] = reserved[0] - b[2] = reserved[1] - b[3] = reserved[2] + copy(b[1:4], reserved[:]) } - _, err = udpConn.WriteTo(b, destination) + _, err = udpConn.WriteTo(b, M.SocksaddrFromNetIP(destination)) if err != nil { udpConn.Close() return err @@ -191,13 +176,21 @@ func (c *ClientBind) Send(bufs [][]byte, ep conn.Endpoint) error { } func (c *ClientBind) ParseEndpoint(s string) (conn.Endpoint, error) { - return Endpoint(M.ParseSocksaddr(s)), nil + ap, err := netip.ParseAddrPort(s) + if err != nil { + return nil, err + } + return Endpoint(ap), nil } func (c *ClientBind) BatchSize() int { return 1 } +func (c *ClientBind) SetReservedForEndpoint(destination netip.AddrPort, reserved [3]byte) { + c.reservedForEndpoint[destination] = reserved +} + type wireConn struct { net.PacketConn access sync.Mutex diff --git a/transport/wireguard/device_stack.go b/transport/wireguard/device_stack.go index 4ddaffe1f2..9d9b4549bd 100644 --- a/transport/wireguard/device_stack.go +++ b/transport/wireguard/device_stack.go @@ -265,7 +265,7 @@ func (ep *wireEndpoint) LinkAddress() tcpip.LinkAddress { } func (ep *wireEndpoint) Capabilities() stack.LinkEndpointCapabilities { - return stack.CapabilityNone + return stack.CapabilityRXChecksumOffload } func (ep *wireEndpoint) Attach(dispatcher stack.NetworkDispatcher) { diff --git a/transport/wireguard/device_system.go b/transport/wireguard/device_system.go index 986264040b..49acc5b90e 100644 --- a/transport/wireguard/device_system.go +++ b/transport/wireguard/device_system.go @@ -2,6 +2,7 @@ package wireguard import ( "context" + "errors" "net" "net/netip" "os" @@ -11,6 +12,7 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" wgTun "github.com/sagernet/wireguard-go/tun" @@ -19,16 +21,17 @@ import ( var _ Device = (*SystemDevice)(nil) type SystemDevice struct { - dialer N.Dialer - device tun.Tun - name string - mtu int - events chan wgTun.Event - addr4 netip.Addr - addr6 netip.Addr + dialer N.Dialer + device tun.Tun + batchDevice tun.LinuxTUN + name string + mtu int + events chan wgTun.Event + addr4 netip.Addr + addr6 netip.Addr } -func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes []netip.Prefix, mtu uint32) (*SystemDevice, error) { +func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes []netip.Prefix, mtu uint32, gso bool) (*SystemDevice, error) { var inet4Addresses []netip.Prefix var inet6Addresses []netip.Prefix for _, prefixes := range localPrefixes { @@ -46,6 +49,7 @@ func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes Inet4Address: inet4Addresses, Inet6Address: inet6Addresses, MTU: mtu, + GSO: gso, }) if err != nil { return nil, err @@ -58,16 +62,25 @@ func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes if len(inet6Addresses) > 0 { inet6Address = inet6Addresses[0].Addr() } + var batchDevice tun.LinuxTUN + if gso { + batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN) + if !isBatchTUN { + return nil, E.New("GSO is not supported on current platform") + } + batchDevice = batchTUN + } return &SystemDevice{ dialer: common.Must1(dialer.NewDefault(router, option.DialerOptions{ BindInterface: interfaceName, })), - device: tunInterface, - name: interfaceName, - mtu: int(mtu), - events: make(chan wgTun.Event), - addr4: inet4Address, - addr6: inet6Address, + device: tunInterface, + batchDevice: batchDevice, + name: interfaceName, + mtu: int(mtu), + events: make(chan wgTun.Event), + addr4: inet4Address, + addr6: inet6Address, }, nil } @@ -97,21 +110,31 @@ func (w *SystemDevice) File() *os.File { } func (w *SystemDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) { - sizes[0], err = w.device.Read(bufs[0][offset-tun.PacketOffset:]) - if err == nil { - count = 1 + if w.batchDevice != nil { + count, err = w.batchDevice.BatchRead(bufs, offset, sizes) + } else { + sizes[0], err = w.device.Read(bufs[0][offset:]) + if err == nil { + count = 1 + } else if errors.Is(err, tun.ErrTooManySegments) { + err = wgTun.ErrTooManySegments + } } return } func (w *SystemDevice) Write(bufs [][]byte, offset int) (count int, err error) { - for _, b := range bufs { - _, err = w.device.Write(b[offset:]) - if err != nil { - return + if w.batchDevice != nil { + return 0, w.batchDevice.BatchWrite(bufs, offset) + } else { + for _, b := range bufs { + _, err = w.device.Write(b[offset:]) + if err != nil { + return + } } - count++ } + // WireGuard will not read count return } @@ -136,5 +159,8 @@ func (w *SystemDevice) Close() error { } func (w *SystemDevice) BatchSize() int { + if w.batchDevice != nil { + return w.batchDevice.BatchSize() + } return 1 } diff --git a/transport/wireguard/endpoint.go b/transport/wireguard/endpoint.go index dd2b7dbc03..3c3ec7db5c 100644 --- a/transport/wireguard/endpoint.go +++ b/transport/wireguard/endpoint.go @@ -3,13 +3,12 @@ package wireguard import ( "net/netip" - M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/wireguard-go/conn" ) var _ conn.Endpoint = (*Endpoint)(nil) -type Endpoint M.Socksaddr +type Endpoint netip.AddrPort func (e Endpoint) ClearSrc() { } @@ -19,16 +18,16 @@ func (e Endpoint) SrcToString() string { } func (e Endpoint) DstToString() string { - return (M.Socksaddr)(e).String() + return (netip.AddrPort)(e).String() } func (e Endpoint) DstToBytes() []byte { - b, _ := (M.Socksaddr)(e).AddrPort().MarshalBinary() + b, _ := (netip.AddrPort)(e).MarshalBinary() return b } func (e Endpoint) DstIP() netip.Addr { - return (M.Socksaddr)(e).Addr + return (netip.AddrPort)(e).Addr() } func (e Endpoint) SrcIP() netip.Addr { diff --git a/transport/wireguard/resolve.go b/transport/wireguard/resolve.go new file mode 100644 index 0000000000..5b4124d201 --- /dev/null +++ b/transport/wireguard/resolve.go @@ -0,0 +1,148 @@ +package wireguard + +import ( + "context" + "encoding/base64" + "encoding/hex" + "net/netip" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" + dns "github.com/sagernet/sing-dns" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" +) + +type PeerConfig struct { + destination M.Socksaddr + domainStrategy dns.DomainStrategy + Endpoint netip.AddrPort + PublicKey string + PreSharedKey string + AllowedIPs []string + Reserved [3]uint8 +} + +func (c PeerConfig) GenerateIpcLines() string { + ipcLines := "\npublic_key=" + c.PublicKey + ipcLines += "\nendpoint=" + c.Endpoint.String() + if c.PreSharedKey != "" { + ipcLines += "\npreshared_key=" + c.PreSharedKey + } + for _, allowedIP := range c.AllowedIPs { + ipcLines += "\nallowed_ip=" + allowedIP + } + return ipcLines +} + +func ParsePeers(options option.WireGuardOutboundOptions) ([]PeerConfig, error) { + var peers []PeerConfig + if len(options.Peers) > 0 { + for peerIndex, rawPeer := range options.Peers { + peer := PeerConfig{ + AllowedIPs: rawPeer.AllowedIPs, + } + destination := rawPeer.ServerOptions.Build() + if destination.IsFqdn() { + peer.destination = destination + peer.domainStrategy = dns.DomainStrategy(options.DomainStrategy) + } else { + peer.Endpoint = destination.AddrPort() + } + { + bytes, err := base64.StdEncoding.DecodeString(rawPeer.PublicKey) + if err != nil { + return nil, E.Cause(err, "decode public key for peer ", peerIndex) + } + peer.PublicKey = hex.EncodeToString(bytes) + } + if rawPeer.PreSharedKey != "" { + bytes, err := base64.StdEncoding.DecodeString(rawPeer.PreSharedKey) + if err != nil { + return nil, E.Cause(err, "decode pre shared key for peer ", peerIndex) + } + peer.PreSharedKey = hex.EncodeToString(bytes) + } + if len(rawPeer.AllowedIPs) == 0 { + return nil, E.New("missing allowed_ips for peer ", peerIndex) + } + if len(rawPeer.Reserved) > 0 { + if len(rawPeer.Reserved) != 3 { + return nil, E.New("invalid reserved value for peer ", peerIndex, ", required 3 bytes, got ", len(peer.Reserved)) + } + copy(peer.Reserved[:], options.Reserved) + } + peers = append(peers, peer) + } + } else { + peer := PeerConfig{} + var ( + addressHas4 bool + addressHas6 bool + ) + for _, localAddress := range options.LocalAddress { + if localAddress.Addr().Is4() { + addressHas4 = true + } else { + addressHas6 = true + } + } + if addressHas4 { + peer.AllowedIPs = append(peer.AllowedIPs, netip.PrefixFrom(netip.IPv4Unspecified(), 0).String()) + } + if addressHas6 { + peer.AllowedIPs = append(peer.AllowedIPs, netip.PrefixFrom(netip.IPv6Unspecified(), 0).String()) + } + destination := options.ServerOptions.Build() + if destination.IsFqdn() { + peer.destination = destination + peer.domainStrategy = dns.DomainStrategy(options.DomainStrategy) + } else { + peer.Endpoint = destination.AddrPort() + } + { + bytes, err := base64.StdEncoding.DecodeString(options.PeerPublicKey) + if err != nil { + return nil, E.Cause(err, "decode peer public key") + } + peer.PublicKey = hex.EncodeToString(bytes) + } + if options.PreSharedKey != "" { + bytes, err := base64.StdEncoding.DecodeString(options.PreSharedKey) + if err != nil { + return nil, E.Cause(err, "decode pre shared key") + } + peer.PreSharedKey = hex.EncodeToString(bytes) + } + if len(options.Reserved) > 0 { + if len(options.Reserved) != 3 { + return nil, E.New("invalid reserved value, required 3 bytes, got ", len(peer.Reserved)) + } + copy(peer.Reserved[:], options.Reserved) + } + peers = append(peers, peer) + } + return peers, nil +} + +func ResolvePeers(ctx context.Context, router adapter.Router, peers []PeerConfig) error { + for peerIndex, peer := range peers { + if peer.Endpoint.IsValid() { + continue + } + destinationAddresses, err := router.Lookup(ctx, peer.destination.Fqdn, peer.domainStrategy) + if err != nil { + if len(peers) == 1 { + return E.Cause(err, "resolve endpoint domain") + } else { + return E.Cause(err, "resolve endpoint domain for peer ", peerIndex) + } + } + if len(destinationAddresses) == 0 { + return E.New("no addresses found for endpoint domain: ", peer.destination.Fqdn) + } + peers[peerIndex].Endpoint = netip.AddrPortFrom(destinationAddresses[0], peer.destination.Port) + + } + return nil +}