diff --git a/README.md b/README.md index e02535af..8675ce0d 100644 --- a/README.md +++ b/README.md @@ -83,14 +83,27 @@ resolve `latest` by default; set `LIBXRAY_GOMOBILE_VERSION` to select a Go modul version. Both `gomobile` and `gobind` use that resolved version. Linux and Windows builds also produce `bin/xray` or `bin/xray.exe`. This -session Core protects Go DNS lookups from the VPN route and accepts only: +session Core can protect Go DNS lookups from the VPN route and accepts: ```shell -xray run -dns -interface -config [-error-file ] +xray run -config [-set-dns=true|false] [-dns -interface ] [-error-file ] ``` -The `-dns`, `-interface`, and `-config` options are required. `-dns` must be an IP endpoint, and `-config` -points directly to the Xray JSON configuration. +The `-config` option is always required and points directly to the Xray JSON +configuration. `-set-dns` defaults to `true`, preserving existing behavior: +`-dns` and `-interface` are required, `-dns` must be an IP endpoint, and the +process resolver is restored after Core shutdown or a startup failure. + +Use `-set-dns=false` to skip both `SetDNS` and `ResetDNS` and leave resolver +management to the Core. In this mode, `-dns` and `-interface` may be omitted; +if supplied, they are ignored. Use the equals-sign form for this boolean flag: + +```shell +xray run -set-dns=false -config xray.json +``` + +This flag controls libXray's process-resolver override only; Core DNS, TUN, +outbound-interface and WFP settings continue to come from the JSON configuration. The optional `-error-file` also writes command failures to a UTF-8 file before exiting, preserving the same error printed to stderr. The file is cleared before diff --git a/desktop_bin/command.go b/desktop_bin/command.go index 6ec76cd5..8cf06959 100644 --- a/desktop_bin/command.go +++ b/desktop_bin/command.go @@ -11,6 +11,7 @@ import ( ) type runOptions struct { + setDNS bool dns string interfaceName string configPath string @@ -25,6 +26,7 @@ func parseRunOptions(args []string) (runOptions, error) { flags := flag.NewFlagSet("run", flag.ContinueOnError) flags.SetOutput(io.Discard) + flags.BoolVar(&options.setDNS, "set-dns", true, "override the process DNS resolver") flags.StringVar(&options.dns, "dns", "", "DNS server IP endpoint") flags.StringVar(&options.interfaceName, "interface", "", "outbound network interface") flags.StringVar(&options.configPath, "config", "", "Xray JSON configuration path") @@ -35,15 +37,19 @@ func parseRunOptions(args []string) (runOptions, error) { if flags.NArg() != 0 { return options, errors.New("unexpected positional arguments") } - if options.dns == "" || options.interfaceName == "" || options.configPath == "" { - return options, errors.New("dns, interface, and config are required") + if options.configPath == "" { + return options, errors.New("config is required") + } + if options.setDNS && (options.dns == "" || options.interfaceName == "") { + return options, errors.New("dns and interface are required when set-dns is enabled") } return options, nil } func execute(args []string, run func(runOptions) error, stdout, stderr io.Writer) int { usage := func() { - fmt.Fprintln(stdout, "Usage: xray run -dns -interface -config [-error-file ]") + fmt.Fprintln(stdout, "Usage: xray run -config [-set-dns=true|false] [-dns -interface ] [-error-file ]") + fmt.Fprintln(stdout, "SetDNS is enabled by default; -dns and -interface are required unless -set-dns=false.") } if len(args) == 1 && (args[0] == "-h" || args[0] == "--help") { usage() diff --git a/desktop_bin/command_test.go b/desktop_bin/command_test.go index adf2e8cf..171d095a 100644 --- a/desktop_bin/command_test.go +++ b/desktop_bin/command_test.go @@ -8,6 +8,8 @@ import ( "os" "path/filepath" "testing" + + "github.com/xtls/libxray/xray" ) func TestParseRunOptions(t *testing.T) { @@ -20,7 +22,7 @@ func TestParseRunOptions(t *testing.T) { if err != nil { t.Fatal(err) } - if options.dns != "8.8.8.8:53" || options.interfaceName != "Ethernet" || options.configPath != `C:\run\xray.json` { + if !options.setDNS || options.dns != "8.8.8.8:53" || options.interfaceName != "Ethernet" || options.configPath != `C:\run\xray.json` { t.Fatalf("unexpected options: %#v", options) } @@ -29,6 +31,86 @@ func TestParseRunOptions(t *testing.T) { } } +func TestParseRunOptionsSetDNS(t *testing.T) { + for _, test := range []struct { + name string + args []string + want bool + }{ + { + name: "explicitly enabled", + args: []string{"run", "-set-dns=true", "-dns", "8.8.8.8:53", "-interface", "Ethernet", "-config", "xray.json"}, + want: true, + }, + { + name: "disabled without DNS options", + args: []string{"run", "-set-dns=false", "-config", "xray.json"}, + }, + { + name: "disabled with unused DNS options", + args: []string{"run", "-set-dns=false", "-dns", "unused", "-interface", "unused", "-config", "xray.json"}, + }, + } { + t.Run(test.name, func(t *testing.T) { + options, err := parseRunOptions(test.args) + if err != nil { + t.Fatal(err) + } + if options.setDNS != test.want || options.configPath != "xray.json" { + t.Fatalf("unexpected options: %#v", options) + } + }) + } +} + +func TestParseRunOptionsRejectsInvalidSetDNSOptions(t *testing.T) { + for _, test := range []struct { + name string + args []string + }{ + {"enabled without DNS", []string{"run", "-set-dns=true", "-interface", "Ethernet", "-config", "xray.json"}}, + {"enabled without interface", []string{"run", "-set-dns=true", "-dns", "8.8.8.8:53", "-config", "xray.json"}}, + {"disabled without config", []string{"run", "-set-dns=false"}}, + {"invalid boolean", []string{"run", "-set-dns=invalid", "-config", "xray.json"}}, + {"separated boolean value", []string{"run", "-set-dns", "false", "-config", "xray.json"}}, + } { + t.Run(test.name, func(t *testing.T) { + if _, err := parseRunOptions(test.args); err == nil { + t.Fatal("invalid options were accepted") + } + }) + } +} + +func TestCommandWithSetDNSDisabledReachesConfigParsing(t *testing.T) { + // Malformed JSON fails before constructing or starting a Core/TUN instance. + // Matching the real parser error proves DNS setup did not fail first. + const invalidConfig = "{" + wantErr := xray.RunXray(invalidConfig) + if wantErr == nil { + t.Fatal("malformed JSON was accepted") + } + path := filepath.Join(commandTestDirectory(t), "xray.json") + if err := os.WriteFile(path, []byte(invalidConfig), 0600); err != nil { + t.Fatal(err) + } + for _, test := range []struct { + name string + args []string + }{ + {"without DNS options", []string{"run", "-set-dns=false", "-config", path}}, + {"with unused DNS options", []string{"run", "-set-dns=false", "-dns", "not-an-endpoint", "-interface", "unused-interface", "-config", path}}, + } { + t.Run(test.name, func(t *testing.T) { + var stdout, stderr bytes.Buffer + code := execute(test.args, run, &stdout, &stderr) + if code != 1 || stderr.String() != wantErr.Error()+"\n" { + t.Fatalf("expected configuration error, got code=%d stderr=%q", code, stderr.String()) + } + }) + } +} + func TestCommandReportsActualStartupError(t *testing.T) { directory := commandTestDirectory(t) path := filepath.Join(directory, "xray.json.error") diff --git a/desktop_bin/main.go b/desktop_bin/main.go index a6433ddc..d03bc5fb 100644 --- a/desktop_bin/main.go +++ b/desktop_bin/main.go @@ -16,10 +16,12 @@ func run(options runOptions) error { if err != nil { return err } - if err := dns.SetDNS(options.dns, options.interfaceName); err != nil { - return err + if options.setDNS { + if err := dns.SetDNS(options.dns, options.interfaceName); err != nil { + return err + } + defer dns.ResetDNS() } - defer dns.ResetDNS() if err := xray.RunXray(string(config)); err != nil { return err diff --git a/readme/README.zh_CN.md b/readme/README.zh_CN.md index f873fcab..4662643c 100644 --- a/readme/README.zh_CN.md +++ b/readme/README.zh_CN.md @@ -49,14 +49,25 @@ gomobile 默认解析 `latest`, `gobind` 使用同一个解析版本。 Linux 和 Windows 构建还会生成 `bin/xray` 或 `bin/xray.exe`。该会话 Core -会保护 Go DNS 查询不被 VPN 路由重新捕获,并且只接受以下命令: +可保护 Go DNS 查询不被 VPN 路由重新捕获,并接受以下命令: ```shell -xray run -dns -interface <网卡名> -config [-error-file <路径>] +xray run -config [-set-dns=true|false] [-dns -interface <网卡名>] [-error-file <路径>] ``` -`-dns`、`-interface`、`-config` 均必须提供。`-dns` 必须是 IP endpoint,`-config` 直接指向 Xray -JSON 配置。 +`-config` 始终必须提供,直接指向 Xray JSON 配置。`-set-dns` 默认为 `true`, +保留现有行为:必须提供 `-dns` 和 `-interface`,`-dns` 必须是 IP endpoint;Core +停止或启动失败后恢复原解析器。 + +使用 `-set-dns=false` 可同时跳过 `SetDNS` 和 `ResetDNS`,由 Core 管理解析器。 +此时可以省略 `-dns`、`-interface`,即使提供也不会使用。布尔参数使用等号传值: + +```shell +xray run -set-dns=false -config xray.json +``` + +该开关仅控制 libXray 的进程解析器覆盖;Core 的 DNS、TUN、出站网卡和 WFP +设置仍由 JSON 配置决定。 可选的 `-error-file` 将命令失败时打印到 stderr 的原始错误同时写入 UTF-8 文件, 随后退出。运行前清空文件,成功时保持为空;父目录必须已经存在。通过提权启动 Core