From 61d75cc1eaad051837015fddd263d261d87fb695 Mon Sep 17 00:00:00 2001 From: Farmehr Date: Wed, 1 Apr 2026 18:17:45 +0200 Subject: [PATCH 1/2] added slipstream support --- internal/actions/tunnel.go | 5 ++ internal/binary/binary.go | 30 +++++++-- internal/clientcfg/generate.go | 13 ++++ internal/config/defaults.go | 11 ++++ internal/config/tunnel.go | 44 +++++++++---- internal/config/validation.go | 18 +++++- internal/handlers/config_load.go | 42 ++++++++++++- internal/handlers/tunnel_add.go | 23 ++++++- internal/handlers/tunnel_status.go | 10 ++- internal/installer/uninstall.go | 1 + internal/transport/builder.go | 99 ++++++++++++++++++++++++++++++ internal/transport/install.go | 12 ++++ internal/updater/service.go | 6 +- internal/updater/updater.go | 1 + internal/updater/version.go | 17 +++-- tests/integration/config_test.go | 53 +++++++++++++++- tests/integration/tunnel_test.go | 84 +++++++++++++++++++++++++ 17 files changed, 437 insertions(+), 32 deletions(-) diff --git a/internal/actions/tunnel.go b/internal/actions/tunnel.go index bd14331..8955195 100644 --- a/internal/actions/tunnel.go +++ b/internal/actions/tunnel.go @@ -424,6 +424,11 @@ func TransportOptions() []SelectOption { Value: string(config.TransportSlipstream), Description: "High-performance DNS tunnel with TLS", }, + { + Label: "Slipstream Plus", + Value: string(config.TransportSlipstreamPlus), + Description: "Slipstream Plus with Turbo mode and backpressure (Fox-Fig fork)", + }, { Label: "DNSTT", Value: string(config.TransportDNSTT), diff --git a/internal/binary/binary.go b/internal/binary/binary.go index d9acba7..e5fdbdd 100644 --- a/internal/binary/binary.go +++ b/internal/binary/binary.go @@ -25,13 +25,15 @@ const ( BinarySSServer BinaryType = "ssserver" BinaryMicrosocks BinaryType = "microsocks" BinarySSHTunUser BinaryType = "sshtun-user" - BinaryVayDNSServer BinaryType = "vaydns-server" + BinaryVayDNSServer BinaryType = "vaydns-server" + BinarySlipstreamPlusServer BinaryType = "slipstream-plus-server" // Client binaries (used in testing) - BinaryDNSTTClient BinaryType = "dnstt-client" - BinarySlipstreamClient BinaryType = "slipstream-client" - BinarySSLocal BinaryType = "sslocal" - BinaryVayDNSClient BinaryType = "vaydns-client" + BinaryDNSTTClient BinaryType = "dnstt-client" + BinarySlipstreamClient BinaryType = "slipstream-client" + BinarySSLocal BinaryType = "sslocal" + BinaryVayDNSClient BinaryType = "vaydns-client" + BinarySlipstreamPlusClient BinaryType = "slipstream-plus-client" ) // BinaryDef defines how to obtain a binary. @@ -109,6 +111,15 @@ var DefaultBinaries = map[BinaryType]BinaryDef{ "windows": {"amd64"}, }, }, + BinarySlipstreamPlusServer: { + Type: BinarySlipstreamPlusServer, + EnvVar: "DNSTM_SLIPSTREAM_PLUS_SERVER_PATH", + URLPattern: "https://github.com/Fox-Fig/slipstream-rust-deploy/releases/latest/download/slipstream-server-{os}-{arch}", + PinnedVersion: "latest", + Platforms: map[string][]string{ + "linux": {"amd64", "arm64"}, + }, + }, // Client binaries - pinned versions for testing only BinaryDNSTTClient: { @@ -153,6 +164,15 @@ var DefaultBinaries = map[BinaryType]BinaryDef{ "windows": {"amd64"}, }, }, + BinarySlipstreamPlusClient: { + Type: BinarySlipstreamPlusClient, + EnvVar: "DNSTM_TEST_SLIPSTREAM_PLUS_CLIENT_PATH", + URLPattern: "https://github.com/Fox-Fig/slipstream-rust-deploy/releases/latest/download/slipstream-client-{os}-{arch}", + PinnedVersion: "latest", // Manual bump only + Platforms: map[string][]string{ + "linux": {"amd64", "arm64"}, + }, + }, } const ( diff --git a/internal/clientcfg/generate.go b/internal/clientcfg/generate.go index b1ff998..62bace4 100644 --- a/internal/clientcfg/generate.go +++ b/internal/clientcfg/generate.go @@ -55,6 +55,19 @@ func Generate(tunnel *config.TunnelConfig, backend *config.BackendConfig, opts G } cfg.Transport.PubKey = pubKey + case config.TransportSlipstreamPlus: + if !opts.NoCert { + certPath := filepath.Join(tunnelDir, "cert.pem") + if tunnel.SlipstreamPlus != nil && tunnel.SlipstreamPlus.Cert != "" { + certPath = tunnel.SlipstreamPlus.Cert + } + certPEM, err := os.ReadFile(certPath) + if err != nil { + return nil, fmt.Errorf("failed to read certificate: %w", err) + } + cfg.Transport.Cert = string(certPEM) + } + case config.TransportVayDNS: pubKeyPath := filepath.Join(tunnelDir, "server.pub") pubKey, err := keys.ReadPublicKey(pubKeyPath) diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 13043f7..d3eb5b2 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -59,6 +59,17 @@ func (c *Config) ApplyDefaults() { t.DNSTT.MTU = 1232 } } + if t.Transport == TransportSlipstreamPlus { + if t.SlipstreamPlus == nil { + t.SlipstreamPlus = &SlipstreamPlusConfig{} + } + if t.SlipstreamPlus.MaxConnections == 0 { + t.SlipstreamPlus.MaxConnections = 256 + } + if t.SlipstreamPlus.IdleTimeoutSeconds == 0 { + t.SlipstreamPlus.IdleTimeoutSeconds = 60 + } + } if t.Transport == TransportVayDNS { if t.VayDNS == nil { t.VayDNS = &VayDNSConfig{} diff --git a/internal/config/tunnel.go b/internal/config/tunnel.go index 024c4f4..b699747 100644 --- a/internal/config/tunnel.go +++ b/internal/config/tunnel.go @@ -4,22 +4,24 @@ package config type TransportType string const ( - TransportSlipstream TransportType = "slipstream" - TransportDNSTT TransportType = "dnstt" - TransportVayDNS TransportType = "vaydns" + TransportSlipstream TransportType = "slipstream" + TransportSlipstreamPlus TransportType = "slipstream-plus" + TransportDNSTT TransportType = "dnstt" + TransportVayDNS TransportType = "vaydns" ) // TunnelConfig configures a DNS tunnel. type TunnelConfig struct { - Tag string `json:"tag"` - Enabled *bool `json:"enabled,omitempty"` - Transport TransportType `json:"transport"` - Backend string `json:"backend"` - Domain string `json:"domain"` - Port int `json:"port,omitempty"` - Slipstream *SlipstreamConfig `json:"slipstream,omitempty"` - DNSTT *DNSTTConfig `json:"dnstt,omitempty"` - VayDNS *VayDNSConfig `json:"vaydns,omitempty"` + Tag string `json:"tag"` + Enabled *bool `json:"enabled,omitempty"` + Transport TransportType `json:"transport"` + Backend string `json:"backend"` + Domain string `json:"domain"` + Port int `json:"port,omitempty"` + Slipstream *SlipstreamConfig `json:"slipstream,omitempty"` + SlipstreamPlus *SlipstreamPlusConfig `json:"slipstream_plus,omitempty"` + DNSTT *DNSTTConfig `json:"dnstt,omitempty"` + VayDNS *VayDNSConfig `json:"vaydns,omitempty"` } // SlipstreamConfig holds Slipstream-specific configuration. @@ -28,6 +30,16 @@ type SlipstreamConfig struct { Key string `json:"key,omitempty"` } +// SlipstreamPlusConfig holds Slipstream Plus-specific configuration. +type SlipstreamPlusConfig struct { + Cert string `json:"cert,omitempty"` + Key string `json:"key,omitempty"` + MaxConnections int `json:"max_connections,omitempty"` + IdleTimeoutSeconds int `json:"idle_timeout_seconds,omitempty"` + Fallback string `json:"fallback,omitempty"` + ResetSeed string `json:"reset_seed,omitempty"` +} + // DNSTTConfig holds DNSTT-specific configuration. type DNSTTConfig struct { MTU int `json:"mtu,omitempty"` @@ -113,6 +125,11 @@ func (t *TunnelConfig) IsSlipstream() bool { return t.Transport == TransportSlipstream } +// IsSlipstreamPlus returns true if this is a Slipstream Plus tunnel. +func (t *TunnelConfig) IsSlipstreamPlus() bool { + return t.Transport == TransportSlipstreamPlus +} + // IsDNSTT returns true if this is a DNSTT tunnel. func (t *TunnelConfig) IsDNSTT() bool { return t.Transport == TransportDNSTT @@ -127,6 +144,7 @@ func (t *TunnelConfig) IsVayDNS() bool { func GetTransportTypes() []TransportType { return []TransportType{ TransportSlipstream, + TransportSlipstreamPlus, TransportDNSTT, TransportVayDNS, } @@ -137,6 +155,8 @@ func GetTransportTypeDisplayName(t TransportType) string { switch t { case TransportSlipstream: return "Slipstream" + case TransportSlipstreamPlus: + return "Slipstream Plus" case TransportDNSTT: return "DNSTT" case TransportVayDNS: diff --git a/internal/config/validation.go b/internal/config/validation.go index c19e087..80eac8f 100644 --- a/internal/config/validation.go +++ b/internal/config/validation.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "net" "regexp" "time" ) @@ -109,7 +110,7 @@ func (c *Config) validateTunnels() error { return fmt.Errorf("tunnel '%s': transport is required", t.Tag) } - if t.Transport != TransportSlipstream && t.Transport != TransportDNSTT && t.Transport != TransportVayDNS { + if t.Transport != TransportSlipstream && t.Transport != TransportSlipstreamPlus && t.Transport != TransportDNSTT && t.Transport != TransportVayDNS { return fmt.Errorf("tunnel '%s': unknown transport %s", t.Tag, t.Transport) } @@ -149,6 +150,21 @@ func (c *Config) validateTunnels() error { } usedDomains[t.Domain] = t.Tag + // Validate Slipstream Plus-specific config + if t.Transport == TransportSlipstreamPlus && t.SlipstreamPlus != nil { + if t.SlipstreamPlus.MaxConnections < 0 { + return fmt.Errorf("tunnel '%s': slipstream_plus.max_connections must not be negative", t.Tag) + } + if t.SlipstreamPlus.IdleTimeoutSeconds < 0 { + return fmt.Errorf("tunnel '%s': slipstream_plus.idle_timeout_seconds must not be negative", t.Tag) + } + if t.SlipstreamPlus.Fallback != "" { + if _, _, err := net.SplitHostPort(t.SlipstreamPlus.Fallback); err != nil { + return fmt.Errorf("tunnel '%s': slipstream_plus.fallback must be host:port: %w", t.Tag, err) + } + } + } + // Validate DNSTT-specific config if t.Transport == TransportDNSTT && t.DNSTT != nil { if t.DNSTT.MTU != 0 && (t.DNSTT.MTU < 512 || t.DNSTT.MTU > 1400) { diff --git a/internal/handlers/config_load.go b/internal/handlers/config_load.go index 0238c85..8472997 100644 --- a/internal/handlers/config_load.go +++ b/internal/handlers/config_load.go @@ -189,7 +189,7 @@ func HandleConfigLoad(ctx *actions.Context) error { for _, tunnel := range newCfg.Tunnels { ctx.Output.Printf("\n %s (%s):\n", tunnel.Tag, tunnel.Domain) tunnelDir := filepath.Join(config.TunnelsDir, tunnel.Tag) - if tunnel.Transport == config.TransportSlipstream { + if tunnel.Transport == config.TransportSlipstream || tunnel.Transport == config.TransportSlipstreamPlus { certPath := filepath.Join(tunnelDir, "cert.pem") keyPath := filepath.Join(tunnelDir, "key.pem") fingerprint, err := certs.ReadCertificateFingerprint(certPath) @@ -279,6 +279,46 @@ func ensureTunnelService(ctx *actions.Context, tunnelCfg *config.TunnelConfig, c tunnelCfg.Slipstream.Key = certInfo.KeyPath ctx.Output.Status(fmt.Sprintf("Generated certificate for %s", tunnelCfg.Domain)) } + } else if tunnelCfg.Transport == config.TransportSlipstreamPlus { + if tunnelCfg.SlipstreamPlus == nil { + tunnelCfg.SlipstreamPlus = &config.SlipstreamPlusConfig{} + } + certProvided := tunnelCfg.SlipstreamPlus.Cert != "" + keyProvided := tunnelCfg.SlipstreamPlus.Key != "" + if certProvided || keyProvided { + if !certProvided || !keyProvided { + return fmt.Errorf("both cert and key paths must be provided for tunnel %s", tunnelCfg.Tag) + } + if _, err := os.Stat(tunnelCfg.SlipstreamPlus.Cert); err != nil { + return fmt.Errorf("certificate file not found: %s", tunnelCfg.SlipstreamPlus.Cert) + } + canRead, err := system.CanDnstmUserReadFile(tunnelCfg.SlipstreamPlus.Cert) + if err != nil { + return fmt.Errorf("failed to check certificate permissions: %w", err) + } + if !canRead { + return fmt.Errorf("dnstm user cannot read certificate file: %s", tunnelCfg.SlipstreamPlus.Cert) + } + if _, err := os.Stat(tunnelCfg.SlipstreamPlus.Key); err != nil { + return fmt.Errorf("key file not found: %s", tunnelCfg.SlipstreamPlus.Key) + } + canRead, err = system.CanDnstmUserReadFile(tunnelCfg.SlipstreamPlus.Key) + if err != nil { + return fmt.Errorf("failed to check key permissions: %w", err) + } + if !canRead { + return fmt.Errorf("dnstm user cannot read key file: %s", tunnelCfg.SlipstreamPlus.Key) + } + ctx.Output.Status(fmt.Sprintf("Using provided certificate for %s", tunnelCfg.Domain)) + } else { + certInfo, err := certs.GetOrCreateInDir(tunnelDir, tunnelCfg.Domain) + if err != nil { + return fmt.Errorf("failed to generate certificate: %w", err) + } + tunnelCfg.SlipstreamPlus.Cert = certInfo.CertPath + tunnelCfg.SlipstreamPlus.Key = certInfo.KeyPath + ctx.Output.Status(fmt.Sprintf("Generated certificate for %s", tunnelCfg.Domain)) + } } else if tunnelCfg.Transport == config.TransportDNSTT { // Initialize DNSTT config if nil if tunnelCfg.DNSTT == nil { diff --git a/internal/handlers/tunnel_add.go b/internal/handlers/tunnel_add.go index 696c072..d9e0e2e 100644 --- a/internal/handlers/tunnel_add.go +++ b/internal/handlers/tunnel_add.go @@ -46,6 +46,7 @@ func addTunnelInteractive(ctx *actions.Context, cfg *config.Config) error { {Label: "VayDNS", Value: string(config.TransportVayDNS)}, {Label: "DNSTT", Value: string(config.TransportDNSTT)}, {Label: "Slipstream", Value: string(config.TransportSlipstream)}, + {Label: "Slipstream Plus", Value: string(config.TransportSlipstreamPlus)}, }, }) if err != nil { @@ -331,6 +332,9 @@ func addTunnelInteractive(ctx *actions.Context, cfg *config.Config) error { RecordType: vaydnsRecordType, } } + if tunnelCfg.Transport == config.TransportSlipstreamPlus { + tunnelCfg.SlipstreamPlus = &config.SlipstreamPlusConfig{} + } // Allocate port port := cfg.AllocateNextPort() @@ -354,8 +358,8 @@ func addTunnelNonInteractive(ctx *actions.Context, cfg *config.Config) error { transportType := config.TransportType(transportStr) // Validate transport type - if transportType != config.TransportSlipstream && transportType != config.TransportDNSTT && transportType != config.TransportVayDNS { - return fmt.Errorf("invalid transport type: %s (must be slipstream, dnstt, or vaydns)", transportType) + if transportType != config.TransportSlipstream && transportType != config.TransportSlipstreamPlus && transportType != config.TransportDNSTT && transportType != config.TransportVayDNS { + return fmt.Errorf("invalid transport type: %s (must be slipstream, slipstream-plus, dnstt, or vaydns)", transportType) } // Validate backend exists and is compatible @@ -434,6 +438,9 @@ func addTunnelNonInteractive(ctx *actions.Context, cfg *config.Config) error { } tunnelCfg.VayDNS = v } + if transportType == config.TransportSlipstreamPlus { + tunnelCfg.SlipstreamPlus = &config.SlipstreamPlusConfig{} + } // Allocate port if port == 0 { @@ -551,6 +558,18 @@ func createTunnel(ctx *actions.Context, tunnelCfg *config.TunnelConfig, cfg *con Key: certInfo.KeyPath, } ctx.Output.Status("TLS certificate ready") + } else if tunnelCfg.Transport == config.TransportSlipstreamPlus { + certInfo, err := certs.GetOrCreateInDir(tunnelDir, tunnelCfg.Domain) + if err != nil { + return fmt.Errorf("failed to generate certificate: %w", err) + } + fingerprint = certInfo.Fingerprint + if tunnelCfg.SlipstreamPlus == nil { + tunnelCfg.SlipstreamPlus = &config.SlipstreamPlusConfig{} + } + tunnelCfg.SlipstreamPlus.Cert = certInfo.CertPath + tunnelCfg.SlipstreamPlus.Key = certInfo.KeyPath + ctx.Output.Status("TLS certificate ready") } else if tunnelCfg.Transport == config.TransportDNSTT { keyInfo, err := keys.GetOrCreateInDir(tunnelDir) if err != nil { diff --git a/internal/handlers/tunnel_status.go b/internal/handlers/tunnel_status.go index 3854e53..8376690 100644 --- a/internal/handlers/tunnel_status.go +++ b/internal/handlers/tunnel_status.go @@ -72,11 +72,14 @@ func HandleTunnelStatus(ctx *actions.Context) error { // Show certificate/key info based on transport type tunnelDir := filepath.Join(config.TunnelsDir, tunnelCfg.Tag) - if tunnelCfg.Transport == config.TransportSlipstream { + if tunnelCfg.Transport == config.TransportSlipstream || tunnelCfg.Transport == config.TransportSlipstreamPlus { certPath := filepath.Join(tunnelDir, "cert.pem") if tunnelCfg.Slipstream != nil && tunnelCfg.Slipstream.Cert != "" { certPath = tunnelCfg.Slipstream.Cert } + if tunnelCfg.SlipstreamPlus != nil && tunnelCfg.SlipstreamPlus.Cert != "" { + certPath = tunnelCfg.SlipstreamPlus.Cert + } fingerprint, err := certs.ReadCertificateFingerprint(certPath) if err == nil { certSection := actions.InfoSection{ @@ -142,11 +145,14 @@ func HandleTunnelStatus(ctx *actions.Context) error { ctx.Output.Println() ctx.Output.Println(tunnel.GetFormattedInfo()) - if tunnelCfg.Transport == config.TransportSlipstream { + if tunnelCfg.Transport == config.TransportSlipstream || tunnelCfg.Transport == config.TransportSlipstreamPlus { certPath := filepath.Join(tunnelDir, "cert.pem") if tunnelCfg.Slipstream != nil && tunnelCfg.Slipstream.Cert != "" { certPath = tunnelCfg.Slipstream.Cert } + if tunnelCfg.SlipstreamPlus != nil && tunnelCfg.SlipstreamPlus.Cert != "" { + certPath = tunnelCfg.SlipstreamPlus.Cert + } fingerprint, err := certs.ReadCertificateFingerprint(certPath) if err == nil { ctx.Output.Println("Certificate Fingerprint:") diff --git a/internal/installer/uninstall.go b/internal/installer/uninstall.go index d34974d..6829b03 100644 --- a/internal/installer/uninstall.go +++ b/internal/installer/uninstall.go @@ -67,6 +67,7 @@ func PerformFullUninstall(output actions.OutputWriter, isInteractive bool) error "/usr/local/bin/ssserver", "/usr/local/bin/sshtun-user", "/usr/local/bin/vaydns-server", + "/usr/local/bin/slipstream-plus-server", "/usr/local/bin/microsocks", } for _, bin := range binaries { diff --git a/internal/transport/builder.go b/internal/transport/builder.go index c6ac3bb..de271e4 100644 --- a/internal/transport/builder.go +++ b/internal/transport/builder.go @@ -61,6 +61,12 @@ func VayDNSBinaryPath() string { return path } +// SlipstreamPlusBinaryPath returns the path to slipstream-plus-server. +func SlipstreamPlusBinaryPath() string { + path, _ := getBinManager().GetPath(binary.BinarySlipstreamPlusServer) + return path +} + // BuildOptions configures how the transport should bind. type BuildOptions struct { BindHost string // "127.0.0.1" for multi mode, or external IP for single mode @@ -142,6 +148,8 @@ func (b *Builder) BuildTunnelService(tunnel *config.TunnelConfig, backend *confi return b.buildDNSTTTunnel(tunnel, backend, targetAddr, opts, result) case config.TransportVayDNS: return b.buildVayDNSTunnel(tunnel, backend, targetAddr, opts, result) + case config.TransportSlipstreamPlus: + return b.buildSlipstreamPlusTunnel(tunnel, backend, targetAddr, opts, result) default: return nil, fmt.Errorf("unknown transport type: %s", tunnel.Transport) } @@ -224,6 +232,97 @@ func (b *Builder) buildSlipstreamShadowsocksTunnel(tunnel *config.TunnelConfig, return result, nil } +// buildSlipstreamPlusTunnel builds a Slipstream Plus-based tunnel service. +func (b *Builder) buildSlipstreamPlusTunnel(tunnel *config.TunnelConfig, backend *config.BackendConfig, targetAddr string, opts *BuildOptions, result *TunnelBuildResult) (*TunnelBuildResult, error) { + if tunnel.SlipstreamPlus == nil || tunnel.SlipstreamPlus.Cert == "" || tunnel.SlipstreamPlus.Key == "" { + return nil, fmt.Errorf("slipstream-plus cert/key paths not set for tunnel %s", tunnel.Tag) + } + + certPath := tunnel.SlipstreamPlus.Cert + keyPath := tunnel.SlipstreamPlus.Key + result.ReadPaths = append(result.ReadPaths, certPath, keyPath) + + if backend.Type == config.BackendShadowsocks { + return b.buildSlipstreamPlusShadowsocksTunnel(tunnel, backend, certPath, keyPath, opts, result) + } + + args := []string{ + "--dns-listen-host", opts.BindHost, + "--domain", tunnel.Domain, + "--dns-listen-port", fmt.Sprintf("%d", opts.BindPort), + "--target-address", targetAddr, + "--cert", certPath, + "--key", keyPath, + } + if tunnel.SlipstreamPlus.MaxConnections > 0 { + args = append(args, "--max-connections", fmt.Sprintf("%d", tunnel.SlipstreamPlus.MaxConnections)) + } + if tunnel.SlipstreamPlus.IdleTimeoutSeconds > 0 { + args = append(args, "--idle-timeout-seconds", fmt.Sprintf("%d", tunnel.SlipstreamPlus.IdleTimeoutSeconds)) + } + if tunnel.SlipstreamPlus.Fallback != "" { + args = append(args, "--fallback", tunnel.SlipstreamPlus.Fallback) + } + if tunnel.SlipstreamPlus.ResetSeed != "" { + args = append(args, "--reset-seed", tunnel.SlipstreamPlus.ResetSeed) + result.ReadPaths = append(result.ReadPaths, tunnel.SlipstreamPlus.ResetSeed) + } + + result.ExecStart = fmt.Sprintf("%s %s", SlipstreamPlusBinaryPath(), strings.Join(args, " ")) + return result, nil +} + +// buildSlipstreamPlusShadowsocksTunnel builds a Slipstream Plus + Shadowsocks tunnel using SIP003 plugin mode. +func (b *Builder) buildSlipstreamPlusShadowsocksTunnel(tunnel *config.TunnelConfig, backend *config.BackendConfig, certPath, keyPath string, opts *BuildOptions, result *TunnelBuildResult) (*TunnelBuildResult, error) { + if backend.Shadowsocks == nil { + return nil, fmt.Errorf("shadowsocks backend missing configuration") + } + + method := backend.Shadowsocks.Method + if method == "" { + method = "aes-256-gcm" + } + + pluginOpts := fmt.Sprintf("domain=%s;dns-listen-host=%s;dns-listen-port=%d;cert=%s;key=%s", + tunnel.Domain, opts.BindHost, opts.BindPort, certPath, keyPath) + if tunnel.SlipstreamPlus.MaxConnections > 0 { + pluginOpts += fmt.Sprintf(";max-connections=%d", tunnel.SlipstreamPlus.MaxConnections) + } + if tunnel.SlipstreamPlus.IdleTimeoutSeconds > 0 { + pluginOpts += fmt.Sprintf(";idle-timeout-seconds=%d", tunnel.SlipstreamPlus.IdleTimeoutSeconds) + } + if tunnel.SlipstreamPlus.Fallback != "" { + pluginOpts += fmt.Sprintf(";fallback=%s", tunnel.SlipstreamPlus.Fallback) + } + + ssConfig := map[string]interface{}{ + "server": opts.BindHost, + "server_port": opts.BindPort, + "password": backend.Shadowsocks.Password, + "method": method, + "mode": "tcp_only", + "plugin": SlipstreamPlusBinaryPath(), + "plugin_opts": pluginOpts, + "plugin_mode": "tcp_only", + } + + configPath := filepath.Join(result.ConfigDir, "config.json") + data, err := json.MarshalIndent(ssConfig, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal config: %w", err) + } + if err := os.WriteFile(configPath, data, 0644); err != nil { + return nil, fmt.Errorf("failed to write config: %w", err) + } + if err := system.ChownToDnstm(configPath); err != nil { + return nil, fmt.Errorf("failed to set config file ownership: %w", err) + } + + result.ExecStart = fmt.Sprintf("%s -c %s", SSServerBinaryPath(), configPath) + result.ReadPaths = append(result.ReadPaths, configPath) + return result, nil +} + // buildDNSTTTunnel builds a DNSTT-based tunnel service. func (b *Builder) buildDNSTTTunnel(tunnel *config.TunnelConfig, backend *config.BackendConfig, targetAddr string, opts *BuildOptions, result *TunnelBuildResult) (*TunnelBuildResult, error) { // DNSTT doesn't support Shadowsocks diff --git a/internal/transport/install.go b/internal/transport/install.go index 17fc062..931ab06 100644 --- a/internal/transport/install.go +++ b/internal/transport/install.go @@ -21,6 +21,8 @@ func EnsureTransportBinariesInstalled(transport config.TransportType) error { return EnsureDnsttInstalled() case config.TransportVayDNS: return EnsureVayDNSInstalled() + case config.TransportSlipstreamPlus: + return EnsureSlipstreamPlusInstalled() default: return nil } @@ -76,6 +78,16 @@ func EnsureVayDNSInstalledWithStatus(statusFn StatusFunc) error { return ensureBinaryInstalled(binary.BinaryVayDNSServer, "vaydns-server", statusFn) } +// EnsureSlipstreamPlusInstalled installs slipstream-plus-server if not present. +func EnsureSlipstreamPlusInstalled() error { + return EnsureSlipstreamPlusInstalledWithStatus(nil) +} + +// EnsureSlipstreamPlusInstalledWithStatus installs slipstream-plus-server with status callback. +func EnsureSlipstreamPlusInstalledWithStatus(statusFn StatusFunc) error { + return ensureBinaryInstalled(binary.BinarySlipstreamPlusServer, "slipstream-plus-server", statusFn) +} + // EnsureSSHTunUserInstalled installs sshtun-user if not present. func EnsureSSHTunUserInstalled() error { return EnsureSSHTunUserInstalledWithStatus(nil) diff --git a/internal/updater/service.go b/internal/updater/service.go index a7767f5..bdb089d 100644 --- a/internal/updater/service.go +++ b/internal/updater/service.go @@ -24,7 +24,7 @@ func GetActiveServicesForBinary(binType binary.BinaryType) []string { services = append(services, proxy.MicrosocksServiceName) } - case binary.BinarySlipstreamServer, binary.BinarySSServer, binary.BinaryDNSTTServer, binary.BinaryVayDNSServer: + case binary.BinarySlipstreamServer, binary.BinarySlipstreamPlusServer, binary.BinarySSServer, binary.BinaryDNSTTServer, binary.BinaryVayDNSServer: // Check tunnel services cfg, err := config.Load() if err != nil || cfg == nil { @@ -70,6 +70,9 @@ func tunnelUsesBinary(tunnelCfg *config.TunnelConfig, binType binary.BinaryType) case binary.BinaryVayDNSServer: return tunnelCfg.Transport == config.TransportVayDNS + + case binary.BinarySlipstreamPlusServer: + return tunnelCfg.Transport == config.TransportSlipstreamPlus } return false @@ -109,6 +112,7 @@ func GetAllActiveServices() map[binary.BinaryType][]string { // Note: dnstt-server is skipped for updates, but we still track its services binary.BinaryDNSTTServer, binary.BinaryVayDNSServer, + binary.BinarySlipstreamPlusServer, } for _, binType := range binaries { diff --git a/internal/updater/updater.go b/internal/updater/updater.go index cd48635..0cd9544 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -98,6 +98,7 @@ func checkBinaryUpdates(manifest *VersionManifest) []BinaryUpdate { binary.BinaryMicrosocks, binary.BinarySSHTunUser, binary.BinaryVayDNSServer, + binary.BinarySlipstreamPlusServer, } for _, binType := range binariesToCheck { diff --git a/internal/updater/version.go b/internal/updater/version.go index 4700eb6..4517f8f 100644 --- a/internal/updater/version.go +++ b/internal/updater/version.go @@ -18,12 +18,13 @@ const ( // VersionManifest stores installed versions of transport binaries. type VersionManifest struct { - SlipstreamServer string `json:"slipstream-server,omitempty"` - SSServer string `json:"ssserver,omitempty"` - Microsocks string `json:"microsocks,omitempty"` - SSHTunUser string `json:"sshtun-user,omitempty"` - VayDNSServer string `json:"vaydns-server,omitempty"` - UpdatedAt time.Time `json:"updated_at"` + SlipstreamServer string `json:"slipstream-server,omitempty"` + SlipstreamPlusServer string `json:"slipstream-plus-server,omitempty"` + SSServer string `json:"ssserver,omitempty"` + Microsocks string `json:"microsocks,omitempty"` + SSHTunUser string `json:"sshtun-user,omitempty"` + VayDNSServer string `json:"vaydns-server,omitempty"` + UpdatedAt time.Time `json:"updated_at"` } // GetManifestPath returns the path to the version manifest file. @@ -83,6 +84,8 @@ func (m *VersionManifest) GetVersion(binaryName string) string { return m.SSHTunUser case "vaydns-server": return m.VayDNSServer + case "slipstream-plus-server": + return m.SlipstreamPlusServer default: return "" } @@ -101,6 +104,8 @@ func (m *VersionManifest) SetVersion(binaryName, version string) { m.SSHTunUser = version case "vaydns-server": m.VayDNSServer = version + case "slipstream-plus-server": + m.SlipstreamPlusServer = version } } diff --git a/tests/integration/config_test.go b/tests/integration/config_test.go index 6248b95..86f7429 100644 --- a/tests/integration/config_test.go +++ b/tests/integration/config_test.go @@ -167,6 +167,20 @@ func TestConfigSerialization(t *testing.T) { Fallback: "127.0.0.1:8888", }, }, + { + Tag: "tunnel-d", + Transport: config.TransportSlipstreamPlus, + Backend: "socks", + Domain: "d.example.com", + Port: 5313, + SlipstreamPlus: &config.SlipstreamPlusConfig{ + Cert: "/path/to/plus-cert", + Key: "/path/to/plus-key", + MaxConnections: 128, + IdleTimeoutSeconds: 30, + Fallback: "127.0.0.1:9999", + }, + }, }, } @@ -193,8 +207,8 @@ func TestConfigSerialization(t *testing.T) { t.Errorf("len(Backends) = %d, want 2", len(loaded.Backends)) } - if len(loaded.Tunnels) != 3 { - t.Errorf("len(Tunnels) = %d, want 3", len(loaded.Tunnels)) + if len(loaded.Tunnels) != 4 { + t.Errorf("len(Tunnels) = %d, want 4", len(loaded.Tunnels)) } // Verify shadowsocks config @@ -241,6 +255,24 @@ func TestConfigSerialization(t *testing.T) { if vaydns.VayDNS.Fallback != "127.0.0.1:8888" { t.Errorf("vaydns.VayDNS.Fallback = %q, want '127.0.0.1:8888'", vaydns.VayDNS.Fallback) } + + // Verify Slipstream Plus config + plus := loaded.GetTunnelByTag("tunnel-d") + if plus == nil { + t.Fatal("tunnel-d not found") + } + if plus.SlipstreamPlus == nil { + t.Fatal("plus.SlipstreamPlus is nil") + } + if plus.SlipstreamPlus.MaxConnections != 128 { + t.Errorf("plus.SlipstreamPlus.MaxConnections = %d, want 128", plus.SlipstreamPlus.MaxConnections) + } + if plus.SlipstreamPlus.IdleTimeoutSeconds != 30 { + t.Errorf("plus.SlipstreamPlus.IdleTimeoutSeconds = %d, want 30", plus.SlipstreamPlus.IdleTimeoutSeconds) + } + if plus.SlipstreamPlus.Fallback != "127.0.0.1:9999" { + t.Errorf("plus.SlipstreamPlus.Fallback = %q, want '127.0.0.1:9999'", plus.SlipstreamPlus.Fallback) + } } func TestConfigApplyDefaults(t *testing.T) { @@ -276,6 +308,12 @@ func TestConfigApplyDefaults(t *testing.T) { DnsttCompat: true, }, }, + { + Tag: "tunnel-e", + Transport: config.TransportSlipstreamPlus, + Backend: "socks", + Domain: "e.example.com", + }, }, } @@ -346,6 +384,17 @@ func TestConfigApplyDefaults(t *testing.T) { if vaydnsCompat.VayDNS.ClientIDSize != 0 { t.Errorf("compat VayDNS.ClientIDSize = %d, want 0 (server uses 8-byte ID)", vaydnsCompat.VayDNS.ClientIDSize) } + + plus := cfg.GetTunnelByTag("tunnel-e") + if plus.SlipstreamPlus == nil { + t.Fatal("SlipstreamPlus config should be created") + } + if plus.SlipstreamPlus.MaxConnections != 256 { + t.Errorf("SlipstreamPlus.MaxConnections = %d, want 256", plus.SlipstreamPlus.MaxConnections) + } + if plus.SlipstreamPlus.IdleTimeoutSeconds != 60 { + t.Errorf("SlipstreamPlus.IdleTimeoutSeconds = %d, want 60", plus.SlipstreamPlus.IdleTimeoutSeconds) + } } func TestConfigEnsureBuiltinBackends(t *testing.T) { diff --git a/tests/integration/tunnel_test.go b/tests/integration/tunnel_test.go index ddfea08..4c1e888 100644 --- a/tests/integration/tunnel_test.go +++ b/tests/integration/tunnel_test.go @@ -286,6 +286,68 @@ func TestTunnelAdd_VayDNS_WithFallback(t *testing.T) { } } +func TestTunnelAdd_SlipstreamPlus(t *testing.T) { + env := NewTestEnv(t) + + cfg := env.DefaultConfig() + if err := env.WriteConfig(cfg); err != nil { + t.Fatalf("failed to write config: %v", err) + } + + loaded, err := env.ReadConfig() + if err != nil { + t.Fatalf("failed to read config: %v", err) + } + + loaded.Tunnels = append(loaded.Tunnels, config.TunnelConfig{ + Tag: "test-plus", + Transport: config.TransportSlipstreamPlus, + Backend: "socks", + Domain: "plus.example.com", + Port: 5314, + Enabled: boolPtr(true), + SlipstreamPlus: &config.SlipstreamPlusConfig{ + Cert: "/path/to/cert", + Key: "/path/to/key", + MaxConnections: 128, + IdleTimeoutSeconds: 45, + Fallback: "127.0.0.1:9999", + }, + }) + + if err := env.WriteConfig(loaded); err != nil { + t.Fatalf("failed to write config: %v", err) + } + + reloaded, err := env.ReadConfig() + if err != nil { + t.Fatalf("failed to reload config: %v", err) + } + + tunnel := reloaded.GetTunnelByTag("test-plus") + if tunnel == nil { + t.Fatal("tunnel not found") + } + if tunnel.Transport != config.TransportSlipstreamPlus { + t.Errorf("transport = %v, want %v", tunnel.Transport, config.TransportSlipstreamPlus) + } + if tunnel.SlipstreamPlus == nil { + t.Fatal("SlipstreamPlus config is nil") + } + if tunnel.SlipstreamPlus.MaxConnections != 128 { + t.Errorf("MaxConnections = %d, want 128", tunnel.SlipstreamPlus.MaxConnections) + } + if tunnel.SlipstreamPlus.IdleTimeoutSeconds != 45 { + t.Errorf("IdleTimeoutSeconds = %d, want 45", tunnel.SlipstreamPlus.IdleTimeoutSeconds) + } + if tunnel.SlipstreamPlus.Fallback != "127.0.0.1:9999" { + t.Errorf("Fallback = %q, want '127.0.0.1:9999'", tunnel.SlipstreamPlus.Fallback) + } + if !tunnel.IsSlipstreamPlus() { + t.Error("IsSlipstreamPlus() should return true") + } +} + func TestTunnelList(t *testing.T) { env := NewTestEnv(t) @@ -523,6 +585,28 @@ func TestTunnelValidation(t *testing.T) { }, wantErr: "vaydns.mtu must be between", }, + { + name: "slipstream-plus bad fallback", + tunnel: config.TunnelConfig{ + Tag: "plus-bad-fb", + Transport: config.TransportSlipstreamPlus, + Backend: "socks", + Domain: "test.example.com", + SlipstreamPlus: &config.SlipstreamPlusConfig{Fallback: "not-host-port"}, + }, + wantErr: "slipstream_plus.fallback must be host:port", + }, + { + name: "slipstream-plus negative max_connections", + tunnel: config.TunnelConfig{ + Tag: "plus-neg-mc", + Transport: config.TransportSlipstreamPlus, + Backend: "socks", + Domain: "test.example.com", + SlipstreamPlus: &config.SlipstreamPlusConfig{MaxConnections: -1}, + }, + wantErr: "max_connections must not be negative", + }, } for _, tt := range tests { From 04fdf74558566df2bc04e35753c078b0124668aa Mon Sep 17 00:00:00 2001 From: Farmehr Date: Thu, 2 Apr 2026 22:41:40 +0200 Subject: [PATCH 2/2] fix: resolve version.go merge conflict Remove duplicate old implementation that was incorrectly merged. The main branch refactored to use binman wrappers, but conflict resolution kept both old switch-based and new wrapper implementations. Made-with: Cursor --- internal/updater/version.go | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/internal/updater/version.go b/internal/updater/version.go index fc23efb..5e51184 100644 --- a/internal/updater/version.go +++ b/internal/updater/version.go @@ -58,41 +58,6 @@ func (vm *VersionManifest) Save() error { } // GetVersion returns the installed version for a binary. -func (m *VersionManifest) GetVersion(binaryName string) string { - switch binaryName { - case "slipstream-server": - return m.SlipstreamServer - case "ssserver": - return m.SSServer - case "microsocks": - return m.Microsocks - case "sshtun-user": - return m.SSHTunUser - case "vaydns-server": - return m.VayDNSServer - case "slipstream-plus-server": - return m.SlipstreamPlusServer - default: - return "" - } -} - -// SetVersion sets the installed version for a binary. -func (m *VersionManifest) SetVersion(binaryName, version string) { - switch binaryName { - case "slipstream-server": - m.SlipstreamServer = version - case "ssserver": - m.SSServer = version - case "microsocks": - m.Microsocks = version - case "sshtun-user": - m.SSHTunUser = version - case "vaydns-server": - m.VayDNSServer = version - case "slipstream-plus-server": - m.SlipstreamPlusServer = version - } func (vm *VersionManifest) GetVersion(name string) string { return vm.m.GetVersion(name) }