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
29 changes: 23 additions & 6 deletions internal/config/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net"
"os"
"os/user"
"strings"
"time"

"github.com/rs/zerolog"
Expand Down Expand Up @@ -91,10 +92,11 @@ func CreateCommand(
Flags: []cli.Flag{
&cli.StringFlag{
Name: "app-mode",
Usage: fmt.Sprintf(`<"http"|"socks5"|"tun">
Usage: fmt.Sprintf(`%s
Specifies the proxy mode.
Note that 'socks5' and 'tun' modes are currently experimental.
(default: %q)`,
enumUsage(availableAppModeValues),
defaultCfg.Startup.App.Mode.String(),
),
OnlyOnce: true,
Expand Down Expand Up @@ -178,9 +180,10 @@ func CreateCommand(

&cli.StringFlag{
Name: "dns-mode",
Usage: fmt.Sprintf(`<'udp'|'doh'|'sys'>
Usage: fmt.Sprintf(`%s
Default resolution mode for domains that do not match any specific rule.
(default: %q)`,
enumUsage(availableDNSModeValues),
defaultCfg.Runtime.DNS.Mode.String(),
),
Value: "udp",
Expand Down Expand Up @@ -216,9 +219,10 @@ func CreateCommand(

&cli.StringFlag{
Name: "dns-qtype",
Usage: fmt.Sprintf(`<"ipv4"|"ipv6"|"all">
Usage: fmt.Sprintf(`%s
Filters DNS queries by record type (A for IPv4, AAAA for IPv6).
(default: %q)`,
enumUsage(availableDNSQueryValues),
defaultCfg.Runtime.DNS.QType.String(),
),
Value: "ipv4",
Expand Down Expand Up @@ -307,8 +311,9 @@ func CreateCommand(

&cli.StringFlag{
Name: "https-split-mode",
Usage: fmt.Sprintf(`<"sni"|"random"|"chunk"|"sni"|"custom"|"none">
Usage: fmt.Sprintf(`%s
Specifies the default packet fragmentation strategy to use. (default: %q)`,
enumUsage(availableHTTPSModeValues),
defaultCfg.Runtime.HTTPS.SplitMode.String(),
),
Value: "chunk",
Expand Down Expand Up @@ -344,9 +349,9 @@ func CreateCommand(
Name: "https-chunk-size",
Usage: fmt.Sprintf(`
The chunk size (in bytes) for packet fragmentation. This value is only applied
when 'https-split-default' is 'chunk'. While setting the size to '0' internally
when 'https-split-mode' is 'chunk'. While setting the size to '0' internally
disables fragmentation (to avoid division-by-zero errors), you should set
'https-split-default' to 'none' to disable the feature cleanly.
'https-split-mode' to 'none' to disable the feature cleanly.
(default: %v, max: %v)`,
defaultCfg.Runtime.HTTPS.ChunkSize,
math.MaxUint8,
Expand Down Expand Up @@ -571,6 +576,18 @@ func CreateCommand(
return cmd
}

// enumUsage renders the accepted values of an enum flag as a usage hint,
// e.g. <"udp"|"https"|"system">. It is derived from the same slice the
// flag's validator checks against, so the help text cannot drift away from
// what the flag actually accepts.
func enumUsage(values []string) string {
quoted := make([]string, len(values))
for i, v := range values {
quoted[i] = fmt.Sprintf("%q", v)
}
return "<" + strings.Join(quoted, "|") + ">"
}

func determineRealHome() string {
sudoUser := os.Getenv("SUDO_USER")
if sudoUser != "" {
Expand Down
56 changes: 56 additions & 0 deletions internal/config/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import (
"net"
"os"
"path/filepath"
"slices"
"testing"
"time"

"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
)

func TestCreateCommand_Flags(t *testing.T) {
Expand Down Expand Up @@ -373,3 +375,57 @@ func TestLoad_RuleInheritsFromCLIAndTOML(t *testing.T) {
assert.False(t, rule.Config.HTTPS.Disorder,
"rule inherits disorder=false from package default")
}

// TestCreateCommand_EnumUsageMatchesValidator guards the enum flags' help text
// against drifting away from the values their validators actually accept.
// Regression test: --dns-mode advertised <'udp'|'doh'|'sys'> while only
// "udp", "https" and "system" were accepted, and --https-split-mode listed
// "sni" twice while omitting "first-byte".
func TestCreateCommand_EnumUsageMatchesValidator(t *testing.T) {
tcs := []struct {
flag string
values []string
}{
{flag: "app-mode", values: availableAppModeValues},
{flag: "dns-mode", values: availableDNSModeValues},
{flag: "dns-qtype", values: availableDNSQueryValues},
{flag: "https-split-mode", values: availableHTTPSModeValues},
}

runFunc := func(ctx context.Context, configDir string, cfg *Config) error {
return nil
}
cmd := CreateCommand(runFunc, "v0.0.0", "commit", "build")

usageOf := func(t *testing.T, name string) string {
t.Helper()
for _, f := range cmd.Flags {
if !slices.Contains(f.Names(), name) {
continue
}
df, ok := f.(cli.DocGenerationFlag)
require.True(t, ok, "flag --%s does not expose its usage", name)
return df.GetUsage()
}
t.Fatalf("flag --%s not found", name)
return ""
}

for _, tc := range tcs {
t.Run(tc.flag, func(t *testing.T) {
// The hint must list every accepted value, exactly once each,
// and advertise nothing the validator would reject.
assert.Contains(t, usageOf(t, tc.flag), enumUsage(tc.values))
})
}
}

// TestEnumUsage pins the rendering of the usage hint.
func TestEnumUsage(t *testing.T) {
assert.Equal(
t,
`<"udp"|"https"|"system">`,
enumUsage([]string{"udp", "https", "system"}),
)
assert.Equal(t, `<"only">`, enumUsage([]string{"only"}))
}
Loading