Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,17 @@ cp scripts/e2e-config.json.example scripts/e2e-config.json
"shadowsocks": {
"multi": { "method": "aes-256-gcm", "password": "..." },
"single": { "method": "chacha20-ietf-poly1305", "password": "..." }
},
"socks_auth": {
"user": "your-socks-user",
"password": "your-socks-password"
}
}
```

- `ssh_target`: SSH config alias or `user@host` (required)
- `dns_resolver`: public DNS resolver for client connections (default: `8.8.8.8`)
- `socks_auth`: SOCKS5 authentication credentials for auth tests (optional, enables auth tests in `single` and `config-reload` phases)

### Usage

Expand All @@ -206,11 +211,11 @@ cp scripts/e2e-config.json.example scripts/e2e-config.json

| Phase | What it tests |
|-------|---------------|
| `single` | Fresh install, `tunnel add` for all 5 types, each tested individually in single mode |
| `single` | Fresh install, `tunnel add` for all 5 types, each tested individually in single mode, SOCKS auth enable/disable with both transports |
| `multi` | Setup multi-mode state via `config load`, test 3 tunnels simultaneously |
| `mode-switch` | Switch multi→single→multi, verify tunnels work after each switch |
| `config-load` | Clean reinstall, `config load` with multi config, verify tunnels work |
| `config-reload` | `config load` single config over existing multi, validate old resources cleaned up |
| `config-reload` | `config load` single config (with SOCKS auth) over existing multi, validate cleanup and auth enforcement |

Each phase is standalone — it sets up its own prerequisite state and can be run independently via `--phase`.

Expand Down
73 changes: 73 additions & 0 deletions internal/actions/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,52 @@ func init() {
},
})

// Register backend.auth action
Register(&Action{
ID: ActionBackendAuth,
Parent: ActionBackend,
Use: "auth",
Short: "Configure SOCKS5 authentication",
Long: "Enable, disable, or change SOCKS5 proxy authentication credentials",
MenuLabel: "Authentication",
RequiresRoot: true,
RequiresInstalled: true,
Args: &ArgsSpec{
Name: "tag",
Description: "Backend tag",
Required: true,
PickerFunc: SocksBackendPicker,
},
Inputs: []InputField{
{
Name: "disable",
Label: "Disable authentication",
Type: InputTypeBool,
Description: "Disable SOCKS5 authentication",
},
{
Name: "user",
Label: "Username",
ShortFlag: 'u',
Type: InputTypeText,
Description: "SOCKS5 username",
ShowIf: func(ctx *Context) bool {
return !ctx.GetBool("disable")
},
},
{
Name: "password",
Label: "Password",
ShortFlag: 'p',
Type: InputTypePassword,
Description: "SOCKS5 password",
ShowIf: func(ctx *Context) bool {
return !ctx.GetBool("disable")
},
},
},
})

// Register backend.remove action
Register(&Action{
ID: ActionBackendRemove,
Expand Down Expand Up @@ -182,6 +228,33 @@ func BackendPicker(ctx *Context) (string, error) {
return "", nil
}

// SocksBackendPicker provides interactive selection filtered to SOCKS backends only.
func SocksBackendPicker(ctx *Context) (string, error) {
cfg, err := config.Load()
if err != nil {
return "", err
}

var options []SelectOption
for _, b := range cfg.Backends {
if b.Type != config.BackendSOCKS {
continue
}
label := fmt.Sprintf("%s (SOCKS5)", b.Tag)
options = append(options, SelectOption{
Label: label,
Value: b.Tag,
})
}

if len(options) == 0 {
return "", fmt.Errorf("no SOCKS backends configured")
}

ctx.Set("_picker_options", options)
return "", nil
}

// BackendTypeOptions returns the available backend type options for adding new backends.
// Note: SOCKS and SSH are built-in backends and cannot be added manually.
func BackendTypeOptions() []SelectOption {
Expand Down
1 change: 1 addition & 0 deletions internal/actions/ids.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const (
ActionBackendAdd = "backend.add"
ActionBackendRemove = "backend.remove"
ActionBackendStatus = "backend.status"
ActionBackendAuth = "backend.auth"

// Tunnel actions
ActionTunnel = "tunnel"
Expand Down
5 changes: 4 additions & 1 deletion internal/clientcfg/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ func Generate(tunnel *config.TunnelConfig, backend *config.BackendConfig, opts G

switch backend.Type {
case config.BackendSOCKS:
// No extra fields needed
if backend.HasSocksAuth() {
cfg.Backend.User = backend.Socks.User
cfg.Backend.Password = backend.Socks.Password
}

case config.BackendSSH:
cfg.Backend.User = opts.User
Expand Down
12 changes: 12 additions & 0 deletions internal/config/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ type BackendConfig struct {
Type BackendType `json:"type"`
Address string `json:"address,omitempty"`
Shadowsocks *ShadowsocksConfig `json:"shadowsocks,omitempty"`
Socks *SocksConfig `json:"socks,omitempty"`
}

// SocksConfig holds SOCKS5 authentication configuration.
type SocksConfig struct {
User string `json:"user"`
Password string `json:"password"`
}

// ShadowsocksConfig holds Shadowsocks-specific configuration.
Expand All @@ -26,6 +33,11 @@ type ShadowsocksConfig struct {
Password string `json:"password"`
}

// HasSocksAuth returns true if SOCKS5 authentication is configured.
func (b *BackendConfig) HasSocksAuth() bool {
return b.Socks != nil && b.Socks.User != "" && b.Socks.Password != ""
}

// IsManaged returns true if dnstm manages this backend type.
func (b *BackendConfig) IsManaged() bool {
switch b.Type {
Expand Down
5 changes: 5 additions & 0 deletions internal/config/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ func (c *Config) validateBackends() error {
if b.Address == "" {
return fmt.Errorf("backend '%s': address is required for type %s", b.Tag, b.Type)
}
if b.Type == BackendSOCKS && b.Socks != nil {
if b.Socks.User == "" || b.Socks.Password == "" {
return fmt.Errorf("backend '%s': socks auth requires both user and password", b.Tag)
}
}
case BackendShadowsocks:
if b.Shadowsocks == nil {
return fmt.Errorf("backend '%s': shadowsocks config is required for type %s", b.Tag, b.Type)
Expand Down
73 changes: 73 additions & 0 deletions internal/handlers/backend_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package handlers

import (
"fmt"

"github.com/net2share/dnstm/internal/actions"
"github.com/net2share/dnstm/internal/config"
"github.com/net2share/dnstm/internal/proxy"
)

func init() {
actions.SetBackendHandler(actions.ActionBackendAuth, HandleBackendAuth)
}

// HandleBackendAuth enables, disables, or changes SOCKS5 authentication.
func HandleBackendAuth(ctx *actions.Context) error {
cfg, err := RequireConfig(ctx)
if err != nil {
return err
}

tag, err := RequireTag(ctx, "backend")
if err != nil {
return err
}

backend := cfg.GetBackendByTag(tag)
if backend == nil {
return actions.BackendNotFoundError(tag)
}

if backend.Type != config.BackendSOCKS {
return fmt.Errorf("backend '%s' is not a SOCKS backend", tag)
}

disable := ctx.GetBool("disable")

if disable {
backend.Socks = nil
if err := cfg.Save(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}

if err := proxy.ReconfigureMicrosocks(cfg.Proxy.Port, "", ""); err != nil {
return fmt.Errorf("failed to reconfigure microsocks: %w", err)
}

ctx.Output.Success("SOCKS5 authentication disabled")
return nil
}

user := ctx.GetString("user")
password := ctx.GetString("password")

if user == "" || password == "" {
return fmt.Errorf("both user and password are required to enable authentication")
}

backend.Socks = &config.SocksConfig{
User: user,
Password: password,
}
if err := cfg.Save(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}

if err := proxy.ReconfigureMicrosocks(cfg.Proxy.Port, user, password); err != nil {
return fmt.Errorf("failed to reconfigure microsocks: %w", err)
}

ctx.Output.Success(fmt.Sprintf("SOCKS5 authentication enabled (user: %s)", user))
return nil
}
31 changes: 31 additions & 0 deletions internal/handlers/backend_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,25 @@ func HandleBackendStatus(ctx *actions.Context) error {
}
infoCfg.Sections = append(infoCfg.Sections, mainSection)

// Show SOCKS5 auth config if applicable
if backend.Type == config.BackendSOCKS {
authSection := actions.InfoSection{
Title: "Authentication",
}
if backend.HasSocksAuth() {
authSection.Rows = []actions.InfoRow{
{Key: "Status", Value: "Enabled"},
{Key: "User", Value: backend.Socks.User},
{Key: "Password", Value: backend.Socks.Password},
}
} else {
authSection.Rows = []actions.InfoRow{
{Key: "Status", Value: "Disabled"},
}
}
infoCfg.Sections = append(infoCfg.Sections, authSection)
}

// Show shadowsocks config if applicable
if backend.Shadowsocks != nil {
ssSection := actions.InfoSection{
Expand Down Expand Up @@ -94,6 +113,18 @@ func HandleBackendStatus(ctx *actions.Context) error {
ctx.Output.KV("Removable", fmt.Sprintf("%v", !backend.IsBuiltIn() || (tag != "socks" && tag != "ssh"))),
})

if backend.Type == config.BackendSOCKS {
ctx.Output.Println()
ctx.Output.Println("Authentication:")
if backend.HasSocksAuth() {
ctx.Output.Printf(" Status: Enabled\n")
ctx.Output.Printf(" User: %s\n", backend.Socks.User)
ctx.Output.Printf(" Password: %s\n", backend.Socks.Password)
} else {
ctx.Output.Printf(" Status: Disabled\n")
}
}

if backend.Shadowsocks != nil {
ctx.Output.Println()
ctx.Output.Println("Shadowsocks Configuration:")
Expand Down
17 changes: 13 additions & 4 deletions internal/handlers/config_load.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,24 @@ func HandleConfigLoad(ctx *actions.Context) error {

ctx.Output.Status("Configuration saved to " + config.GetConfigPath())

// Reconfigure microsocks if the proxy port was explicitly specified
if newCfg.Proxy.Port != 0 && proxy.IsMicrosocksInstalled() {
if err := proxy.ConfigureMicrosocks(newCfg.Proxy.Port); err != nil {
// Reconfigure microsocks with port and auth from loaded config
if proxy.IsMicrosocksInstalled() {
port := newCfg.Proxy.Port
if port == 0 {
port = 1080
}
var socksUser, socksPass string
if socksBackend := newCfg.GetBackendByTag("socks"); socksBackend != nil && socksBackend.HasSocksAuth() {
socksUser = socksBackend.Socks.User
socksPass = socksBackend.Socks.Password
}
if err := proxy.ConfigureMicrosocksWithAuth(port, socksUser, socksPass); err != nil {
ctx.Output.Warning(fmt.Sprintf("Failed to reconfigure microsocks: %v", err))
} else {
if err := proxy.RestartMicrosocks(); err != nil {
ctx.Output.Warning(fmt.Sprintf("Failed to restart microsocks: %v", err))
} else {
ctx.Output.Status(fmt.Sprintf("Microsocks reconfigured on port %d", newCfg.Proxy.Port))
ctx.Output.Status(fmt.Sprintf("Microsocks reconfigured on port %d", port))
}
}
}
Expand Down
8 changes: 7 additions & 1 deletion internal/handlers/system_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,13 @@ func HandleInstall(ctx *actions.Context) error {
if err := cfg.Save(); err != nil {
ctx.Output.Warning("Failed to save proxy port: " + err.Error())
}
if err := proxy.ConfigureMicrosocks(port); err != nil {
// Preserve existing auth config on reinstall
var socksUser, socksPass string
if socksBackend := cfg.GetBackendByTag("socks"); socksBackend != nil && socksBackend.HasSocksAuth() {
socksUser = socksBackend.Socks.User
socksPass = socksBackend.Socks.Password
}
if err := proxy.ConfigureMicrosocksWithAuth(port, socksUser, socksPass); err != nil {
ctx.Output.Warning("microsocks service config: " + err.Error())
} else {
if err := proxy.StartMicrosocks(); err != nil {
Expand Down
22 changes: 22 additions & 0 deletions internal/handlers/tunnel_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@ func HandleTunnelStatus(ctx *actions.Context) error {
Key: "Address", Value: backend.Address,
})
}
if backend.Type == config.BackendSOCKS {
if backend.HasSocksAuth() {
backendSection.Rows = append(backendSection.Rows,
actions.InfoRow{Key: "Auth", Value: "Enabled"},
actions.InfoRow{Key: "User", Value: backend.Socks.User},
actions.InfoRow{Key: "Password", Value: backend.Socks.Password},
)
} else {
backendSection.Rows = append(backendSection.Rows,
actions.InfoRow{Key: "Auth", Value: "Disabled"},
)
}
}
infoCfg.Sections = append(infoCfg.Sections, backendSection)
}
}
Expand Down Expand Up @@ -145,6 +158,15 @@ func HandleTunnelStatus(ctx *actions.Context) error {
if backend.Address != "" {
ctx.Output.Printf(" Address: %s\n", backend.Address)
}
if backend.Type == config.BackendSOCKS {
if backend.HasSocksAuth() {
ctx.Output.Printf(" Auth: Enabled\n")
ctx.Output.Printf(" User: %s\n", backend.Socks.User)
ctx.Output.Printf(" Password: %s\n", backend.Socks.Password)
} else {
ctx.Output.Printf(" Auth: Disabled\n")
}
}
ctx.Output.Println()
}
}
Expand Down
Loading
Loading