Răsfoiți Sursa

Add merge command

世界 2 ani în urmă
părinte
comite
e7b7ae811f

+ 167 - 0
cmd/sing-box/cmd_merge.go

@@ -0,0 +1,167 @@
+package main
+
+import (
+	"bytes"
+	"os"
+	"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"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/rw"
+
+	"github.com/spf13/cobra"
+)
+
+var commandMerge = &cobra.Command{
+	Use:   "merge [output]",
+	Short: "Merge configurations",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := merge(args[0])
+		if err != nil {
+			log.Fatal(err)
+		}
+	},
+	Args: cobra.ExactArgs(1),
+}
+
+func init() {
+	mainCommand.AddCommand(commandMerge)
+}
+
+func merge(outputPath string) error {
+	mergedOptions, err := readConfigAndMerge()
+	if err != nil {
+		return err
+	}
+	err = mergePathResources(&mergedOptions)
+	if err != nil {
+		return err
+	}
+	buffer := new(bytes.Buffer)
+	encoder := json.NewEncoder(buffer)
+	encoder.SetIndent("", "  ")
+	err = encoder.Encode(mergedOptions)
+	if err != nil {
+		return E.Cause(err, "encode config")
+	}
+	if existsContent, err := os.ReadFile(outputPath); err != nil {
+		if string(existsContent) == buffer.String() {
+			return nil
+		}
+	}
+	err = rw.WriteFile(outputPath, buffer.Bytes())
+	if err != nil {
+		return err
+	}
+	outputPath, _ = filepath.Abs(outputPath)
+	os.Stderr.WriteString(outputPath + "\n")
+	return nil
+}
+
+func mergePathResources(options *option.Options) error {
+	for index, inbound := range options.Inbounds {
+		switch inbound.Type {
+		case C.TypeHTTP:
+			inbound.HTTPOptions.TLS = mergeTLSInboundOptions(inbound.HTTPOptions.TLS)
+		case C.TypeMixed:
+			inbound.MixedOptions.TLS = mergeTLSInboundOptions(inbound.MixedOptions.TLS)
+		case C.TypeVMess:
+			inbound.VMessOptions.TLS = mergeTLSInboundOptions(inbound.VMessOptions.TLS)
+		case C.TypeTrojan:
+			inbound.TrojanOptions.TLS = mergeTLSInboundOptions(inbound.TrojanOptions.TLS)
+		case C.TypeNaive:
+			inbound.NaiveOptions.TLS = mergeTLSInboundOptions(inbound.NaiveOptions.TLS)
+		case C.TypeHysteria:
+			inbound.HysteriaOptions.TLS = mergeTLSInboundOptions(inbound.HysteriaOptions.TLS)
+		case C.TypeVLESS:
+			inbound.VLESSOptions.TLS = mergeTLSInboundOptions(inbound.VLESSOptions.TLS)
+		case C.TypeTUIC:
+			inbound.TUICOptions.TLS = mergeTLSInboundOptions(inbound.TUICOptions.TLS)
+		case C.TypeHysteria2:
+			inbound.Hysteria2Options.TLS = mergeTLSInboundOptions(inbound.Hysteria2Options.TLS)
+		default:
+			continue
+		}
+		options.Inbounds[index] = inbound
+	}
+	for index, outbound := range options.Outbounds {
+		switch outbound.Type {
+		case C.TypeHTTP:
+			outbound.HTTPOptions.TLS = mergeTLSOutboundOptions(outbound.HTTPOptions.TLS)
+		case C.TypeVMess:
+			outbound.VMessOptions.TLS = mergeTLSOutboundOptions(outbound.VMessOptions.TLS)
+		case C.TypeTrojan:
+			outbound.TrojanOptions.TLS = mergeTLSOutboundOptions(outbound.TrojanOptions.TLS)
+		case C.TypeHysteria:
+			outbound.HysteriaOptions.TLS = mergeTLSOutboundOptions(outbound.HysteriaOptions.TLS)
+		case C.TypeSSH:
+			outbound.SSHOptions = mergeSSHOutboundOptions(outbound.SSHOptions)
+		case C.TypeVLESS:
+			outbound.VLESSOptions.TLS = mergeTLSOutboundOptions(outbound.VLESSOptions.TLS)
+		case C.TypeTUIC:
+			outbound.TUICOptions.TLS = mergeTLSOutboundOptions(outbound.TUICOptions.TLS)
+		case C.TypeHysteria2:
+			outbound.Hysteria2Options.TLS = mergeTLSOutboundOptions(outbound.Hysteria2Options.TLS)
+		default:
+			continue
+		}
+		options.Outbounds[index] = outbound
+	}
+	return nil
+}
+
+func mergeTLSInboundOptions(options *option.InboundTLSOptions) *option.InboundTLSOptions {
+	if options == nil {
+		return nil
+	}
+	if options.CertificatePath != "" {
+		if content, err := os.ReadFile(options.CertificatePath); err == nil {
+			options.Certificate = strings.Split(string(content), "\n")
+		}
+	}
+	if options.KeyPath != "" {
+		if content, err := os.ReadFile(options.KeyPath); err == nil {
+			options.Key = strings.Split(string(content), "\n")
+		}
+	}
+	if options.ECH != nil {
+		if options.ECH.KeyPath != "" {
+			if content, err := os.ReadFile(options.ECH.KeyPath); err == nil {
+				options.ECH.Key = strings.Split(string(content), "\n")
+			}
+		}
+	}
+	return options
+}
+
+func mergeTLSOutboundOptions(options *option.OutboundTLSOptions) *option.OutboundTLSOptions {
+	if options == nil {
+		return nil
+	}
+	if options.CertificatePath != "" {
+		if content, err := os.ReadFile(options.CertificatePath); err == nil {
+			options.Certificate = strings.Split(string(content), "\n")
+		}
+	}
+	if options.ECH != nil {
+		if options.ECH.ConfigPath != "" {
+			if content, err := os.ReadFile(options.ECH.ConfigPath); err == nil {
+				options.ECH.Config = strings.Split(string(content), "\n")
+			}
+		}
+	}
+	return options
+}
+
+func mergeSSHOutboundOptions(options option.SSHOutboundOptions) option.SSHOutboundOptions {
+	if options.PrivateKeyPath != "" {
+		if content, err := os.ReadFile(options.PrivateKeyPath); err == nil {
+			options.PrivateKey = strings.Split(string(content), "\n")
+		}
+	}
+	return options
+}

+ 2 - 2
common/tls/ech_client.go

@@ -149,8 +149,8 @@ func NewECHClient(ctx context.Context, serverAddress string, options option.Outb
 		}
 	}
 	var certificate []byte
-	if options.Certificate != "" {
-		certificate = []byte(options.Certificate)
+	if len(options.Certificate) > 0 {
+		certificate = []byte(strings.Join(options.Certificate, "\n"))
 	} else if options.CertificatePath != "" {
 		content, err := os.ReadFile(options.CertificatePath)
 		if err != nil {

+ 3 - 2
common/tls/std_client.go

@@ -7,6 +7,7 @@ import (
 	"net"
 	"net/netip"
 	"os"
+	"strings"
 
 	"github.com/sagernet/sing-box/option"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -111,8 +112,8 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
 		}
 	}
 	var certificate []byte
-	if options.Certificate != "" {
-		certificate = []byte(options.Certificate)
+	if len(options.Certificate) > 0 {
+		certificate = []byte(strings.Join(options.Certificate, "\n"))
 	} else if options.CertificatePath != "" {
 		content, err := os.ReadFile(options.CertificatePath)
 		if err != nil {

+ 3 - 2
common/tls/utls_client.go

@@ -10,6 +10,7 @@ import (
 	"net"
 	"net/netip"
 	"os"
+	"strings"
 
 	"github.com/sagernet/sing-box/option"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -168,8 +169,8 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
 		}
 	}
 	var certificate []byte
-	if options.Certificate != "" {
-		certificate = []byte(options.Certificate)
+	if len(options.Certificate) > 0 {
+		certificate = []byte(strings.Join(options.Certificate, "\n"))
 	} else if options.CertificatePath != "" {
 		content, err := os.ReadFile(options.CertificatePath)
 		if err != nil {

+ 8 - 2
docs/configuration/index.md

@@ -31,11 +31,17 @@ sing-box uses JSON for configuration files.
 ### Check
 
 ```bash
-$ sing-box check
+sing-box check
 ```
 
 ### Format
 
 ```bash
-$ sing-box format -w
+sing-box format -w -c config.json -D config_directory
+```
+
+### Merge
+
+```bash
+sing-box merge output.json -c config.json -D config_directory
 ```

+ 8 - 2
docs/configuration/index.zh.md

@@ -29,11 +29,17 @@ sing-box 使用 JSON 作为配置文件格式。
 ### 检查
 
 ```bash
-$ sing-box check
+sing-box check
 ```
 
 ### 格式化
 
 ```bash
-$ sing-box format -w
+sing-box format -w -c config.json -D config_directory
+```
+
+### 合并
+
+```bash
+sing-box merge output.json -c config.json -D config_directory
 ```

+ 1 - 1
option/ssh.go

@@ -5,7 +5,7 @@ type SSHOutboundOptions struct {
 	ServerOptions
 	User                 string           `json:"user,omitempty"`
 	Password             string           `json:"password,omitempty"`
-	PrivateKey           string           `json:"private_key,omitempty"`
+	PrivateKey           Listable[string] `json:"private_key,omitempty"`
 	PrivateKeyPath       string           `json:"private_key_path,omitempty"`
 	PrivateKeyPassphrase string           `json:"private_key_passphrase,omitempty"`
 	HostKey              Listable[string] `json:"host_key,omitempty"`

+ 1 - 1
option/tls.go

@@ -26,7 +26,7 @@ type OutboundTLSOptions struct {
 	MinVersion      string                  `json:"min_version,omitempty"`
 	MaxVersion      string                  `json:"max_version,omitempty"`
 	CipherSuites    Listable[string]        `json:"cipher_suites,omitempty"`
-	Certificate     string                  `json:"certificate,omitempty"`
+	Certificate     Listable[string]        `json:"certificate,omitempty"`
 	CertificatePath string                  `json:"certificate_path,omitempty"`
 	ECH             *OutboundECHOptions     `json:"ech,omitempty"`
 	UTLS            *OutboundUTLSOptions    `json:"utls,omitempty"`

+ 4 - 3
outbound/ssh.go

@@ -8,6 +8,7 @@ import (
 	"net"
 	"os"
 	"strconv"
+	"strings"
 	"sync"
 
 	"github.com/sagernet/sing-box/adapter"
@@ -76,10 +77,10 @@ func NewSSH(ctx context.Context, router adapter.Router, logger log.ContextLogger
 	if options.Password != "" {
 		outbound.authMethod = append(outbound.authMethod, ssh.Password(options.Password))
 	}
-	if options.PrivateKey != "" || options.PrivateKeyPath != "" {
+	if len(options.PrivateKey) > 0 || options.PrivateKeyPath != "" {
 		var privateKey []byte
-		if options.PrivateKey != "" {
-			privateKey = []byte(options.PrivateKey)
+		if len(options.PrivateKey) > 0 {
+			privateKey = []byte(strings.Join(options.PrivateKey, "\n"))
 		} else {
 			var err error
 			privateKey, err = os.ReadFile(os.ExpandEnv(options.PrivateKeyPath))

+ 1 - 1
transport/sip003/v2ray.go

@@ -32,7 +32,7 @@ func newV2RayPlugin(ctx context.Context, pluginOpts Args, router adapter.Router,
 		certHead := "-----BEGIN CERTIFICATE-----"
 		certTail := "-----END CERTIFICATE-----"
 		fixedCert := certHead + "\n" + certRaw + "\n" + certTail
-		tlsOptions.Certificate = fixedCert
+		tlsOptions.Certificate = []string{fixedCert}
 	}
 
 	mode := "websocket"