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
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <IP:port> -interface <name> -config <xray.json> [-error-file <path>]
xray run -config <xray.json> [-set-dns=true|false] [-dns <IP:port> -interface <name>] [-error-file <path>]
```

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
Expand Down
12 changes: 9 additions & 3 deletions desktop_bin/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
)

type runOptions struct {
setDNS bool
dns string
interfaceName string
configPath string
Expand All @@ -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")
Expand All @@ -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 <IP:port> -interface <name> -config <xray.json> [-error-file <path>]")
fmt.Fprintln(stdout, "Usage: xray run -config <xray.json> [-set-dns=true|false] [-dns <IP:port> -interface <name>] [-error-file <path>]")
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()
Expand Down
84 changes: 83 additions & 1 deletion desktop_bin/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"os"
"path/filepath"
"testing"

"github.com/xtls/libxray/xray"
)

func TestParseRunOptions(t *testing.T) {
Expand All @@ -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)
}

Expand All @@ -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")
Expand Down
8 changes: 5 additions & 3 deletions desktop_bin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 15 additions & 4 deletions readme/README.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <IP:port> -interface <网卡名> -config <xray.json> [-error-file <路径>]
xray run -config <xray.json> [-set-dns=true|false] [-dns <IP:port> -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
Expand Down