Add merge command

This commit is contained in:
世界 2023-09-19 19:59:07 +08:00
parent a9743b77f6
commit e7b7ae811f
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
10 changed files with 198 additions and 16 deletions

167
cmd/sing-box/cmd_merge.go Normal file
View File

@ -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
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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
```

View File

@ -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
```

View File

@ -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"`

View File

@ -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"`

View File

@ -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))

View File

@ -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"