Skip to content

Commit 7f9a40c

Browse files
committed
Merge branch 'dev'
# Conflicts: # internal/clientcfg/fields.go # internal/clientcfg/generate.go # internal/version/version.go
2 parents 62f8c0f + fcf9e74 commit 7f9a40c

25 files changed

Lines changed: 1001 additions & 124 deletions

README.md

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ Unified tunnel manager for Linux servers. Manages DNS tunnels (DNSTT, NoizDNS, S
44

55
## Features
66

7-
- **Multi-transport**: DNSTT/NoizDNS (DNS tunnels with Curve25519 encryption), Slipstream (QUIC-based DNS), VayDNS (KCP-based DNS with Curve25519), NaiveProxy (HTTPS with Caddy)
7+
- **Multi-transport**: DNSTT/NoizDNS (DNS tunnels with Curve25519 encryption), Slipstream (QUIC-based DNS), VayDNS (KCP-based DNS with Curve25519), NaiveProxy (HTTPS with Caddy), StunTLS (SSH over TLS + WebSocket)
88
- **Dual backend**: Built-in SOCKS5 proxy or SSH forwarding
99
- **DNS routing**: Single-tunnel or multi-tunnel mode with domain-based dispatch
10+
- **External routing**: Forward DNS queries for a domain to a custom port for user-managed protocols
1011
- **WARP integration**: Optional Cloudflare WARP outbound routing (see [dnstun-ezpz](https://github.com/aleskxyz/dnstun-ezpz) for an alternative approach)
11-
- **User management**: Managed SSH + SOCKS credentials per user
12+
- **User management**: Multi-user SSH + SOCKS credentials (all users authenticate simultaneously)
1213
- **Live dashboard**: Real-time TUI with CPU, RAM, traffic sparklines, per-protocol connection stats, and tunnel status
1314
- **Diagnostics**: Built-in health checks for services, ports, keys, DNS resolution, and boot persistence
1415
- **Interactive TUI + CLI**: Menu-driven setup or scriptable subcommands
@@ -21,7 +22,7 @@ Unified tunnel manager for Linux servers. Manages DNS tunnels (DNSTT, NoizDNS, S
2122

2223
- **OS**: Linux (Ubuntu 20.04+, Debian 11+, or similar)
2324
- **Domain**: DNS A record pointed at your server (required for DNS tunnels and NaiveProxy)
24-
- **Ports**: 53/udp (DNS tunnels), 443/tcp (NaiveProxy)
25+
- **Ports**: 53/udp (DNS tunnels), 443/tcp (NaiveProxy, StunTLS)
2526

2627
## Quick Start
2728

@@ -103,6 +104,7 @@ slipgate config import # Import configuration
103104
# Internal (used by systemd services)
104105
slipgate dnsrouter serve # Start DNS router
105106
slipgate socks serve # Start built-in SOCKS5 proxy
107+
slipgate stuntls serve # Start StunTLS proxy
106108
```
107109

108110
### Non-Interactive Examples
@@ -168,6 +170,19 @@ sudo slipgate tunnel add \
168170
--email admin@example.com \
169171
--decoy-url https://www.wikipedia.org
170172

173+
# StunTLS tunnel (SSH over TLS + WebSocket)
174+
sudo slipgate tunnel add \
175+
--transport stuntls \
176+
--tag mytls
177+
178+
# External DNS routing (forward queries to a custom port)
179+
sudo slipgate tunnel add \
180+
--transport external \
181+
--tag my-proto \
182+
--domain j.example.com
183+
# → prompts for target UDP port (e.g. 5301)
184+
# Queries for j.example.com route to 127.0.0.1:5301
185+
171186
# Direct SSH / SOCKS5 transports
172187
sudo slipgate tunnel add --transport direct-ssh --tag myssh
173188
sudo slipgate tunnel add --transport direct-socks5 --tag mysocks
@@ -202,7 +217,7 @@ sudo slipgate tunnel share mydnstt
202217
│ │
203218
└────────┬─────────┘
204219
205-
DNS :53/udp ──────┼────── HTTPS :443/tcp
220+
DNS :53/udp ──────┼────── HTTPS/TLS :443/tcp
206221
│ │ │
207222
┌───────────────────┼───────────┼───────────┼──────────────────┐
208223
│ SERVER v │ v │
@@ -211,12 +226,13 @@ sudo slipgate tunnel share mydnstt
211226
│ │ DNS Router │ │ │ NaiveProxy │ │
212227
│ │ domain-based dispatch │ │ │ Caddy + Auto-TLS │ │
213228
│ │ single / multi mode │ │ │ + decoy website │ │
214-
│ └──┬────────┬────────┬───┘ │ └───────────┬───────────┘ │
215-
│ │ │ │ │ │ │
216-
│ v v v │ │ │
217-
│ ┌──────┐┌────────┐┌──────┐ │ │ │
218-
│ │DNSTT ││Slip- ││VayDNS│ │ │ │
219-
│ │NoizDN││stream ││ │ │ │ │
229+
│ │ + external routing │ │ └───────────┬───────────┘ │
230+
│ └──┬────────┬────────┬───┘ │ │ │
231+
│ │ │ │ │ ┌───────────────────────┐ │
232+
│ v v v │ │ StunTLS │ │
233+
│ ┌──────┐┌────────┐┌──────┐ │ │ SSH over TLS + WS │ │
234+
│ │DNSTT ││Slip- ││VayDNS│ │ │ self-signed cert │ │
235+
│ │NoizDN││stream ││ │ │ └───────────┬───────────┘ │
220236
│ │──────││────────││──────│ │ │ │
221237
│ │DNS ││QUIC ││KCP │ │ │ │
222238
│ │Curve ││TLS cert││Curve │ │ │ │
@@ -255,6 +271,8 @@ sudo slipgate tunnel share mydnstt
255271
| **Slipstream** | QUIC DNS | 53/udp | QUIC-based tunnel with certificate authentication |
256272
| **VayDNS** | KCP DNS | 53/udp | KCP-based DNS tunnel with Curve25519 encryption. Supports configurable idle timeout, keepalive, queue size, and multiple DNS record types |
257273
| **NaiveProxy** | HTTPS | 443/tcp | Caddy with forwardproxy plugin. Auto-TLS via Let's Encrypt. Probe-resistant with decoy site |
274+
| **StunTLS** | TLS/WSS | 443/tcp | SSH over TLS + WebSocket proxy. Auto-detects WebSocket, HTTP CONNECT, raw TLS, and payload (DPI bypass) modes. Self-signed TLS cert, no domain required |
275+
| **External** | DNS | 53/udp | Routes DNS queries for a domain to a user-specified UDP port. No managed service — use for custom/private protocol testing |
258276

259277
### Domain Layout
260278

cmd/socks.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package cmd
22

33
import (
4+
"strings"
5+
46
"github.com/anonvector/slipgate/internal/proxy"
57
"github.com/spf13/cobra"
68
)
@@ -17,17 +19,35 @@ func init() {
1719
RunE: func(cmd *cobra.Command, args []string) error {
1820
addr, _ := cmd.Flags().GetString("addr")
1921
port, _ := cmd.Flags().GetInt("port")
20-
user, _ := cmd.Flags().GetString("user")
21-
pass, _ := cmd.Flags().GetString("pass")
22-
return proxy.Serve(addr, port, user, pass)
22+
credsList, _ := cmd.Flags().GetStringArray("creds")
23+
24+
// Parse --creds user:pass pairs
25+
creds := make(map[string]string)
26+
for _, c := range credsList {
27+
if i := strings.IndexByte(c, ':'); i > 0 {
28+
creds[c[:i]] = c[i+1:]
29+
}
30+
}
31+
32+
// Fall back to legacy --user/--pass for single-user compat
33+
if len(creds) == 0 {
34+
user, _ := cmd.Flags().GetString("user")
35+
pass, _ := cmd.Flags().GetString("pass")
36+
if user != "" {
37+
creds[user] = pass
38+
}
39+
}
40+
41+
return proxy.ServeMulti(addr, port, creds)
2342
},
2443
SilenceUsage: true,
2544
SilenceErrors: true,
2645
}
2746
serveCmd.Flags().String("addr", "127.0.0.1", "Listen address")
2847
serveCmd.Flags().Int("port", 1080, "Listen port")
29-
serveCmd.Flags().String("user", "", "Username for auth")
30-
serveCmd.Flags().String("pass", "", "Password for auth")
48+
serveCmd.Flags().String("user", "", "Username for auth (single user, legacy)")
49+
serveCmd.Flags().String("pass", "", "Password for auth (single user, legacy)")
50+
serveCmd.Flags().StringArray("creds", nil, "Credentials as user:pass (repeatable)")
3151

3252
socksCmd.AddCommand(serveCmd)
3353
rootCmd.AddCommand(socksCmd)

cmd/stuntls.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package cmd
2+
3+
import (
4+
"github.com/anonvector/slipgate/internal/proxy"
5+
"github.com/spf13/cobra"
6+
)
7+
8+
func init() {
9+
stuntlsCmd := &cobra.Command{
10+
Use: "stuntls",
11+
Short: "Built-in TLS + WebSocket SSH proxy",
12+
}
13+
14+
serveCmd := &cobra.Command{
15+
Use: "serve",
16+
Short: "Start the TLS/WebSocket SSH proxy (used by systemd)",
17+
RunE: func(cmd *cobra.Command, args []string) error {
18+
addr, _ := cmd.Flags().GetString("addr")
19+
port, _ := cmd.Flags().GetInt("port")
20+
sshAddr, _ := cmd.Flags().GetString("ssh")
21+
cert, _ := cmd.Flags().GetString("cert")
22+
key, _ := cmd.Flags().GetString("key")
23+
return proxy.ServeStunTLS(addr, port, sshAddr, cert, key)
24+
},
25+
SilenceUsage: true,
26+
SilenceErrors: true,
27+
}
28+
serveCmd.Flags().String("addr", "0.0.0.0", "Listen address")
29+
serveCmd.Flags().Int("port", 443, "Listen port")
30+
serveCmd.Flags().String("ssh", "127.0.0.1:22", "SSH backend address")
31+
serveCmd.Flags().String("cert", "", "TLS certificate file")
32+
serveCmd.Flags().String("key", "", "TLS private key file")
33+
34+
stuntlsCmd.AddCommand(serveCmd)
35+
rootCmd.AddCommand(stuntlsCmd)
36+
}

install.sh

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,9 @@ OS=$(uname -s | tr '[:upper:]' '[:lower:]')
3030

3131
BINARY="slipgate-${OS}-${ARCH}"
3232

33-
# When fetched from the dev branch, find the latest dev-* pre-release;
34-
# otherwise use the latest stable release.
35-
# Can also be overridden: SLIPGATE_RELEASE_TAG=dev-abc1234 bash install.sh
33+
# Override with: SLIPGATE_RELEASE_TAG=v1.5.1 bash install.sh
3634
RELEASE_TAG="${SLIPGATE_RELEASE_TAG:-}"
37-
CHANNEL="dev" # ← set to "dev" on dev branch, empty on main
38-
if [[ -z "$RELEASE_TAG" && "$CHANNEL" == "dev" ]]; then
39-
{
40-
RELEASE_TAG=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases" \
41-
| grep -o '"tag_name": *"dev-[^"]*"' | head -1 | cut -d'"' -f4 || true)
42-
}
43-
fi
35+
CHANNEL="" # ← set to "dev" on dev branch, empty on main
4436

4537
if [[ -n "$RELEASE_TAG" ]]; then
4638
URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${BINARY}"

internal/actions/options.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,27 @@ package actions
22

33
// Shared select options used across multiple actions.
44

5+
// TransportOptions is the full list shown in `tunnel add`.
56
var TransportOptions = []SelectOption{
67
{Value: "dnstt", Label: "DNSTT / NoizDNS — DNS tunnel"},
78
{Value: "slipstream", Label: "Slipstream — QUIC DNS tunnel"},
89
{Value: "vaydns", Label: "VayDNS — KCP DNS tunnel"},
910
{Value: "naive", Label: "NaiveProxy — HTTPS proxy with Caddy"},
11+
{Value: "stuntls", Label: "StunTLS — SSH over TLS + WebSocket proxy"},
12+
{Value: "external", Label: "External — Route DNS to a custom port"},
13+
{Value: "direct-ssh", Label: "SSH — Direct SSH tunnel"},
14+
{Value: "direct-socks5", Label: "SOCKS5 — Direct SOCKS5 proxy"},
15+
}
16+
17+
// InstallTransportOptions is the subset shown in the install/wizard flows.
18+
// External is excluded because it routes to a user-managed service that
19+
// wouldn't exist yet during initial setup.
20+
var InstallTransportOptions = []SelectOption{
21+
{Value: "dnstt", Label: "DNSTT / NoizDNS — DNS tunnel"},
22+
{Value: "slipstream", Label: "Slipstream — QUIC DNS tunnel"},
23+
{Value: "vaydns", Label: "VayDNS — KCP DNS tunnel"},
24+
{Value: "naive", Label: "NaiveProxy — HTTPS proxy with Caddy"},
25+
{Value: "stuntls", Label: "StunTLS — SSH over TLS + WebSocket proxy"},
1026
{Value: "direct-ssh", Label: "SSH — Direct SSH tunnel"},
1127
{Value: "direct-socks5", Label: "SOCKS5 — Direct SOCKS5 proxy"},
1228
}

internal/clientcfg/fields.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,30 @@ const (
4646
FNoizDNSStealth = 38 // "0" or "1" (v18)
4747
FDNSPayloadSize = 39 // integer (v18)
4848
FSOCKS5ServerPort = 40 // integer, default 1080 (v18)
49-
// v19: VayDNS fields
49+
// v19: VayDNS fields (41-44)
5050
FVayDNSDnsttCompat = 41
5151
FVayDNSRecordType = 42
5252
FVayDNSMaxQnameLen = 43
5353
FVayDNSRps = 44
54-
// v20: VayDNS advanced
54+
// v20: VayDNS advanced (45-49)
5555
FVayDNSIdleTimeout = 45
5656
FVayDNSKeepalive = 46
5757
FVayDNSUdpTimeout = 47
5858
FVayDNSMaxNumLabels = 48
5959
FVayDNSClientIdSize = 49
60-
TotalFields = 50
60+
// v21: SSH over TLS + HTTP proxy + WebSocket (50-58)
61+
FSSHTlsEnabled = 50
62+
FSSHTlsSni = 51
63+
FSSHHttpProxyHost = 52
64+
FSSHHttpProxyPort = 53
65+
FSSHHttpProxyCustomHost = 54
66+
FSSHWsEnabled = 55
67+
FSSHWsPath = 56
68+
FSSHWsUseTls = 57
69+
FSSHWsCustomHost = 58
70+
// v22: SSH payload (raw prefix for DPI bypass)
71+
FSSHPayload = 59
72+
TotalFields = 60
6173
)
6274

6375
// Client modes for DNSTT transport (server is the same, client behavior differs).
@@ -96,6 +108,11 @@ var TunnelTypeMap = map[string]map[string]map[string]string{
96108
config.BackendSSH: "naive_ssh",
97109
},
98110
},
111+
config.TransportStunTLS: {
112+
"": {
113+
config.BackendSSH: "ssh",
114+
},
115+
},
99116
config.TransportSSH: {
100117
"": {
101118
config.BackendSSH: "ssh",

internal/clientcfg/generate.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func GenerateURI(tunnel *config.TunnelConfig, backend *config.BackendConfig, cfg
3030
var fields [TotalFields]string
3131

3232
// Version and type
33-
fields[FVersion] = "20"
33+
fields[FVersion] = "22"
3434
fields[FTunnelType] = GetTunnelType(tunnel.Transport, tunnel.Backend, opts.ClientMode)
3535

3636
name := tunnel.Tag
@@ -78,6 +78,14 @@ func GenerateURI(tunnel *config.TunnelConfig, backend *config.BackendConfig, cfg
7878
fields[FVayDNSUdpTimeout] = "0"
7979
fields[FVayDNSMaxNumLabels] = "0"
8080
fields[FVayDNSClientIdSize] = "0"
81+
// v21 defaults
82+
fields[FSSHTlsEnabled] = "0"
83+
fields[FSSHWsEnabled] = "0"
84+
fields[FSSHWsPath] = "/"
85+
fields[FSSHWsUseTls] = "1"
86+
fields[FSSHHttpProxyPort] = "8080"
87+
// v22 defaults
88+
fields[FSSHPayload] = b64("")
8189

8290
// Transport-specific
8391
switch tunnel.Transport {
@@ -99,8 +107,6 @@ func GenerateURI(tunnel *config.TunnelConfig, backend *config.BackendConfig, cfg
99107
} else {
100108
fields[FVayDNSRecordType] = "txt"
101109
}
102-
fields[FVayDNSMaxQnameLen] = "101"
103-
fields[FVayDNSRps] = "0"
104110
if tunnel.VayDNS.IdleTimeout != "" {
105111
fields[FVayDNSIdleTimeout] = durationToSeconds(tunnel.VayDNS.ResolvedIdleTimeout())
106112
}
@@ -127,6 +133,19 @@ func GenerateURI(tunnel *config.TunnelConfig, backend *config.BackendConfig, cfg
127133
}
128134
}
129135

136+
case config.TransportStunTLS:
137+
if tunnel.StunTLS != nil {
138+
// StunTLS server accepts raw TLS, WebSocket, HTTP CONNECT, and payload.
139+
// Default to WebSocket (most compatible with CDNs and restrictive firewalls).
140+
// Only set WebSocket fields — don't also set sshTlsEnabled, which is
141+
// a Direct-mode flag and would be dead weight.
142+
fields[FDomain] = getServerIP()
143+
fields[FSSHPort] = fmt.Sprintf("%d", tunnel.StunTLS.Port)
144+
fields[FSSHWsEnabled] = "1"
145+
fields[FSSHWsUseTls] = "1"
146+
fields[FSSHWsPath] = "/"
147+
}
148+
130149
case config.TransportSSH, config.TransportSOCKS:
131150
// Direct transports have no domain — use server IP
132151
fields[FDomain] = getServerIP()
@@ -164,7 +183,6 @@ func GenerateURI(tunnel *config.TunnelConfig, backend *config.BackendConfig, cfg
164183
return Encode(fields), nil
165184
}
166185

167-
// durationToSeconds converts a Go duration string (e.g. "10s", "2m") to seconds string.
168186
func durationToSeconds(d string) string {
169187
if d == "" {
170188
return "0"

internal/config/tunnel.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ const (
66
TransportSlipstream = "slipstream"
77
TransportVayDNS = "vaydns"
88
TransportNaive = "naive"
9+
TransportStunTLS = "stuntls"
910
TransportSSH = "direct-ssh"
1011
TransportSOCKS = "direct-socks5"
12+
TransportExternal = "external"
1113
)
1214

1315
// TunnelConfig defines a single tunnel.
@@ -24,6 +26,7 @@ type TunnelConfig struct {
2426
Slipstream *SlipstreamConfig `json:"slipstream,omitempty"`
2527
VayDNS *VayDNSConfig `json:"vaydns,omitempty"`
2628
Naive *NaiveConfig `json:"naive,omitempty"`
29+
StunTLS *StunTLSConfig `json:"stuntls,omitempty"`
2730
}
2831

2932
// DNSTTConfig holds config for DNSTT transport (serves both DNSTT and NoizDNS clients).
@@ -109,15 +112,29 @@ func (v *VayDNSConfig) ResolvedClientIDSize() int {
109112
return v.ClientIDSize
110113
}
111114

115+
// StunTLSConfig holds config for the TLS + WebSocket SSH proxy transport.
116+
// Accepts both raw TLS connections (stunnel-style) and WebSocket upgrades,
117+
// forwarding traffic to the SSH daemon.
118+
type StunTLSConfig struct {
119+
Cert string `json:"cert"` // path to TLS certificate
120+
Key string `json:"key"` // path to TLS private key
121+
Port int `json:"port"` // listen port (typically 443)
122+
}
123+
112124
// IsDNSTunnel returns true if the transport uses DNS port 53.
113125
func (t *TunnelConfig) IsDNSTunnel() bool {
114126
switch t.Transport {
115-
case TransportDNSTT, TransportSlipstream, TransportVayDNS:
127+
case TransportDNSTT, TransportSlipstream, TransportVayDNS, TransportExternal:
116128
return true
117129
}
118130
return false
119131
}
120132

133+
// HasManagedService returns true if slipgate manages a systemd service for this tunnel.
134+
func (t *TunnelConfig) HasManagedService() bool {
135+
return t.IsDNSTunnel() && t.Transport != TransportExternal
136+
}
137+
121138
// IsDirectTransport returns true for transports that expose a service directly (no tunnel).
122139
func (t *TunnelConfig) IsDirectTransport() bool {
123140
switch t.Transport {

0 commit comments

Comments
 (0)