Parcourir la source

Add multiple configuration support

世界 il y a 2 ans
Parent
commit
c7f89ad88e

+ 1 - 0
.gitignore

@@ -12,3 +12,4 @@
 /*.aar
 /*.xcframework/
 .DS_Store
+/config.d/

+ 1 - 1
cmd/sing-box/cmd_check.go

@@ -26,7 +26,7 @@ func init() {
 }
 
 func check() error {
-	options, err := readConfig()
+	options, err := readConfigAndMerge()
 	if err != nil {
 		return err
 	}

+ 38 - 0
cmd/sing-box/cmd_format.go

@@ -33,6 +33,44 @@ func init() {
 }
 
 func format() error {
+	optionsList, err := readConfig()
+	if err != nil {
+		return err
+	}
+	for _, optionsEntry := range optionsList {
+		buffer := new(bytes.Buffer)
+		encoder := json.NewEncoder(buffer)
+		encoder.SetIndent("", "  ")
+		err = encoder.Encode(optionsEntry.options)
+		if err != nil {
+			return E.Cause(err, "encode config")
+		}
+		outputPath, _ := filepath.Abs(optionsEntry.path)
+		if !commandFormatFlagWrite {
+			if len(optionsList) > 1 {
+				os.Stdout.WriteString(outputPath + "\n")
+			}
+			os.Stdout.WriteString(buffer.String() + "\n")
+			continue
+		}
+		if bytes.Equal(optionsEntry.content, buffer.Bytes()) {
+			continue
+		}
+		output, err := os.Create(optionsEntry.path)
+		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
+}
+
+func formatOne(configPath string) error {
 	configContent, err := os.ReadFile(configPath)
 	if err != nil {
 		return E.Cause(err, "read config")

+ 67 - 7
cmd/sing-box/cmd_run.go

@@ -5,10 +5,14 @@ import (
 	"io"
 	"os"
 	"os/signal"
+	"path/filepath"
 	runtimeDebug "runtime/debug"
+	"sort"
+	"strings"
 	"syscall"
 
 	"github.com/sagernet/sing-box"
+	"github.com/sagernet/sing-box/common/badjsonmerge"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -31,29 +35,85 @@ func init() {
 	mainCommand.AddCommand(commandRun)
 }
 
-func readConfig() (option.Options, error) {
+type OptionsEntry struct {
+	content []byte
+	path    string
+	options option.Options
+}
+
+func readConfigAt(path string) (*OptionsEntry, error) {
 	var (
 		configContent []byte
 		err           error
 	)
-	if configPath == "stdin" {
+	if path == "stdin" {
 		configContent, err = io.ReadAll(os.Stdin)
 	} else {
-		configContent, err = os.ReadFile(configPath)
+		configContent, err = os.ReadFile(path)
 	}
 	if err != nil {
-		return option.Options{}, E.Cause(err, "read config")
+		return nil, E.Cause(err, "read config at ", path)
 	}
 	var options option.Options
 	err = options.UnmarshalJSON(configContent)
 	if err != nil {
-		return option.Options{}, E.Cause(err, "decode config")
+		return nil, E.Cause(err, "decode config at ", path)
+	}
+	return &OptionsEntry{
+		content: configContent,
+		path:    path,
+		options: options,
+	}, nil
+}
+
+func readConfig() ([]*OptionsEntry, error) {
+	var optionsList []*OptionsEntry
+	for _, path := range configPaths {
+		optionsEntry, err := readConfigAt(path)
+		if err != nil {
+			return nil, err
+		}
+		optionsList = append(optionsList, optionsEntry)
+	}
+	for _, directory := range configDirectories {
+		entries, err := os.ReadDir(directory)
+		if err != nil {
+			return nil, E.Cause(err, "read config directory at ", directory)
+		}
+		for _, entry := range entries {
+			if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() {
+				continue
+			}
+			optionsEntry, err := readConfigAt(filepath.Join(directory, entry.Name()))
+			if err != nil {
+				return nil, err
+			}
+			optionsList = append(optionsList, optionsEntry)
+		}
+	}
+	sort.Slice(optionsList, func(i, j int) bool {
+		return optionsList[i].path < optionsList[j].path
+	})
+	return optionsList, nil
+}
+
+func readConfigAndMerge() (option.Options, error) {
+	optionsList, err := readConfig()
+	if err != nil {
+		return option.Options{}, err
+	}
+	var mergedOptions option.Options
+	for _, options := range optionsList {
+		mergedOptions, err = badjsonmerge.MergeOptions(options.options, mergedOptions)
+		if err != nil {
+			return option.Options{}, E.Cause(err, "merge config at ", options.path)
+		}
 	}
-	return options, nil
+	return mergedOptions, nil
 }
 
 func create() (*box.Box, context.CancelFunc, error) {
-	options, err := readConfig()
+	options, err := readConfigAndMerge()
 	if err != nil {
 		return nil, nil, err
 	}

+ 9 - 4
cmd/sing-box/main.go

@@ -11,9 +11,10 @@ import (
 )
 
 var (
-	configPath   string
-	workingDir   string
-	disableColor bool
+	configPaths       []string
+	configDirectories []string
+	workingDir        string
+	disableColor      bool
 )
 
 var mainCommand = &cobra.Command{
@@ -22,7 +23,8 @@ var mainCommand = &cobra.Command{
 }
 
 func init() {
-	mainCommand.PersistentFlags().StringVarP(&configPath, "config", "c", "config.json", "set configuration file path")
+	mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", nil, "set configuration file path")
+	mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
 	mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
 	mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
 }
@@ -42,4 +44,7 @@ func preRun(cmd *cobra.Command, args []string) {
 			log.Fatal(err)
 		}
 	}
+	if len(configPaths) == 0 && len(configDirectories) == 0 {
+		configPaths = append(configPaths, "config.json")
+	}
 }

+ 80 - 0
common/badjsonmerge/merge.go

@@ -0,0 +1,80 @@
+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
+	}
+}

+ 59 - 0
common/badjsonmerge/merge_test.go

@@ -0,0 +1,59 @@
+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:  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:  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 - 1
go.mod

@@ -25,7 +25,7 @@ require (
 	github.com/sagernet/gomobile v0.0.0-20221130124640-349ebaa752ca
 	github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32
 	github.com/sagernet/reality v0.0.0-20230312150606-35ea9af0e0b8
-	github.com/sagernet/sing v0.2.1-0.20230318083058-18cd006d266e
+	github.com/sagernet/sing v0.2.1-0.20230318094614-4bbf5f2c3046
 	github.com/sagernet/sing-dns v0.1.4
 	github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9
 	github.com/sagernet/sing-shadowtls v0.1.0

+ 2 - 2
go.sum

@@ -111,8 +111,8 @@ github.com/sagernet/reality v0.0.0-20230312150606-35ea9af0e0b8 h1:4M3+0/kqvJuTsi
 github.com/sagernet/reality v0.0.0-20230312150606-35ea9af0e0b8/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
 github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
 github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
-github.com/sagernet/sing v0.2.1-0.20230318083058-18cd006d266e h1:KDaZ0GIlpdhCVn2vf7YL2r/8E5kSZiMMeMgn5CF7eJU=
-github.com/sagernet/sing v0.2.1-0.20230318083058-18cd006d266e/go.mod h1:9uHswk2hITw8leDbiLS/xn0t9nzBcbePxzm9PJhwdlw=
+github.com/sagernet/sing v0.2.1-0.20230318094614-4bbf5f2c3046 h1:/+ZWbxRvQmco9ES2qT5Eh/x/IiQRjAcUyRG/vQ4dpxc=
+github.com/sagernet/sing v0.2.1-0.20230318094614-4bbf5f2c3046/go.mod h1:9uHswk2hITw8leDbiLS/xn0t9nzBcbePxzm9PJhwdlw=
 github.com/sagernet/sing-dns v0.1.4 h1:7VxgeoSCiiazDSaXXQVcvrTBxFpOePPq/4XdgnUDN+0=
 github.com/sagernet/sing-dns v0.1.4/go.mod h1:1+6pCa48B1AI78lD+/i/dLgpw4MwfnsSpZo0Ds8wzzk=
 github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9 h1:qS39eA4C7x+zhEkySbASrtmb6ebdy5v0y2M6mgkmSO0=

+ 6 - 0
route/router.go

@@ -169,6 +169,9 @@ func NewRouter(
 		} else {
 			tag = F.ToString(i)
 		}
+		if transportTagMap[tag] {
+			return nil, E.New("duplicate dns server tag: ", tag)
+		}
 		transportTags[i] = tag
 		transportTagMap[tag] = true
 	}
@@ -241,6 +244,9 @@ func NewRouter(
 		}), func(index int, server option.DNSServerOptions) string {
 			return transportTags[index]
 		})
+		if len(unresolvedTags) == 0 {
+			panic(F.ToString("unexpected unresolved dns servers: ", len(transports), " ", len(dummyTransportMap), " ", len(transportMap)))
+		}
 		return nil, E.New("found circular reference in dns servers: ", strings.Join(unresolvedTags, " "))
 	}
 	var defaultTransport dns.Transport