mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-22 08:31:30 +00:00
Add multiple configuration support
This commit is contained in:
parent
e0d9f79445
commit
c7f89ad88e
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,3 +12,4 @@
|
||||||
/*.aar
|
/*.aar
|
||||||
/*.xcframework/
|
/*.xcframework/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
/config.d/
|
||||||
|
|
|
@ -26,7 +26,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func check() error {
|
func check() error {
|
||||||
options, err := readConfig()
|
options, err := readConfigAndMerge()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,44 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func format() error {
|
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)
|
configContent, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "read config")
|
return E.Cause(err, "read config")
|
||||||
|
|
|
@ -5,10 +5,14 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
runtimeDebug "runtime/debug"
|
runtimeDebug "runtime/debug"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box"
|
"github.com/sagernet/sing-box"
|
||||||
|
"github.com/sagernet/sing-box/common/badjsonmerge"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
@ -31,29 +35,85 @@ func init() {
|
||||||
mainCommand.AddCommand(commandRun)
|
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 (
|
var (
|
||||||
configContent []byte
|
configContent []byte
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if configPath == "stdin" {
|
if path == "stdin" {
|
||||||
configContent, err = io.ReadAll(os.Stdin)
|
configContent, err = io.ReadAll(os.Stdin)
|
||||||
} else {
|
} else {
|
||||||
configContent, err = os.ReadFile(configPath)
|
configContent, err = os.ReadFile(path)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return option.Options{}, E.Cause(err, "read config")
|
return nil, E.Cause(err, "read config at ", path)
|
||||||
}
|
}
|
||||||
var options option.Options
|
var options option.Options
|
||||||
err = options.UnmarshalJSON(configContent)
|
err = options.UnmarshalJSON(configContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return option.Options{}, E.Cause(err, "decode config")
|
return nil, E.Cause(err, "decode config at ", path)
|
||||||
}
|
}
|
||||||
return options, nil
|
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 mergedOptions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func create() (*box.Box, context.CancelFunc, error) {
|
func create() (*box.Box, context.CancelFunc, error) {
|
||||||
options, err := readConfig()
|
options, err := readConfigAndMerge()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
configPath string
|
configPaths []string
|
||||||
workingDir string
|
configDirectories []string
|
||||||
disableColor bool
|
workingDir string
|
||||||
|
disableColor bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var mainCommand = &cobra.Command{
|
var mainCommand = &cobra.Command{
|
||||||
|
@ -22,7 +23,8 @@ var mainCommand = &cobra.Command{
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
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().StringVarP(&workingDir, "directory", "D", "", "set working directory")
|
||||||
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
|
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
|
||||||
}
|
}
|
||||||
|
@ -42,4 +44,7 @@ func preRun(cmd *cobra.Command, args []string) {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(configPaths) == 0 && len(configDirectories) == 0 {
|
||||||
|
configPaths = append(configPaths, "config.json")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
80
common/badjsonmerge/merge.go
Normal file
80
common/badjsonmerge/merge.go
Normal file
|
@ -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
common/badjsonmerge/merge_test.go
Normal file
59
common/badjsonmerge/merge_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -25,7 +25,7 @@ require (
|
||||||
github.com/sagernet/gomobile v0.0.0-20221130124640-349ebaa752ca
|
github.com/sagernet/gomobile v0.0.0-20221130124640-349ebaa752ca
|
||||||
github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32
|
github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32
|
||||||
github.com/sagernet/reality v0.0.0-20230312150606-35ea9af0e0b8
|
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-dns v0.1.4
|
||||||
github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9
|
github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9
|
||||||
github.com/sagernet/sing-shadowtls v0.1.0
|
github.com/sagernet/sing-shadowtls v0.1.0
|
||||||
|
|
4
go.sum
4
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/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.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.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.20230318094614-4bbf5f2c3046 h1:/+ZWbxRvQmco9ES2qT5Eh/x/IiQRjAcUyRG/vQ4dpxc=
|
||||||
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/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 h1:7VxgeoSCiiazDSaXXQVcvrTBxFpOePPq/4XdgnUDN+0=
|
||||||
github.com/sagernet/sing-dns v0.1.4/go.mod h1:1+6pCa48B1AI78lD+/i/dLgpw4MwfnsSpZo0Ds8wzzk=
|
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=
|
github.com/sagernet/sing-shadowsocks v0.1.2-0.20230221080503-769c01d6bba9 h1:qS39eA4C7x+zhEkySbASrtmb6ebdy5v0y2M6mgkmSO0=
|
||||||
|
|
|
@ -169,6 +169,9 @@ func NewRouter(
|
||||||
} else {
|
} else {
|
||||||
tag = F.ToString(i)
|
tag = F.ToString(i)
|
||||||
}
|
}
|
||||||
|
if transportTagMap[tag] {
|
||||||
|
return nil, E.New("duplicate dns server tag: ", tag)
|
||||||
|
}
|
||||||
transportTags[i] = tag
|
transportTags[i] = tag
|
||||||
transportTagMap[tag] = true
|
transportTagMap[tag] = true
|
||||||
}
|
}
|
||||||
|
@ -241,6 +244,9 @@ func NewRouter(
|
||||||
}), func(index int, server option.DNSServerOptions) string {
|
}), func(index int, server option.DNSServerOptions) string {
|
||||||
return transportTags[index]
|
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, " "))
|
return nil, E.New("found circular reference in dns servers: ", strings.Join(unresolvedTags, " "))
|
||||||
}
|
}
|
||||||
var defaultTransport dns.Transport
|
var defaultTransport dns.Transport
|
||||||
|
|
Loading…
Reference in a new issue