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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,26 @@ getXrayState

Used to solve the socket protect problem on Android.

### DNS resolver

Android may expose a loopback DNS server to Go's resolver while a VPN is
active. Call `SetDNS` before `runXray` to make Go use the DNS server selected by
the VPN configuration and protect the DNS socket from the VPN tunnel. The
server must be an IP endpoint with a port, such as `8.8.8.8:53` or
`[2001:4860:4860::8888]:53`.

Call `ResetDNS` after Xray has stopped. These APIs are available only in the
Android artifact and change the process-wide Go resolver.

```java
LibXray.setDNS(controller, "8.8.8.8:53");
LibXray.invoke(runXrayRequest);

// Later, when stopping the core:
LibXray.invoke(stopXrayRequest);
LibXray.resetDNS();
```

### Process finder (per-app routing)

`ConnectivityManager.getConnectionOwnerUid()` is API 30+. On older Android
Expand Down
23 changes: 22 additions & 1 deletion android_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,33 @@

package libXray

import c "github.com/xtls/libxray/controller"
import (
"errors"

c "github.com/xtls/libxray/controller"
"github.com/xtls/libxray/dns"
)

type DialerController interface {
ProtectFd(int) bool
}

// SetDNS installs an Android VPN-aware process resolver. server must be an IP
// endpoint such as 8.8.8.8:53 or [2001:4860:4860::8888]:53.
func SetDNS(controller DialerController, server string) error {
if controller == nil {
return errors.New("dns dialer controller is nil")
}
return dns.SetDNS(server, func(fd uintptr) bool {
return controller.ProtectFd(int(fd))
})
}

// ResetDNS restores the resolver that was active before SetDNS.
func ResetDNS() {
dns.ResetDNS()
}

// ProcessFinder -> implemented by apps to resolve UID from connection details.
// See RegisterProcessFinder.
type ProcessFinder interface {
Expand Down
41 changes: 41 additions & 0 deletions dns/dns_android.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//go:build android

package dns

import (
"net"
"sync"
)

var (
resolverMu sync.Mutex
previousResolver *net.Resolver
)

// SetDNS replaces Go's process-wide default resolver with an Android VPN-aware
// resolver. The caller must serialize this with the Xray lifecycle.
func SetDNS(server string, protect protectSocket) error {
resolver, err := newResolver(server, protect)
if err != nil {
return err
}

resolverMu.Lock()
defer resolverMu.Unlock()
if previousResolver == nil {
previousResolver = net.DefaultResolver
}
net.DefaultResolver = resolver
return nil
}

// ResetDNS restores the resolver that was active before SetDNS.
func ResetDNS() {
resolverMu.Lock()
defer resolverMu.Unlock()
if previousResolver == nil {
return
}
net.DefaultResolver = previousResolver
previousResolver = nil
}
66 changes: 66 additions & 0 deletions dns/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package dns

import (
"context"
"errors"
"fmt"
"net"
"strconv"
"strings"
"syscall"
"time"
)

const resolverTimeout = 16 * time.Second

var errProtectDNSConnection = errors.New("protect DNS connection failed")

type protectSocket func(fd uintptr) bool

func newResolver(server string, protect protectSocket) (*net.Resolver, error) {
if err := validateServer(server); err != nil {
return nil, err
}

dialer := &net.Dialer{Timeout: resolverTimeout}
if protect != nil {
dialer.Control = func(_, _ string, connection syscall.RawConn) error {
var protectErr error
if err := connection.Control(func(fd uintptr) {
if !protect(fd) {
protectErr = errProtectDNSConnection
}
}); err != nil {
return err
}
return protectErr
}
}

return &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
// Android may report a loopback resolver to Go. Always use the DNS
// endpoint selected by the VPN configuration instead.
return dialer.DialContext(ctx, network, server)
},
}, nil
}

func validateServer(server string) error {
host, portText, err := net.SplitHostPort(server)
if err != nil {
return fmt.Errorf("invalid DNS server %q: %w", server, err)
}
if zoneIndex := strings.LastIndexByte(host, '%'); zoneIndex >= 0 {
host = host[:zoneIndex]
}
if net.ParseIP(host) == nil {
return fmt.Errorf("invalid DNS server IP %q", host)
}
port, err := strconv.Atoi(portText)
if err != nil || port < 1 || port > 65535 {
return fmt.Errorf("invalid DNS server port %q", portText)
}
return nil
}
84 changes: 84 additions & 0 deletions dns/resolver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package dns

import (
"context"
"net"
"testing"

"github.com/stretchr/testify/require"
)

func TestNewResolverUsesConfiguredServerAndProtectsSocket(t *testing.T) {
server, err := net.ListenPacket("udp", "127.0.0.1:0")
require.NoError(t, err)
defer server.Close()

protected := false
resolver, err := newResolver(server.LocalAddr().String(), func(uintptr) bool {
protected = true
return true
})
require.NoError(t, err)

connection, err := resolver.Dial(
context.Background(),
"udp",
"127.0.0.1:53",
)
require.NoError(t, err)
defer connection.Close()

require.True(t, protected)
require.Equal(t, server.LocalAddr().String(), connection.RemoteAddr().String())
}

func TestNewResolverRejectsFailedProtection(t *testing.T) {
server, err := net.ListenPacket("udp", "127.0.0.1:0")
require.NoError(t, err)
defer server.Close()

resolver, err := newResolver(server.LocalAddr().String(), func(uintptr) bool {
return false
})
require.NoError(t, err)

connection, err := resolver.Dial(
context.Background(),
"udp",
"127.0.0.1:53",
)
if connection != nil {
connection.Close()
}
require.ErrorIs(t, err, errProtectDNSConnection)
}

func TestNewResolverValidatesServer(t *testing.T) {
tests := []struct {
name string
server string
}{
{name: "missing port", server: "8.8.8.8"},
{name: "hostname", server: "dns.example.com:53"},
{name: "zero port", server: "8.8.8.8:0"},
{name: "invalid port", server: "8.8.8.8:dns"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resolver, err := newResolver(test.server, nil)
require.Nil(t, resolver)
require.Error(t, err)
})
}

for _, server := range []string{
"8.8.8.8:53",
"[2001:4860:4860::8888]:53",
} {
t.Run(server, func(t *testing.T) {
resolver, err := newResolver(server, nil)
require.NotNil(t, resolver)
require.NoError(t, err)
})
}
}
19 changes: 19 additions & 0 deletions readme/README.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,25 @@ getXrayState

用于解决 Android 上 socket protect 问题。

### DNS 解析器

Android VPN 运行时可能会向 Go 解析器提供回环 DNS 地址。请在调用
`runXray` 前调用 `SetDNS`,让 Go 使用 VPN 配置指定的 DNS,并通过
`protectFd` 将 DNS socket 排除在 VPN 隧道外。DNS 必须是包含端口的 IP
地址,例如 `8.8.8.8:53` 或 `[2001:4860:4860::8888]:53`。

Xray 停止后调用 `ResetDNS`。这两个 API 仅存在于 Android 产物中,并会
修改 Go 进程级默认解析器。

```java
LibXray.setDNS(controller, "8.8.8.8:53");
LibXray.invoke(runXrayRequest);

// 稍后停止 Core 时:
LibXray.invoke(stopXrayRequest);
LibXray.resetDNS();
```

## geo

### count
Expand Down