mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
Compare commits
15 Commits
dev-next
...
v1.8.0-alp
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4af7bf9dcd | ||
![]() |
8be5ca0b17 | ||
![]() |
330a712be6 | ||
![]() |
4bea4e3e2a | ||
![]() |
adb3c57c51 | ||
![]() |
72f91dfe5d | ||
![]() |
38608d138f | ||
![]() |
96b18f597d | ||
![]() |
d14c66058d | ||
![]() |
71960374ff | ||
![]() |
1f36575e75 | ||
![]() |
be3367c11e | ||
![]() |
7c2ce9563a | ||
![]() |
151fb26541 | ||
![]() |
b4825fa33a |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
/.idea/
|
||||
/vendor/
|
||||
/*.json
|
||||
/*.srs
|
||||
/*.db
|
||||
/site/
|
||||
/bin/
|
||||
|
@ -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 {
|
||||
|
@ -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{}
|
||||
|
@ -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"
|
||||
@ -19,7 +21,7 @@ type Router interface {
|
||||
|
||||
Outbounds() []Outbound
|
||||
Outbound(tag string) (Outbound, bool)
|
||||
DefaultOutbound(network string) Outbound
|
||||
DefaultOutbound(network string) (Outbound, error)
|
||||
|
||||
FakeIPStore() FakeIPStore
|
||||
|
||||
@ -28,6 +30,8 @@ type Router interface {
|
||||
GeoIPReader() *geoip.Reader
|
||||
LoadGeosite(code string) (Rule, error)
|
||||
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
|
||||
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
|
||||
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
|
||||
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
|
||||
@ -62,11 +66,15 @@ func RouterFromContext(ctx context.Context) Router {
|
||||
return service.FromContext[Router](ctx)
|
||||
}
|
||||
|
||||
type HeadlessRule interface {
|
||||
Match(metadata *InboundContext) bool
|
||||
}
|
||||
|
||||
type Rule interface {
|
||||
HeadlessRule
|
||||
Service
|
||||
Type() string
|
||||
UpdateGeosite() error
|
||||
Match(metadata *InboundContext) bool
|
||||
Outbound() string
|
||||
String() string
|
||||
}
|
||||
@ -77,6 +85,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()
|
||||
}
|
||||
|
98
box.go
98
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()
|
||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
||||
var needCacheFile bool
|
||||
var needClashAPI bool
|
||||
var needV2RayAPI bool
|
||||
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil {
|
||||
needCacheFile = true
|
||||
}
|
||||
if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
|
||||
needClashAPI = true
|
||||
}
|
||||
@ -145,8 +153,14 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "initialize platform interface")
|
||||
}
|
||||
}
|
||||
preServices := make(map[string]adapter.Service)
|
||||
preServices1 := make(map[string]adapter.Service)
|
||||
preServices2 := make(map[string]adapter.Service)
|
||||
postServices := make(map[string]adapter.Service)
|
||||
if needCacheFile {
|
||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
preServices1["cache file"] = cacheFile
|
||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||
}
|
||||
if needClashAPI {
|
||||
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
||||
@ -155,7 +169,7 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "create clash api server")
|
||||
}
|
||||
router.SetClashServer(clashServer)
|
||||
preServices["clash api"] = clashServer
|
||||
preServices2["clash api"] = clashServer
|
||||
}
|
||||
if needV2RayAPI {
|
||||
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
||||
@ -163,7 +177,7 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "create v2ray api server")
|
||||
}
|
||||
router.SetV2RayServer(v2rayServer)
|
||||
preServices["v2ray api"] = v2rayServer
|
||||
preServices2["v2ray api"] = v2rayServer
|
||||
}
|
||||
return &Box{
|
||||
router: router,
|
||||
@ -172,7 +186,8 @@ func New(options Options) (*Box, error) {
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
preServices: preServices,
|
||||
preServices1: preServices1,
|
||||
preServices2: preServices2,
|
||||
postServices: postServices,
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
@ -217,16 +232,34 @@ 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 {
|
||||
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()
|
||||
for serviceName, service := range s.preServices2 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
err = s.startOutbounds()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -238,8 +271,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)
|
||||
@ -252,7 +290,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, "]")
|
||||
@ -263,7 +300,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)
|
||||
@ -271,14 +307,16 @@ 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")
|
||||
err := s.router.PostStart()
|
||||
if err != nil {
|
||||
return E.Cause(err, "post-start router")
|
||||
}
|
||||
return s.router.PostStart()
|
||||
}
|
||||
|
||||
@ -289,41 +327,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
|
||||
|
@ -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, "]")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
43
cmd/sing-box/cmd_geoip.go
Normal file
43
cmd/sing-box/cmd_geoip.go
Normal file
@ -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
|
||||
}
|
98
cmd/sing-box/cmd_geoip_export.go
Normal file
98
cmd/sing-box/cmd_geoip_export.go
Normal file
@ -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-<country>.srs"
|
||||
|
||||
var commandGeoipExport = &cobra.Command{
|
||||
Use: "export <country>",
|
||||
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)
|
||||
}
|
31
cmd/sing-box/cmd_geoip_list.go
Normal file
31
cmd/sing-box/cmd_geoip_list.go
Normal file
@ -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
|
||||
}
|
47
cmd/sing-box/cmd_geoip_lookup.go
Normal file
47
cmd/sing-box/cmd_geoip_lookup.go
Normal file
@ -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 <address>",
|
||||
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
|
||||
}
|
41
cmd/sing-box/cmd_geosite.go
Normal file
41
cmd/sing-box/cmd_geosite.go
Normal file
@ -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
|
||||
}
|
81
cmd/sing-box/cmd_geosite_export.go
Normal file
81
cmd/sing-box/cmd_geosite_export.go
Normal file
@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/common/geosite"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandGeositeExportOutput string
|
||||
|
||||
const commandGeositeExportDefaultOutput = "geosite-<category>.json"
|
||||
|
||||
var commandGeositeExport = &cobra.Command{
|
||||
Use: "export <category>",
|
||||
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)
|
||||
}
|
50
cmd/sing-box/cmd_geosite_list.go
Normal file
50
cmd/sing-box/cmd_geosite_list.go
Normal file
@ -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 <category>",
|
||||
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
|
||||
}
|
97
cmd/sing-box/cmd_geosite_lookup.go
Normal file
97
cmd/sing-box/cmd_geosite_lookup.go
Normal file
@ -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] <domain>",
|
||||
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
|
||||
}
|
56
cmd/sing-box/cmd_geosite_matcher.go
Normal file
56
cmd/sing-box/cmd_geosite_matcher.go
Normal file
@ -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 ""
|
||||
}
|
@ -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 <output>",
|
||||
Short: "Merge configurations",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := merge(args[0])
|
||||
|
14
cmd/sing-box/cmd_rule_set.go
Normal file
14
cmd/sing-box/cmd_rule_set.go
Normal file
@ -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)
|
||||
}
|
80
cmd/sing-box/cmd_rule_set_compile.go
Normal file
80
cmd/sing-box/cmd_rule_set_compile.go
Normal file
@ -0,0 +1,80 @@
|
||||
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 = "<file_name>.srs"
|
||||
|
||||
var commandRuleSetCompile = &cobra.Command{
|
||||
Use: "compile [source-path]",
|
||||
Short: "Compile rule-set json to binary",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := compileRuleSet(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandRuleSet.AddCommand(commandRuleSetCompile)
|
||||
commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file")
|
||||
}
|
||||
|
||||
func compileRuleSet(sourcePath string) error {
|
||||
var (
|
||||
reader io.Reader
|
||||
err error
|
||||
)
|
||||
if sourcePath == "stdin" {
|
||||
reader = os.Stdin
|
||||
} else {
|
||||
reader, err = os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
decoder := json.NewDecoder(json.NewCommentFilter(reader))
|
||||
decoder.DisallowUnknownFields()
|
||||
var plainRuleSet option.PlainRuleSetCompat
|
||||
err = decoder.Decode(&plainRuleSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ruleSet := plainRuleSet.Upgrade()
|
||||
var outputPath string
|
||||
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
|
||||
if strings.HasSuffix(sourcePath, ".json") {
|
||||
outputPath = sourcePath[:len(sourcePath)-5] + ".srs"
|
||||
} else {
|
||||
outputPath = sourcePath + ".srs"
|
||||
}
|
||||
} else {
|
||||
outputPath = flagRuleSetCompileOutput
|
||||
}
|
||||
outputFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = srs.Write(outputFile, ruleSet)
|
||||
if err != nil {
|
||||
outputFile.Close()
|
||||
os.Remove(outputPath)
|
||||
return err
|
||||
}
|
||||
outputFile.Close()
|
||||
return nil
|
||||
}
|
87
cmd/sing-box/cmd_rule_set_format.go
Normal file
87
cmd/sing-box/cmd_rule_set_format.go
Normal file
@ -0,0 +1,87 @@
|
||||
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 <source-path>",
|
||||
Short: "Format rule-set json",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := formatRuleSet(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
|
||||
commandRuleSet.AddCommand(commandRuleSetFormat)
|
||||
}
|
||||
|
||||
func formatRuleSet(sourcePath string) error {
|
||||
var (
|
||||
reader io.Reader
|
||||
err error
|
||||
)
|
||||
if sourcePath == "stdin" {
|
||||
reader = os.Stdin
|
||||
} else {
|
||||
reader, err = os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content)))
|
||||
decoder.DisallowUnknownFields()
|
||||
var plainRuleSet option.PlainRuleSetCompat
|
||||
err = decoder.Decode(&plainRuleSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ruleSet := plainRuleSet.Upgrade()
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(ruleSet)
|
||||
if err != nil {
|
||||
return E.Cause(err, "encode config")
|
||||
}
|
||||
outputPath, _ := filepath.Abs(sourcePath)
|
||||
if !commandRuleSetFormatFlagWrite || sourcePath == "stdin" {
|
||||
os.Stdout.WriteString(buffer.String() + "\n")
|
||||
return nil
|
||||
}
|
||||
if bytes.Equal(content, buffer.Bytes()) {
|
||||
return nil
|
||||
}
|
||||
output, err := os.Create(sourcePath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "open output")
|
||||
}
|
||||
_, err = output.Write(buffer.Bytes())
|
||||
output.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "write output")
|
||||
}
|
||||
os.Stderr.WriteString(outputPath + "\n")
|
||||
return nil
|
||||
}
|
@ -13,10 +13,11 @@ 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/badjson"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -108,7 +109,7 @@ func readConfigAndMerge() (option.Options, error) {
|
||||
}
|
||||
var mergedOptions option.Options
|
||||
for _, options := range optionsList {
|
||||
mergedOptions, err = badjsonmerge.MergeOptions(options.options, mergedOptions)
|
||||
mergedOptions, err = badjson.Merge(options.options, mergedOptions)
|
||||
if err != nil {
|
||||
return option.Options{}, E.Cause(err, "merge config at ", options.path)
|
||||
}
|
||||
@ -193,7 +194,7 @@ func run() error {
|
||||
}
|
||||
|
||||
func closeMonitor(ctx context.Context) {
|
||||
time.Sleep(3 * time.Second)
|
||||
time.Sleep(C.DefaultStopFatalTimeout)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
@ -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 {
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
var commandConnectFlagNetwork string
|
||||
|
||||
var commandConnect = &cobra.Command{
|
||||
Use: "connect [address]",
|
||||
Use: "connect <address>",
|
||||
Short: "Connect to an address",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@ -37,7 +38,7 @@ func main() {
|
||||
|
||||
func preRun(cmd *cobra.Command, args []string) {
|
||||
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)
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
119
common/badtls/read_wait.go
Normal file
119
common/badtls/read_wait.go
Normal file
@ -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 := rawHalfConn.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 := rawHalfConn.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()))
|
||||
rawReadRecord := rawConn.MethodByName("readRecord")
|
||||
if !rawReadRecord.IsValid() {
|
||||
return nil, E.New("badtls: invalid readRecord")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
//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
|
13
common/badtls/read_wait_stub.go
Normal file
13
common/badtls/read_wait_stub.go
Normal file
@ -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
|
||||
}
|
@ -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())
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
)
|
485
common/srs/binary.go
Normal file
485
common/srs/binary.go
Normal file
@ -0,0 +1,485 @@
|
||||
package srs
|
||||
|
||||
import (
|
||||
"compress/zlib"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net/netip"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/domain"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
|
||||
"go4.org/netipx"
|
||||
)
|
||||
|
||||
var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS
|
||||
|
||||
const (
|
||||
ruleItemQueryType uint8 = iota
|
||||
ruleItemNetwork
|
||||
ruleItemDomain
|
||||
ruleItemDomainKeyword
|
||||
ruleItemDomainRegex
|
||||
ruleItemSourceIPCIDR
|
||||
ruleItemIPCIDR
|
||||
ruleItemSourcePort
|
||||
ruleItemSourcePortRange
|
||||
ruleItemPort
|
||||
ruleItemPortRange
|
||||
ruleItemProcessName
|
||||
ruleItemProcessPath
|
||||
ruleItemPackageName
|
||||
ruleItemWIFISSID
|
||||
ruleItemWIFIBSSID
|
||||
ruleItemFinal uint8 = 0xFF
|
||||
)
|
||||
|
||||
func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) {
|
||||
var magicBytes [3]byte
|
||||
_, err = io.ReadFull(reader, magicBytes[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if magicBytes != MagicBytes {
|
||||
err = E.New("invalid sing-box rule set file")
|
||||
return
|
||||
}
|
||||
var version uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &version)
|
||||
if err != nil {
|
||||
return ruleSet, err
|
||||
}
|
||||
if version != 1 {
|
||||
return ruleSet, E.New("unsupported version: ", version)
|
||||
}
|
||||
zReader, err := zlib.NewReader(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
length, err := rw.ReadUVariant(zReader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ruleSet.Rules = make([]option.HeadlessRule, length)
|
||||
for i := uint64(0); i < length; i++ {
|
||||
ruleSet.Rules[i], err = readRule(zReader, recovery)
|
||||
if err != nil {
|
||||
err = E.Cause(err, "read rule[", i, "]")
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Write(writer io.Writer, ruleSet option.PlainRuleSet) error {
|
||||
_, err := writer.Write(MagicBytes[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, uint8(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rule := range ruleSet.Rules {
|
||||
err = writeRule(zWriter, rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return zWriter.Close()
|
||||
}
|
||||
|
||||
func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) {
|
||||
var ruleType uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &ruleType)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch ruleType {
|
||||
case 0:
|
||||
rule.DefaultOptions, err = readDefaultRule(reader, recovery)
|
||||
case 1:
|
||||
rule.LogicalOptions, err = readLogicalRule(reader, recovery)
|
||||
default:
|
||||
err = E.New("unknown rule type: ", ruleType)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func writeRule(writer io.Writer, rule option.HeadlessRule) error {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
return writeDefaultRule(writer, rule.DefaultOptions)
|
||||
case C.RuleTypeLogical:
|
||||
return writeLogicalRule(writer, rule.LogicalOptions)
|
||||
default:
|
||||
panic("unknown rule type: " + rule.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) {
|
||||
var lastItemType uint8
|
||||
for {
|
||||
var itemType uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &itemType)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch itemType {
|
||||
case ruleItemQueryType:
|
||||
var rawQueryType []uint16
|
||||
rawQueryType, err = readRuleItemUint16(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType {
|
||||
return option.DNSQueryType(it)
|
||||
})
|
||||
case ruleItemNetwork:
|
||||
rule.Network, err = readRuleItemString(reader)
|
||||
case ruleItemDomain:
|
||||
var matcher *domain.Matcher
|
||||
matcher, err = domain.ReadMatcher(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rule.DomainMatcher = matcher
|
||||
case ruleItemDomainKeyword:
|
||||
rule.DomainKeyword, err = readRuleItemString(reader)
|
||||
case ruleItemDomainRegex:
|
||||
rule.DomainRegex, err = readRuleItemString(reader)
|
||||
case ruleItemSourceIPCIDR:
|
||||
rule.SourceIPSet, err = readIPSet(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if recovery {
|
||||
rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String)
|
||||
}
|
||||
case ruleItemIPCIDR:
|
||||
rule.IPSet, err = readIPSet(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if recovery {
|
||||
rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String)
|
||||
}
|
||||
case ruleItemSourcePort:
|
||||
rule.SourcePort, err = readRuleItemUint16(reader)
|
||||
case ruleItemSourcePortRange:
|
||||
rule.SourcePortRange, err = readRuleItemString(reader)
|
||||
case ruleItemPort:
|
||||
rule.Port, err = readRuleItemUint16(reader)
|
||||
case ruleItemPortRange:
|
||||
rule.PortRange, err = readRuleItemString(reader)
|
||||
case ruleItemProcessName:
|
||||
rule.ProcessName, err = readRuleItemString(reader)
|
||||
case ruleItemProcessPath:
|
||||
rule.ProcessPath, err = readRuleItemString(reader)
|
||||
case ruleItemPackageName:
|
||||
rule.PackageName, err = readRuleItemString(reader)
|
||||
case ruleItemWIFISSID:
|
||||
rule.WIFISSID, err = readRuleItemString(reader)
|
||||
case ruleItemWIFIBSSID:
|
||||
rule.WIFIBSSID, err = readRuleItemString(reader)
|
||||
case ruleItemFinal:
|
||||
err = binary.Read(reader, binary.BigEndian, &rule.Invert)
|
||||
return
|
||||
default:
|
||||
err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
lastItemType = itemType
|
||||
}
|
||||
}
|
||||
|
||||
func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
|
||||
err := binary.Write(writer, binary.BigEndian, uint8(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rule.QueryType) > 0 {
|
||||
err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 {
|
||||
return uint16(it)
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.Network) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemNetwork, rule.Network)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 {
|
||||
err = binary.Write(writer, binary.BigEndian, ruleItemDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.DomainKeyword) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.DomainRegex) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.SourceIPCIDR) > 0 {
|
||||
err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR)
|
||||
if err != nil {
|
||||
return E.Cause(err, "source_ipcidr")
|
||||
}
|
||||
}
|
||||
if len(rule.IPCIDR) > 0 {
|
||||
err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR)
|
||||
if err != nil {
|
||||
return E.Cause(err, "ipcidr")
|
||||
}
|
||||
}
|
||||
if len(rule.SourcePort) > 0 {
|
||||
err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.SourcePortRange) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.Port) > 0 {
|
||||
err = writeRuleItemUint16(writer, ruleItemPort, rule.Port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.PortRange) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.ProcessName) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.ProcessPath) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.PackageName) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.WIFISSID) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.WIFIBSSID) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, ruleItemFinal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, rule.Invert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readRuleItemString(reader io.Reader) ([]string, error) {
|
||||
length, err := rw.ReadUVariant(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value := make([]string, length)
|
||||
for i := uint64(0); i < length; i++ {
|
||||
value[i], err = rw.ReadVString(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error {
|
||||
err := binary.Write(writer, binary.BigEndian, itemType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rw.WriteUVariant(writer, uint64(len(value)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range value {
|
||||
err = rw.WriteVString(writer, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readRuleItemUint16(reader io.Reader) ([]uint16, error) {
|
||||
length, err := rw.ReadUVariant(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value := make([]uint16, length)
|
||||
for i := uint64(0); i < length; i++ {
|
||||
err = binary.Read(reader, binary.BigEndian, &value[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error {
|
||||
err := binary.Write(writer, binary.BigEndian, itemType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rw.WriteUVariant(writer, uint64(len(value)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range value {
|
||||
err = binary.Write(writer, binary.BigEndian, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error {
|
||||
var builder netipx.IPSetBuilder
|
||||
for i, prefixString := range value {
|
||||
prefix, err := netip.ParsePrefix(prefixString)
|
||||
if err == nil {
|
||||
builder.AddPrefix(prefix)
|
||||
continue
|
||||
}
|
||||
addr, addrErr := netip.ParseAddr(prefixString)
|
||||
if addrErr == nil {
|
||||
builder.Add(addr)
|
||||
continue
|
||||
}
|
||||
return E.Cause(err, "parse [", i, "]")
|
||||
}
|
||||
ipSet, err := builder.IPSet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, itemType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeIPSet(writer, ipSet)
|
||||
}
|
||||
|
||||
func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) {
|
||||
var mode uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &mode)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch mode {
|
||||
case 0:
|
||||
logicalRule.Mode = C.LogicalTypeAnd
|
||||
case 1:
|
||||
logicalRule.Mode = C.LogicalTypeOr
|
||||
default:
|
||||
err = E.New("unknown logical mode: ", mode)
|
||||
return
|
||||
}
|
||||
length, err := rw.ReadUVariant(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
logicalRule.Rules = make([]option.HeadlessRule, length)
|
||||
for i := uint64(0); i < length; i++ {
|
||||
logicalRule.Rules[i], err = readRule(reader, recovery)
|
||||
if err != nil {
|
||||
err = E.Cause(err, "read logical rule [", i, "]")
|
||||
return
|
||||
}
|
||||
}
|
||||
err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error {
|
||||
err := binary.Write(writer, binary.BigEndian, uint8(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch logicalRule.Mode {
|
||||
case C.LogicalTypeAnd:
|
||||
err = binary.Write(writer, binary.BigEndian, uint8(0))
|
||||
case C.LogicalTypeOr:
|
||||
err = binary.Write(writer, binary.BigEndian, uint8(1))
|
||||
default:
|
||||
panic("unknown logical mode: " + logicalRule.Mode)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rule := range logicalRule.Rules {
|
||||
err = writeRule(writer, rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, logicalRule.Invert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
116
common/srs/ip_set.go
Normal file
116
common/srs/ip_set.go
Normal file
@ -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
|
||||
}
|
31
common/taskmonitor/monitor.go
Normal file
31
common/taskmonitor/monitor.go
Normal file
@ -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()
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -9,3 +9,11 @@ const (
|
||||
LogicalTypeAnd = "and"
|
||||
LogicalTypeOr = "or"
|
||||
)
|
||||
|
||||
const (
|
||||
RuleSetTypeLocal = "local"
|
||||
RuleSetTypeRemote = "remote"
|
||||
RuleSetVersion1 = 1
|
||||
RuleSetFormatSource = "source"
|
||||
RuleSetFormatBinary = "binary"
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -6,12 +6,12 @@ import (
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
@ -2,16 +2,128 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
# ChangeLog
|
||||
#### 1.8.0-alpha.14
|
||||
|
||||
* 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
|
||||
@ -43,11 +155,13 @@ 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.
|
||||
Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound
|
||||
options.
|
||||
|
||||
**4**
|
||||
|
||||
Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server, see [TCP Brutal](/configuration/shared/tcp-brutal) for details.
|
||||
Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server,
|
||||
see [TCP Brutal](/configuration/shared/tcp-brutal) for details.
|
||||
|
||||
**5**:
|
||||
|
||||
@ -150,11 +264,13 @@ Only supported in graphical clients on Android and iOS.
|
||||
|
||||
**1**:
|
||||
|
||||
Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound options.
|
||||
Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound
|
||||
options.
|
||||
|
||||
**2**
|
||||
|
||||
Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server, see [TCP Brutal](/configuration/shared/tcp-brutal) for details.
|
||||
Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server,
|
||||
see [TCP Brutal](/configuration/shared/tcp-brutal) for details.
|
||||
|
||||
#### 1.7.0-alpha.3
|
||||
|
||||
@ -221,8 +337,8 @@ When `auto_route` is enabled and `strict_route` is disabled, the device can now
|
||||
|
||||
**2**:
|
||||
|
||||
Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave.
|
||||
|
||||
Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High
|
||||
Sierra, 10.14 Mojave.
|
||||
|
||||
#### 1.6.0-rc.4
|
||||
|
||||
@ -235,7 +351,8 @@ Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008
|
||||
|
||||
**1**:
|
||||
|
||||
Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave.
|
||||
Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High
|
||||
Sierra, 10.14 Mojave.
|
||||
|
||||
#### 1.6.0-beta.4
|
||||
|
||||
|
@ -1,3 +1,14 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.8.0"
|
||||
|
||||
:material-plus: [rule_set](#rule_set)
|
||||
:material-plus: [source_ip_is_private](#source_ip_is_private)
|
||||
:material-delete-clock: [geoip](#geoip)
|
||||
:material-delete-clock: [geosite](#geosite)
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
@ -46,6 +57,7 @@
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
],
|
||||
"source_ip_is_private": false,
|
||||
"source_port": [
|
||||
12345
|
||||
],
|
||||
@ -85,6 +97,10 @@
|
||||
"wifi_bssid": [
|
||||
"00:00:00:00:00:00"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
],
|
||||
"invert": false,
|
||||
"outbound": [
|
||||
"direct"
|
||||
@ -166,15 +182,29 @@ Match domain using regular expression.
|
||||
|
||||
#### geosite
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets).
|
||||
|
||||
Match geosite.
|
||||
|
||||
#### source_geoip
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets).
|
||||
|
||||
Match source geoip.
|
||||
|
||||
#### source_ip_cidr
|
||||
|
||||
Match source ip cidr.
|
||||
Match source IP CIDR.
|
||||
|
||||
#### source_ip_is_private
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
Match non-public source IP.
|
||||
|
||||
#### source_port
|
||||
|
||||
@ -250,6 +280,12 @@ Match WiFi SSID.
|
||||
|
||||
Match WiFi BSSID.
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
Match [Rule Set](/configuration/route/#rule_set).
|
||||
|
||||
#### invert
|
||||
|
||||
Invert match result.
|
||||
@ -286,4 +322,4 @@ Rewrite TTL in DNS responses.
|
||||
|
||||
#### rules
|
||||
|
||||
Included default rules.
|
||||
Included rules.
|
@ -1,3 +1,14 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.8.0 中的更改"
|
||||
|
||||
:material-plus: [rule_set](#rule_set)
|
||||
:material-plus: [source_ip_is_private](#source_ip_is_private)
|
||||
:material-delete-clock: [geoip](#geoip)
|
||||
:material-delete-clock: [geosite](#geosite)
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
@ -45,6 +56,7 @@
|
||||
"source_ip_cidr": [
|
||||
"10.0.0.0/24"
|
||||
],
|
||||
"source_ip_is_private": false,
|
||||
"source_port": [
|
||||
12345
|
||||
],
|
||||
@ -84,6 +96,10 @@
|
||||
"wifi_bssid": [
|
||||
"00:00:00:00:00:00"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
],
|
||||
"invert": false,
|
||||
"outbound": [
|
||||
"direct"
|
||||
@ -163,16 +179,30 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
#### geosite
|
||||
|
||||
匹配 GeoSite。
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/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 +275,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
匹配 WiFi BSSID。
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "自 sing-box 1.8.0 起"
|
||||
|
||||
匹配[规则集](/zh/configuration/route/#rule_set)。
|
||||
|
||||
#### invert
|
||||
|
||||
反选匹配结果。
|
||||
@ -281,4 +317,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
#### rules
|
||||
|
||||
包括的默认规则。
|
||||
包括的规则。
|
@ -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` |
|
||||
|
@ -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` | `无错误` |
|
||||
|
34
docs/configuration/experimental/cache-file.md
Normal file
34
docs/configuration/experimental/cache-file.md
Normal file
@ -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.
|
32
docs/configuration/experimental/cache-file.zh.md
Normal file
32
docs/configuration/experimental/cache-file.zh.md
Normal file
@ -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
|
||||
|
||||
缓存文件中的标识符。
|
||||
|
||||
如果不为空,配置特定的数据将使用由其键控的单独存储。
|
114
docs/configuration/experimental/clash-api.md
Normal file
114
docs/configuration/experimental/clash-api.md
Normal file
@ -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.
|
112
docs/configuration/experimental/clash-api.zh.md
Normal file
112
docs/configuration/experimental/clash-api.zh.md
Normal file
@ -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。
|
||||
|
||||
如果不为空,配置特定的数据将使用由其键控的单独存储。
|
@ -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 ""
|
||||
### Fields
|
||||
|
||||
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
|
||||
|
||||
User list to count traffic.
|
||||
| Key | Format |
|
||||
|--------------|----------------------------|
|
||||
| `cache_file` | [Cache File](./cache-file) |
|
||||
| `clash_api` | [Clash API](./clash-api) |
|
||||
| `v2ray_api` | [V2Ray API](./v2ray-api) |
|
@ -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
|
||||
|
||||
统计流量的用户列表。
|
||||
| 键 | 格式 |
|
||||
|--------------|--------------------------|
|
||||
| `cache_file` | [缓存文件](./cache-file) |
|
||||
| `clash_api` | [Clash API](./clash-api) |
|
||||
| `v2ray_api` | [V2Ray API](./v2ray-api) |
|
50
docs/configuration/experimental/v2ray-api.md
Normal file
50
docs/configuration/experimental/v2ray-api.md
Normal file
@ -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.
|
50
docs/configuration/experimental/v2ray-api.zh.md
Normal file
50
docs/configuration/experimental/v2ray-api.zh.md
Normal file
@ -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
|
||||
|
||||
统计流量的用户列表。
|
@ -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.
|
||||
|
@ -29,10 +29,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含被 Hysteria 依赖的 QUIC,参阅 [安装](/zh/#_2)。
|
||||
|
||||
### 监听字段
|
||||
|
||||
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||
|
@ -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**,
|
||||
|
@ -26,10 +26,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含被 Hysteria2 依赖的 QUIC,参阅 [安装](/zh/#_2)。
|
||||
|
||||
!!! warning "与官方 Hysteria2 的区别"
|
||||
|
||||
官方程序支持一种名为 **userpass** 的验证方式,
|
||||
|
@ -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.
|
||||
|
@ -18,10 +18,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含 HTTP3 传输层, 参阅 [安装](/zh/#_2)。
|
||||
|
||||
### 监听字段
|
||||
|
||||
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||
|
@ -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.
|
||||
|
@ -22,10 +22,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含被 TUI 依赖的 QUIC,参阅 [安装](/zh/#_2)。
|
||||
|
||||
### 监听字段
|
||||
|
||||
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||
|
@ -171,7 +171,7 @@ TCP/IP stack.
|
||||
|
||||
!!! warning ""
|
||||
|
||||
gVisor and LWIP stacks is not included by default, see [Installation](./#installation).
|
||||
LWIP stacks is not included by default, see [Installation](/installation/build-from-source/#build-tags).
|
||||
|
||||
#### include_interface
|
||||
|
||||
|
@ -167,7 +167,7 @@ TCP/IP 栈。
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含 gVisor 和 LWIP 栈,请参阅 [安装](/zh/#_2)。
|
||||
默认安装不包含 LWIP 栈,参阅 [安装](/zh/installation/build-from-source/#_5)。
|
||||
|
||||
#### include_interface
|
||||
|
||||
|
@ -24,10 +24,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning ""
|
||||
|
||||
QUIC, which is required by hysteria is not included by default, see [Installation](./#installation).
|
||||
|
||||
### Fields
|
||||
|
||||
#### server
|
||||
|
@ -24,10 +24,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含被 Hysteria 依赖的 QUIC,参阅 [安装](/zh/#_2)。
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
|
@ -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**,
|
||||
|
@ -22,10 +22,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含被 Hysteria2 依赖的 QUIC,参阅 [安装](/zh/#_2)。
|
||||
|
||||
!!! warning "与官方 Hysteria2 的区别"
|
||||
|
||||
官方程序支持一种名为 **userpass** 的验证方式,
|
||||
|
@ -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) |
|
||||
|
@ -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) |
|
||||
|
@ -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.
|
@ -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/)。
|
@ -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
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
!!! info ""
|
||||
|
||||
默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/#_2)。
|
||||
默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#_5)。
|
||||
|
||||
### 字段
|
||||
|
||||
|
@ -21,10 +21,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning ""
|
||||
|
||||
QUIC, which is required by TUIC is not included by default, see [Installation](./#installation).
|
||||
|
||||
### Fields
|
||||
|
||||
#### server
|
||||
|
@ -21,10 +21,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含被 TUI 依赖的 QUIC,参阅 [安装](/zh/#_2)。
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
当选定的出站发生更改时,中断现有连接。
|
||||
|
@ -36,14 +36,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
|
||||
|
@ -24,14 +24,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含 WireGuard, 参阅 [安装](/zh/#_2)。
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含被非特权 WireGuard 需要的 gVisor, 参阅 [安装](/zh/#_2)。
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
|
@ -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
|
||||
|
@ -1,3 +1,11 @@
|
||||
---
|
||||
icon: material/delete-clock
|
||||
---
|
||||
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
|
@ -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
|
||||
|
@ -1,3 +1,11 @@
|
||||
---
|
||||
icon: material/delete-clock
|
||||
---
|
||||
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,5 +1,15 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
# 路由
|
||||
|
||||
!!! quote "sing-box 1.8.0 中的更改"
|
||||
|
||||
:material-plus: [rule_set](#rule_set)
|
||||
:material-delete-clock: [geoip](#geoip)
|
||||
:material-delete-clock: [geosite](#geosite)
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
@ -7,8 +17,8 @@
|
||||
"route": {
|
||||
"geoip": {},
|
||||
"geosite": {},
|
||||
"ip_rules": [],
|
||||
"rules": [],
|
||||
"rule_set": [],
|
||||
"final": "",
|
||||
"auto_detect_interface": false,
|
||||
"override_android_vpn": false,
|
||||
@ -20,11 +30,21 @@
|
||||
|
||||
### 字段
|
||||
|
||||
| 键 | 格式 |
|
||||
|------------|-------------------------|
|
||||
| `geoip` | [GeoIP](./geoip) |
|
||||
| `geosite` | [GeoSite](./geosite) |
|
||||
| `rules` | 一组 [路由规则](./rule) |
|
||||
| 键 | 格式 |
|
||||
|------------|-----------------------------------|
|
||||
| `geoip` | [GeoIP](./geoip) |
|
||||
| `geosite` | [Geosite](./geosite) |
|
||||
|
||||
|
||||
#### rule
|
||||
|
||||
一组 [路由规则](./rule)。
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "自 sing-box 1.8.0 起"
|
||||
|
||||
一组 [规则集](/configuration/rule-set)。
|
||||
|
||||
#### final
|
||||
|
||||
|
@ -1,3 +1,17 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.8.0"
|
||||
|
||||
:material-plus: [rule_set](#rule_set)
|
||||
:material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
|
||||
:material-plus: [source_ip_is_private](#source_ip_is_private)
|
||||
:material-plus: [ip_is_private](#ip_is_private)
|
||||
:material-delete-clock: [source_geoip](#source_geoip)
|
||||
:material-delete-clock: [geoip](#geoip)
|
||||
:material-delete-clock: [geosite](#geosite)
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
@ -46,10 +60,12 @@
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
],
|
||||
"source_ip_is_private": false,
|
||||
"ip_cidr": [
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
],
|
||||
"ip_is_private": false,
|
||||
"source_port": [
|
||||
12345
|
||||
],
|
||||
@ -89,6 +105,10 @@
|
||||
"wifi_bssid": [
|
||||
"00:00:00:00:00:00"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
],
|
||||
"invert": false,
|
||||
"outbound": "direct"
|
||||
},
|
||||
@ -160,23 +180,47 @@ Match domain using regular expression.
|
||||
|
||||
#### geosite
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets).
|
||||
|
||||
Match geosite.
|
||||
|
||||
#### source_geoip
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets).
|
||||
|
||||
Match source geoip.
|
||||
|
||||
#### geoip
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.8.0"
|
||||
|
||||
GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets).
|
||||
|
||||
Match geoip.
|
||||
|
||||
#### source_ip_cidr
|
||||
|
||||
Match source ip cidr.
|
||||
Match source IP CIDR.
|
||||
|
||||
#### ip_is_private
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
Match non-public IP.
|
||||
|
||||
#### ip_cidr
|
||||
|
||||
Match ip cidr.
|
||||
Match IP CIDR.
|
||||
|
||||
#### source_ip_is_private
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
Match non-public source IP.
|
||||
|
||||
#### source_port
|
||||
|
||||
@ -250,6 +294,18 @@ Match WiFi SSID.
|
||||
|
||||
Match WiFi BSSID.
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
Match [Rule Set](/configuration/route/#rule_set).
|
||||
|
||||
#### rule_set_ipcidr_match_source
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
Make `ipcidr` in rule sets match the source IP.
|
||||
|
||||
#### invert
|
||||
|
||||
Invert match result.
|
||||
@ -276,4 +332,4 @@ Tag of the target outbound.
|
||||
|
||||
==Required==
|
||||
|
||||
Included default rules.
|
||||
Included rules.
|
||||
|
@ -1,3 +1,17 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.8.0 中的更改"
|
||||
|
||||
:material-plus: [rule_set](#rule_set)
|
||||
:material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
|
||||
:material-plus: [source_ip_is_private](#source_ip_is_private)
|
||||
:material-plus: [ip_is_private](#ip_is_private)
|
||||
:material-delete-clock: [source_geoip](#source_geoip)
|
||||
:material-delete-clock: [geoip](#geoip)
|
||||
:material-delete-clock: [geosite](#geosite)
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
@ -45,9 +59,11 @@
|
||||
"source_ip_cidr": [
|
||||
"10.0.0.0/24"
|
||||
],
|
||||
"source_ip_is_private": false,
|
||||
"ip_cidr": [
|
||||
"10.0.0.0/24"
|
||||
],
|
||||
"ip_is_private": false,
|
||||
"source_port": [
|
||||
12345
|
||||
],
|
||||
@ -87,6 +103,10 @@
|
||||
"wifi_bssid": [
|
||||
"00:00:00:00:00:00"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
],
|
||||
"invert": false,
|
||||
"outbound": "direct"
|
||||
},
|
||||
@ -158,24 +178,48 @@
|
||||
|
||||
#### geosite
|
||||
|
||||
匹配 GeoSite。
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/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 +292,18 @@
|
||||
|
||||
匹配 WiFi BSSID。
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "自 sing-box 1.8.0 起"
|
||||
|
||||
匹配[规则集](/zh/configuration/route/#rule_set)。
|
||||
|
||||
#### rule_set_ipcidr_match_source
|
||||
|
||||
!!! question "自 sing-box 1.8.0 起"
|
||||
|
||||
使规则集中的 `ipcidr` 规则匹配源 IP。
|
||||
|
||||
#### invert
|
||||
|
||||
反选匹配结果。
|
||||
@ -274,4 +330,4 @@
|
||||
|
||||
==必填==
|
||||
|
||||
包括的默认规则。
|
||||
包括的规则。
|
207
docs/configuration/rule-set/headless-rule.md
Normal file
207
docs/configuration/rule-set/headless-rule.md
Normal file
@ -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.
|
97
docs/configuration/rule-set/index.md
Normal file
97
docs/configuration/rule-set/index.md
Normal file
@ -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.
|
34
docs/configuration/rule-set/source-format.md
Normal file
34
docs/configuration/rule-set/source-format.md
Normal file
@ -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 <file-name>.srs] <file-name>.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).
|
@ -199,10 +199,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.
|
||||
@ -226,10 +222,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.
|
||||
|
||||
@ -278,10 +270,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.
|
||||
@ -357,14 +345,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==
|
||||
|
@ -193,10 +193,6 @@ TLS 版本值:
|
||||
|
||||
==仅客户端==
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含 uTLS, 参阅 [安装](/zh/#_2)。
|
||||
|
||||
!!! note ""
|
||||
|
||||
uTLS 维护不善且其效果可能未经证实,使用风险自负。
|
||||
@ -220,14 +216,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
|
||||
@ -273,10 +264,6 @@ ECH PEM 配置路径
|
||||
|
||||
### ACME 字段
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含 ACME,参阅 [安装](/zh/#_2)。
|
||||
|
||||
#### domain
|
||||
|
||||
一组域名。
|
||||
@ -348,14 +335,6 @@ ACME DNS01 验证字段。如果配置,将禁用其他验证方法。
|
||||
|
||||
### Reality 字段
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含 reality 服务器,参阅 [安装](/zh/#_2)。
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含被 reality 客户端需要的 uTLS, 参阅 [安装](/zh/#_2)。
|
||||
|
||||
#### handshake
|
||||
|
||||
==仅服务器==
|
||||
|
@ -129,10 +129,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:
|
||||
@ -142,7 +138,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
|
||||
{
|
||||
|
@ -128,10 +128,6 @@ HTTP 请求的额外标头。
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning ""
|
||||
|
||||
默认安装不包含 QUIC, 参阅 [安装](/zh/#_2)。
|
||||
|
||||
!!! warning "与 v2ray-core 的区别"
|
||||
|
||||
没有额外的加密支持:
|
||||
@ -141,7 +137,7 @@ HTTP 请求的额外标头。
|
||||
|
||||
!!! note ""
|
||||
|
||||
默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/#_2)。
|
||||
默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/installation/build-from-source/#_5)。
|
||||
|
||||
```json
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user