mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-22 00:21:30 +00:00
Add merge command
This commit is contained in:
parent
a9743b77f6
commit
e7b7ae811f
167
cmd/sing-box/cmd_merge.go
Normal file
167
cmd/sing-box/cmd_merge.go
Normal 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
|
||||||
|
}
|
|
@ -149,8 +149,8 @@ func NewECHClient(ctx context.Context, serverAddress string, options option.Outb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var certificate []byte
|
var certificate []byte
|
||||||
if options.Certificate != "" {
|
if len(options.Certificate) > 0 {
|
||||||
certificate = []byte(options.Certificate)
|
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
||||||
} else if options.CertificatePath != "" {
|
} else if options.CertificatePath != "" {
|
||||||
content, err := os.ReadFile(options.CertificatePath)
|
content, err := os.ReadFile(options.CertificatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"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"
|
||||||
|
@ -111,8 +112,8 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var certificate []byte
|
var certificate []byte
|
||||||
if options.Certificate != "" {
|
if len(options.Certificate) > 0 {
|
||||||
certificate = []byte(options.Certificate)
|
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
||||||
} else if options.CertificatePath != "" {
|
} else if options.CertificatePath != "" {
|
||||||
content, err := os.ReadFile(options.CertificatePath)
|
content, err := os.ReadFile(options.CertificatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"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"
|
||||||
|
@ -168,8 +169,8 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var certificate []byte
|
var certificate []byte
|
||||||
if options.Certificate != "" {
|
if len(options.Certificate) > 0 {
|
||||||
certificate = []byte(options.Certificate)
|
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
||||||
} else if options.CertificatePath != "" {
|
} else if options.CertificatePath != "" {
|
||||||
content, err := os.ReadFile(options.CertificatePath)
|
content, err := os.ReadFile(options.CertificatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -31,11 +31,17 @@ sing-box uses JSON for configuration files.
|
||||||
### Check
|
### Check
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ sing-box check
|
sing-box check
|
||||||
```
|
```
|
||||||
|
|
||||||
### Format
|
### Format
|
||||||
|
|
||||||
```bash
|
```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
|
||||||
```
|
```
|
|
@ -29,11 +29,17 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||||
### 检查
|
### 检查
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ sing-box check
|
sing-box check
|
||||||
```
|
```
|
||||||
|
|
||||||
### 格式化
|
### 格式化
|
||||||
|
|
||||||
```bash
|
```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
|
||||||
```
|
```
|
|
@ -5,7 +5,7 @@ type SSHOutboundOptions struct {
|
||||||
ServerOptions
|
ServerOptions
|
||||||
User string `json:"user,omitempty"`
|
User string `json:"user,omitempty"`
|
||||||
Password string `json:"password,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"`
|
PrivateKeyPath string `json:"private_key_path,omitempty"`
|
||||||
PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"`
|
PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"`
|
||||||
HostKey Listable[string] `json:"host_key,omitempty"`
|
HostKey Listable[string] `json:"host_key,omitempty"`
|
||||||
|
|
|
@ -26,7 +26,7 @@ type OutboundTLSOptions struct {
|
||||||
MinVersion string `json:"min_version,omitempty"`
|
MinVersion string `json:"min_version,omitempty"`
|
||||||
MaxVersion string `json:"max_version,omitempty"`
|
MaxVersion string `json:"max_version,omitempty"`
|
||||||
CipherSuites Listable[string] `json:"cipher_suites,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"`
|
CertificatePath string `json:"certificate_path,omitempty"`
|
||||||
ECH *OutboundECHOptions `json:"ech,omitempty"`
|
ECH *OutboundECHOptions `json:"ech,omitempty"`
|
||||||
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
|
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
@ -76,10 +77,10 @@ func NewSSH(ctx context.Context, router adapter.Router, logger log.ContextLogger
|
||||||
if options.Password != "" {
|
if options.Password != "" {
|
||||||
outbound.authMethod = append(outbound.authMethod, ssh.Password(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
|
var privateKey []byte
|
||||||
if options.PrivateKey != "" {
|
if len(options.PrivateKey) > 0 {
|
||||||
privateKey = []byte(options.PrivateKey)
|
privateKey = []byte(strings.Join(options.PrivateKey, "\n"))
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
privateKey, err = os.ReadFile(os.ExpandEnv(options.PrivateKeyPath))
|
privateKey, err = os.ReadFile(os.ExpandEnv(options.PrivateKeyPath))
|
||||||
|
|
|
@ -32,7 +32,7 @@ func newV2RayPlugin(ctx context.Context, pluginOpts Args, router adapter.Router,
|
||||||
certHead := "-----BEGIN CERTIFICATE-----"
|
certHead := "-----BEGIN CERTIFICATE-----"
|
||||||
certTail := "-----END CERTIFICATE-----"
|
certTail := "-----END CERTIFICATE-----"
|
||||||
fixedCert := certHead + "\n" + certRaw + "\n" + certTail
|
fixedCert := certHead + "\n" + certRaw + "\n" + certTail
|
||||||
tlsOptions.Certificate = fixedCert
|
tlsOptions.Certificate = []string{fixedCert}
|
||||||
}
|
}
|
||||||
|
|
||||||
mode := "websocket"
|
mode := "websocket"
|
||||||
|
|
Loading…
Reference in a new issue