Compare commits

...
Author SHA1 Message Date
43a735dd12 xdrive: drop V1 slot frame compatibility, keep only V2
Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-11-02 08:41:51 +00:00
bcce5ff100 xdrive: slot write retry, hedged head reads, V2 frame with V1 compat
Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-11-02 08:35:25 +00:00
f03c20db42 xdrive: port slot ring optimizations for Google Drive
Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-11-02 03:00:32 +00:00
2dd075d0e2 xdrive: pipelined WAL reader fetches and ramped segment size
Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-11-02 02:39:29 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7c8dbcb4bb Bump google.golang.org/grpc from 1.84.0 to 1.85.0 (#7311)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.84.0 to 1.85.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.84.0...v1.85.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.85.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-02 02:03:49 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8ab161a2b0 Bump golang.org/x/net from 0.61.0 to 0.62.0 (#7312)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.61.0 to 0.62.0.
- [Commits](https://github.com/golang/net/compare/v0.61.0...v0.62.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.62.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-02 02:03:35 +00:00
RPRXandGitHub a4a3dff5b2 Xray-core v26.10.31
Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668
Project X Channel: https://t.me/projectXtls

Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

VLESS Post-Quantum Encryption: https://github.com/XTLS/Xray-core/pull/5067
VLESS NFT: https://opensea.io/collection/vless

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-10-31 13:20:19 +00:00
6cdd323ad8 TUN inbound's autoSystemWfpBlockLeak: Support "tcp/port" and "udp/port"; "dns" covers "tcp/853" and "udp/853" as well (#7288)
https://github.com/XTLS/Xray-core/pull/6853#issuecomment-6492838227
https://github.com/XTLS/Xray-core/pull/7288#issuecomment-6507229727
https://github.com/XTLS/Xray-core/pull/7288#issuecomment-6516762634

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 13:16:31 +00:00
316e07a538 XHTTP server: Add h2Params (#7295)
https://github.com/XTLS/Xray-core/pull/7282#issuecomment-6523157944
https://github.com/XTLS/Xray-core/pull/7295#issuecomment-6543501751

Closes https://github.com/XTLS/Xray-core/issues/6268

---------

Co-authored-by: pqhaz3925 <195911022+pqhaz3925@users.noreply.github.com>
2026-10-31 12:52:14 +00:00
MeowandRPRX 0c3402d778 Geodata: Support streaming gzip decompression when downloading assets (#7294)
https://github.com/XTLS/Xray-core/pull/7294#issuecomment-6542074150
2026-10-31 11:56:59 +00:00
bc7c895278 HTTP inbound: Forward short 100 Continue responses promptly (#7298)
https://github.com/XTLS/Xray-core/pull/7298#issuecomment-6542585254

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-10-31 11:29:02 +00:00
konstpicandRPRX e7a69fa85a XHTTP server: Accept OPTIONS as uplinkHTTPMethod (#7290)
https://github.com/XTLS/Xray-core/pull/7290#issuecomment-6505845186
2026-10-31 08:43:57 +00:00
9b8190f52c QUIC sniffer: Don't accept ClientHello with gaps in CRYPTO data (#7292)
https://github.com/XTLS/Xray-core/pull/7292#pullrequestreview-5712194488

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-10-31 08:31:30 +00:00
6ca6d8a693 Commands: Fix xray api bi balancer1 balancer2 (#7299)
https://github.com/XTLS/Xray-core/pull/7279#issuecomment-6468250523

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-10-31 07:21:32 +00:00
风扇滑翔翼andGitHub d1140de273 Chore: Regenerate *.pb.go files (#7287)
https://github.com/XTLS/Xray-core/pull/7263#issuecomment-6464669635
2026-10-31 07:13:55 +00:00
风扇滑翔翼andGitHub 6ea0ab5d3a DefaultListener: Disable MPTCP by default (#7291)
Fixes https://github.com/XTLS/Xray-core/pull/7291#issuecomment-6540309985
2026-10-31 07:05:32 +00:00
RPRXandGitHub 0afec66085 Xray-core v26.10.29
Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668
Project X Channel: https://t.me/projectXtls

Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

VLESS Post-Quantum Encryption: https://github.com/XTLS/Xray-core/pull/5067
VLESS NFT: https://opensea.io/collection/vless

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-10-29 08:38:50 +00:00
55eecaed16 Config: Move inbound module initialization to the end (#7284)
Fixes https://github.com/XTLS/Xray-core/pull/7284#issuecomment-6491545028

---------

Co-authored-by: Copilot <copilot@github.com>
2026-10-29 08:34:16 +00:00
2dfee4b842 XDRIVE transport: Carry a Google Drive session in a ring of slots (#7217)
https://github.com/XTLS/Xray-core/pull/7217#issuecomment-6489554617

---------

Co-authored-by: Ahmad-2213 <106820058+Ahmad-2213@users.noreply.github.com>
2026-10-29 06:47:42 +00:00
Жора ЗмейкинandGitHub b4123b5748 XDRIVE transport: Add "s3" storage backend (#7231)
https://github.com/XTLS/Xray-core/pull/7231#issuecomment-6488077212
2026-10-29 04:44:41 +00:00
ec124ff9e0 WebSocket transport: Treat normal close frames as io.EOF (#7236)
Fixes https://github.com/XTLS/Xray-core/issues/7235#issuecomment-6353389899

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-10-29 03:42:11 +00:00
SolomonandGitHub 8a3c0cbf1d Sockopt: Fix swapped TCP keepalive idle and interval on Darwin (#7219)
https://github.com/XTLS/Xray-core/pull/7219#pullrequestreview-5691960668
2026-10-29 02:54:53 +00:00
78b5a6ec5c GitHub Action CI: Enforce stricter checks on generated protobuf files (#7263)
https://github.com/XTLS/Xray-core/pull/7263#issuecomment-6447862934
https://github.com/XTLS/Xray-core/pull/7263#issuecomment-6484719865
https://github.com/XTLS/Xray-core/pull/7263#issuecomment-6485805671

---------

Co-authored-by: Fangliding <Fangliding.fshxy@outlook.com>
2026-10-29 02:34:47 +00:00
5865e3ac3d DNS outbound: Keep hijacked A/AAAA replies within the buffer (#7225)
https://github.com/XTLS/Xray-core/pull/7225#pullrequestreview-5613859264

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-10-29 00:42:19 +00:00
48f62b801a Geodata: Limit download size, file extensions, and update frequency (#7267)
https://github.com/XTLS/Xray-core/pull/7265#issuecomment-6464588139

---------

Co-authored-by: xqzr <34030394+xqzr@users.noreply.github.com>
2026-10-29 00:22:51 +00:00
MeowandGitHub 43c29f8eb1 Geodata: Update built-in generalized "private" IP ranges (#7249)
And https://github.com/XTLS/Xray-core/pull/7249#issuecomment-6483478811

https://github.com/XTLS/Xray-core/pull/7249#issuecomment-6391494484
https://github.com/XTLS/Xray-core/pull/7249#issuecomment-6483591550
2026-10-28 23:45:22 +00:00
MeowandGitHub 613d8147bb Direct/Freedom outbound: Warn about migrating fragment and noises to Finalmask (#7242)
https://github.com/XTLS/Xray-core/issues/7240#issuecomment-6361862358
2026-10-28 22:49:30 +00:00
38f090255c Commands: Fix xray api adrules c1.json c2.json (#7276)
https://github.com/XTLS/Xray-core/pull/7276#pullrequestreview-5680949792

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
2026-10-28 21:59:27 +00:00
35a3121a83 File loader: Check file extensions without resolving symlinks (#7255)
https://github.com/XTLS/Xray-core/pull/7255#issuecomment-6481194940
https://github.com/XTLS/Xray-core/pull/7250#issuecomment-6398816842

Fixes https://github.com/XTLS/Xray-core/issues/7239#issuecomment-6356122816

---------

Co-authored-by: Eljees <3.14hell@gmail.com>
2026-10-28 21:59:03 +00:00
风扇滑翔翼andGitHub bca08ac080 Routing: Fix Linux IPv6 process (#7232)
Fixes https://github.com/XTLS/Xray-core/issues/7227
2026-10-28 20:49:08 +00:00
6a4274ab75 TUN inbound: autoOutboundsInterface should not set IP_BOUND_IF on IPv6 sockets on Darwin (mainly macOS) (#7230)
Fixes https://github.com/XTLS/Xray-core/issues/7228

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:34:40 +00:00
solodecodeandGitHub 229799a7a3 Direct/Freedom outbound: Fix blockDelay for DispatchLink() inbounds such as VLESS, Hysteria, etc. (#7238)
https://github.com/XTLS/Xray-core/pull/7238#pullrequestreview-5627787937
https://github.com/XTLS/Xray-core/pull/7238#issuecomment-6467092152
https://github.com/XTLS/Xray-core/pull/6060#issuecomment-4384511163
2026-10-28 11:02:55 +00:00
3071fbfa25 Sockopt: Handle single-family interface binding on Windows, including TUN inbound's autoOutboundsInterface (#7214)
And https://github.com/XTLS/Xray-core/pull/7214#issuecomment-6329118637

https://github.com/XTLS/Xray-core/pull/7201#issuecomment-6314453444

Fixes https://github.com/XTLS/Xray-core/issues/7160#issuecomment-6246944012

---------

Co-authored-by: patterniha <71074308+patterniha@users.noreply.github.com>
2026-10-28 10:15:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1576e79563 Bump github.com/pires/go-proxyproto from 0.15.0 to 0.16.0 (#7237)
Bumps [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) from 0.15.0 to 0.16.0.
- [Release notes](https://github.com/pires/go-proxyproto/releases)
- [Commits](https://github.com/pires/go-proxyproto/compare/v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: github.com/pires/go-proxyproto
  dependency-version: 0.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-27 04:31:46 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
67e7f82ca1 Bump docker/login-action from 4.6.0 to 4.7.0 (#7257)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.6.0 to 4.7.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v4.6.0...v4.7.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-27 04:31:31 +00:00
157 changed files with 3618 additions and 1086 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ jobs:
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v4.6.0 uses: docker/login-action@v4.7.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
+3 -18
View File
@@ -35,24 +35,7 @@ jobs:
if: steps.check-assets.outputs.missing == 'true' if: steps.check-assets.outputs.missing == 'true'
run: sleep 90 run: sleep 90
check-proto: check-format-and-proto:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Checkout codebase
uses: actions/checkout@v7
- name: Check Proto Version Header
run: |
head -n 4 core/config.pb.go > ref.txt
find . -name "*.pb.go" ! -name "*_grpc.pb.go" -print0 | while IFS= read -r -d '' file; do
if ! cmp -s ref.txt <(head -n 4 "$file"); then
echo "Error: Header mismatch in $file"
head -n 4 "$file"
exit 1
fi
done
check-format:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
permissions: permissions:
@@ -68,6 +51,8 @@ jobs:
cache: false cache: false
- name: Check Format - name: Check Format
run: go run ./infra/vformat/main.go -mode check -pwd ./ run: go run ./infra/vformat/main.go -mode check -pwd ./
- name: Check Protobuf
run: go run ./infra/vprotogen/main.go -mode check -pwd ./
test: test:
needs: check-assets needs: check-assets
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/commander/config.proto // source: app/commander/config.proto
package commander package commander
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/dispatcher/config.proto // source: app/dispatcher/config.proto
package dispatcher package dispatcher
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/dns/config.proto // source: app/dns/config.proto
package dns package dns
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/dns/fakedns/fakedns.proto // source: app/dns/fakedns/fakedns.proto
package fakedns package fakedns
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/geodata/config.proto // source: app/geodata/config.proto
package geodata package geodata
+33 -36
View File
@@ -1,6 +1,8 @@
package geodata package geodata
import ( import (
"bufio"
"compress/gzip"
"context" "context"
"crypto/tls" "crypto/tls"
go_errors "errors" go_errors "errors"
@@ -21,7 +23,10 @@ import (
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
const idleTimeout = 30 * time.Second const (
idleTimeout = 30 * time.Second
maxDownloadSize int64 = 256 * 1024 * 1024
)
// DO NOT EDIT. Ignored by geodata, rejected by Lua. // DO NOT EDIT. Ignored by geodata, rejected by Lua.
const luaGuardHeader = "\x12\x01\x00" const luaGuardHeader = "\x12\x01\x00"
@@ -33,7 +38,6 @@ type stage struct {
type downloader struct { type downloader struct {
ctx context.Context ctx context.Context
httpClient *http.Client
httpsClient *http.Client httpsClient *http.Client
} }
@@ -61,12 +65,11 @@ func (c *idleConn) Write(b []byte) (int, error) {
func newDownloader(ctx context.Context, dispatcher routing.Dispatcher, outbound string) *downloader { func newDownloader(ctx context.Context, dispatcher routing.Dispatcher, outbound string) *downloader {
return &downloader{ return &downloader{
ctx: ctx, ctx: ctx,
httpClient: newClient(ctx, dispatcher, outbound, false), httpsClient: newClient(ctx, dispatcher, outbound),
httpsClient: newClient(ctx, dispatcher, outbound, true),
} }
} }
func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string, isHTTPS bool) *http.Client { func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string) *http.Client {
dial := func(ctx context.Context, network, address string) (net.Conn, error) { dial := func(ctx context.Context, network, address string) (net.Conn, error) {
var conn net.Conn var conn net.Conn
err := task.Run(ctx, func() error { err := task.Run(ctx, func() error {
@@ -91,7 +94,6 @@ func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound
Conn: conn, Conn: conn,
}, nil }, nil
} }
if isHTTPS {
return &http.Client{ return &http.Client{
Transport: &http2.Transport{ Transport: &http2.Transport{
DialTLSContext: func(ctx context.Context, network string, address string, cfg *tls.Config) (net.Conn, error) { DialTLSContext: func(ctx context.Context, network string, address string, cfg *tls.Config) (net.Conn, error) {
@@ -114,31 +116,12 @@ func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound
if req.URL.Scheme != "https" { if req.URL.Scheme != "https" {
return errors.New("redirected to non-https URL: ", req.URL.String()) return errors.New("redirected to non-https URL: ", req.URL.String())
} }
if len(via) >= 10 { if len(via) >= 20 {
return errors.New("stopped after 10 redirects") return errors.New("stopped after 20 redirects")
} }
return nil return nil
}, },
} }
} else {
return &http.Client{
Transport: &http.Transport{
Proxy: nil,
DisableKeepAlives: true,
DialContext: dial,
ResponseHeaderTimeout: idleTimeout,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if req.URL.Scheme != "https" {
return errors.New("redirected to non-https URL: ", req.URL.String())
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
},
}
}
} }
func (d *downloader) download(assets []*Asset) ([]stage, error) { func (d *downloader) download(assets []*Asset) ([]stage, error) {
@@ -203,27 +186,41 @@ func (d *downloader) fetch(rawURL string, writer io.Writer) error {
} }
utils.TryDefaultHeadersWith(req.Header, "nav") utils.TryDefaultHeadersWith(req.Header, "nav")
var client *http.Client resp, err := d.httpsClient.Do(req)
if req.URL.Scheme == "https" {
client = d.httpsClient
} else {
client = d.httpClient
}
resp, err := client.Do(req)
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
io.Copy(io.Discard, resp.Body)
return errors.New("unexpected status code: ", resp.StatusCode) return errors.New("unexpected status code: ", resp.StatusCode)
} }
if resp.ContentLength > maxDownloadSize {
return errors.New("geodata download exceeds 256 MiB")
}
n, err := io.Copy(writer, resp.Body) br := bufio.NewReader(resp.Body)
header, err := br.Peek(2)
if err != nil && err != io.EOF {
return err
}
var body io.Reader = br
if len(header) == 2 && header[0] == 0x1f && header[1] == 0x8b {
zr, err := gzip.NewReader(br)
if err != nil { if err != nil {
return err return err
} }
defer zr.Close()
body = zr
}
n, err := io.Copy(writer, io.LimitReader(body, maxDownloadSize+1))
if err != nil {
return err
}
if n > maxDownloadSize {
return errors.New("geodata download exceeds 256 MiB")
}
if n == 0 { if n == 0 {
return errors.New("empty response body") return errors.New("empty response body")
} }
+55 -2
View File
@@ -7,10 +7,13 @@ import (
"net/http/httptest" "net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
commongeodata "github.com/xtls/xray-core/common/geodata" commongeodata "github.com/xtls/xray-core/common/geodata"
xlua "github.com/xtls/xray-core/common/lua" xlua "github.com/xtls/xray-core/common/lua"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/core"
"github.com/yuin/gopher-lua/parse" "github.com/yuin/gopher-lua/parse"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -31,11 +34,11 @@ func TestDownloadOneLuaGuard(t *testing.T) {
if err := os.WriteFile(filepath.Join(dir, "asset.lua"), nil, 0o600); err != nil { if err := os.WriteFile(filepath.Join(dir, "asset.lua"), nil, 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(tc.body)) _, _ = w.Write([]byte(tc.body))
})) }))
defer server.Close() defer server.Close()
d := &downloader{ctx: context.Background(), httpClient: server.Client()} d := &downloader{ctx: context.Background(), httpsClient: server.Client()}
staged, err := d.downloadOne(&Asset{Url: server.URL, File: "asset.lua"}) staged, err := d.downloadOne(&Asset{Url: server.URL, File: "asset.lua"})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -71,3 +74,53 @@ func TestDownloadOneLuaGuard(t *testing.T) {
}) })
} }
} }
func TestProtobufConfigValidation(t *testing.T) {
dir := t.TempDir()
t.Setenv("xray.location.asset", dir)
for _, name := range []string{"asset.dat", "asset.DAT"} {
if err := os.WriteFile(filepath.Join(dir, name), nil, 0o600); err != nil {
t.Fatal(err)
}
}
if err := os.Mkdir(filepath.Join(dir, "directory.dat"), 0o700); err != nil {
t.Fatal(err)
}
server, err := core.New(&core.Config{})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = server.Close() })
for _, tc := range []struct {
name string
config *Config
wantError string
}{
{"minutely cron", &Config{Cron: "* * * * *"}, ""},
{"minimum interval", &Config{Cron: "@every 1m"}, ""},
{"short interval", &Config{Cron: "@every 59s"}, "interval must be at least 1 minute"},
{"fractional interval", &Config{Cron: "@every 59.999s"}, "interval must be at least 1 minute"},
{"invalid cron", &Config{Cron: "invalid"}, "invalid geodata cron"},
{"dat extension", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "asset.dat"}}}, ""},
{"uppercase extension", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "asset.DAT"}}}, ""},
{"invalid extension", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "asset.lua"}}}, "extension must be .dat"},
{"missing extension", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "asset"}}}, "extension must be .dat"},
{"missing file", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "missing.dat"}}}, "invalid geodata asset file"},
{"directory", &Config{Cron: "@daily", Assets: []*Asset{{Url: "https://example.com/asset.dat", File: "directory.dat"}}}, "asset is not a regular file"},
} {
t.Run(tc.name, func(t *testing.T) {
config, err := serial.ToTypedMessage(tc.config).GetInstance()
if err != nil {
t.Fatal(err)
}
_, err = core.CreateObject(server, config)
if tc.wantError == "" {
if err != nil {
t.Fatal(err)
}
} else if err == nil || !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("CreateObject() error = %v; want %q", err, tc.wantError)
}
})
}
}
+23 -3
View File
@@ -2,12 +2,16 @@ package geodata
import ( import (
"context" "context"
"path/filepath"
"strings"
"sync" "sync"
"time"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
commongeodata "github.com/xtls/xray-core/common/geodata" commongeodata "github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
) )
@@ -25,6 +29,24 @@ func New(ctx context.Context, config *Config) (*Instance, error) {
if config.Cron == "" { if config.Cron == "" {
return &Instance{}, nil return &Instance{}, nil
} }
if len(config.Assets) > 100 {
return nil, errors.New("geodata supports at most 100 assets")
}
for _, asset := range config.Assets {
if !strings.EqualFold(filepath.Ext(asset.GetFile()), ".dat") {
return nil, errors.New("geodata asset file extension must be .dat")
}
if _, err := filesystem.ResolveAsset(asset.GetFile()); err != nil {
return nil, errors.New("invalid geodata asset file: ", asset.GetFile()).Base(err)
}
}
schedule, err := cron.ParseStandard(config.Cron)
if err != nil {
return nil, errors.New("invalid geodata cron").Base(err)
}
if interval, ok := schedule.(cron.ConstantDelaySchedule); ok && interval.Delay < time.Minute {
return nil, errors.New("geodata cron interval must be at least 1 minute")
}
g := &Instance{ g := &Instance{
assets: config.Assets, assets: config.Assets,
@@ -44,9 +66,7 @@ func New(ctx context.Context, config *Config) (*Instance, error) {
cron.WithChain(cron.SkipIfStillRunning(cron.DiscardLogger)), cron.WithChain(cron.SkipIfStillRunning(cron.DiscardLogger)),
cron.WithLogger(cron.DiscardLogger), cron.WithLogger(cron.DiscardLogger),
) )
if _, err := g.tasker.AddFunc(config.Cron, g.execute); err != nil { g.tasker.Schedule(schedule, cron.FuncJob(g.execute))
return nil, errors.New("invalid geodata cron").Base(err)
}
errors.LogInfo(ctx, "scheduled geodata reload with cron: ", config.Cron) errors.LogInfo(ctx, "scheduled geodata reload with cron: ", config.Cron)
return g, nil return g, nil
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/log/command/config.proto // source: app/log/command/config.proto
package command package command
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.6.0 // - protoc-gen-go-grpc v1.6.2
// - protoc v6.33.5 // - protoc (unknown)
// source: app/log/command/config.proto // source: app/log/command/config.proto
package command package command
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/log/config.proto // source: app/log/config.proto
package log package log
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/metrics/config.proto // source: app/metrics/config.proto
package metrics package metrics
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/observatory/burst/config.proto // source: app/observatory/burst/config.proto
package burst package burst
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/observatory/command/command.proto // source: app/observatory/command/command.proto
package command package command
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.6.0 // - protoc-gen-go-grpc v1.6.2
// - protoc v6.33.5 // - protoc (unknown)
// source: app/observatory/command/command.proto // source: app/observatory/command/command.proto
package command package command
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/observatory/config.proto // source: app/observatory/config.proto
package observatory package observatory
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/policy/config.proto // source: app/policy/config.proto
package policy package policy
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/proxyman/command/command.proto // source: app/proxyman/command/command.proto
package command package command
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.6.0 // - protoc-gen-go-grpc v1.6.2
// - protoc v6.33.5 // - protoc (unknown)
// source: app/proxyman/command/command.proto // source: app/proxyman/command/command.proto
package command package command
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/proxyman/config.proto // source: app/proxyman/config.proto
package proxyman package proxyman
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/reverse/config.proto // source: app/reverse/config.proto
package reverse package reverse
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/router/command/command.proto // source: app/router/command/command.proto
package command package command
+1
View File
@@ -46,6 +46,7 @@ message RoutingContext {
// - attributes: Select connection's additional attributes. // - attributes: Select connection's additional attributes.
// - outbound: Equivalent as "outbound" and "outbound_group", select both // - outbound: Equivalent as "outbound" and "outbound_group", select both
// outbound tag and outbound group tags. // outbound tag and outbound group tags.
//
// * If FieldSelectors is left empty, all fields will be returned. // * If FieldSelectors is left empty, all fields will be returned.
message SubscribeRoutingStatsRequest { message SubscribeRoutingStatsRequest {
repeated string FieldSelectors = 1; repeated string FieldSelectors = 1;
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.6.0 // - protoc-gen-go-grpc v1.6.2
// - protoc v6.33.5 // - protoc (unknown)
// source: app/router/command/command.proto // source: app/router/command/command.proto
package command package command
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/router/config.proto // source: app/router/config.proto
package router package router
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/stats/command/command.proto // source: app/stats/command/command.proto
package command package command
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.6.0 // - protoc-gen-go-grpc v1.6.2
// - protoc v6.33.5 // - protoc (unknown)
// source: app/stats/command/command.proto // source: app/stats/command/command.proto
package command package command
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/stats/config.proto // source: app/stats/config.proto
package stats package stats
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: app/version/config.proto // source: app/version/config.proto
package version package version
+59 -17
View File
@@ -8,23 +8,65 @@ import (
var privateIPMatcher = sync.OnceValue(func() IPMatcher { var privateIPMatcher = sync.OnceValue(func() IPMatcher {
return common.Must2(IPReg.BuildIPMatcher(common.Must2(ParseIPRules([]string{ return common.Must2(IPReg.BuildIPMatcher(common.Must2(ParseIPRules([]string{
"0.0.0.0/8", // https://www.iana.org/assignments/iana-ipv4-special-registry
"10.0.0.0/8", "0.0.0.0/8", // "This network"
"100.64.0.0/10", "0.0.0.0/32", // "This host on this network"
"127.0.0.0/8", "10.0.0.0/8", // Private-Use
"169.254.0.0/16", "100.64.0.0/10", // Shared Address Space
"172.16.0.0/12", "127.0.0.0/8", // Loopback
"192.0.0.0/24", "169.254.0.0/16", // Link Local
"192.0.2.0/24", "172.16.0.0/12", // Private-Use
"192.88.99.0/24", // "192.0.0.0/24", // IETF Protocol Assignments
"192.168.0.0/16", "192.0.0.0/29", // IPv4 Service Continuity Prefix
"198.18.0.0/15", "192.0.0.8/32", // IPv4 dummy address
"198.51.100.0/24", "192.0.0.9/32", // Port Control Protocol Anycast
"203.0.113.0/24", "192.0.0.10/32", // Traversal Using Relays around NAT Anycast
"224.0.0.0/3", "192.0.0.170/32", // NAT64/DNS64 Discovery
"::/127", "192.0.0.171/32", // NAT64/DNS64 Discovery
"fc00::/7", "192.0.2.0/24", // Documentation (TEST-NET-1)
"fe80::/10", // "192.31.196.0/24", // AS112-v4
"192.52.193.0/24", // AMT
// "192.88.99.0/24", // Deprecated (6to4 Relay Anycast)
"192.88.99.2/32", // 6a44-relay anycast address
"192.168.0.0/16", // Private-Use
// "192.175.48.0/24", // Direct Delegation AS112 Service
"198.18.0.0/15", // Benchmarking
"198.51.100.0/24", // Documentation (TEST-NET-2)
"203.0.113.0/24", // Documentation (TEST-NET-3)
"240.0.0.0/4", // Reserved
"255.255.255.255/32", // Limited Broadcast
// https://www.iana.org/assignments/multicast-addresses
"224.0.0.0/4",
// https://www.iana.org/assignments/iana-ipv6-special-registry
"::1/128", // Loopback Address
"::/128", // Unspecified Address
// "::ffff:0:0/96", // IPv4-mapped Address
"64:ff9b::/96", // IPv4-IPv6 Translat.
"64:ff9b:1::/48", // IPv4-IPv6 Translat.
"100::/64", // Discard-Only Address Block
"100:0:0:1::/64", // Dummy IPv6 Prefix
// "2001::/23", // IETF Protocol Assignments
"2001::/32", // TEREDO
"2001:1::1/128", // Port Control Protocol Anycast
"2001:1::2/128", // Traversal Using Relays around NAT Anycast
"2001:1::3/128", // DNS-SD Service Registration Protocol Anycast
"2001:2::/48", // Benchmarking
"2001:3::/32", // AMT
// "2001:4:112::/48", // AS112-v6
// "2001:10::/28", // Deprecated (previously ORCHID)
"2001:20::/28", // ORCHIDv2
"2001:30::/28", // Drone Remote ID Protocol Entity Tags (DETs) Prefix
"2001:db8::/32", // Documentation
"2002::/16", // 6to4
// "2620:4f:8000::/48", // Direct Delegation AS112 Service
"3fff::/20", // Documentation
"5f00::/16", // Segment Routing (SRv6) SIDs
"fc00::/7", // Unique-Local
"fe80::/10", // Link-Local Unicast
// https://www.iana.org/assignments/ipv6-multicast-addresses
"ff00::/8", "ff00::/8",
})))) }))))
}) })
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: common/geodata/geodat.proto // source: common/geodata/geodat.proto
package geodata package geodata
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: common/log/log.proto // source: common/log/log.proto
package log package log
+2 -2
View File
@@ -11,7 +11,7 @@ import (
"github.com/xtls/xray-core/common/base64" "github.com/xtls/xray-core/common/base64"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/common/platform/filesystem"
glua "github.com/yuin/gopher-lua" glua "github.com/yuin/gopher-lua"
"github.com/yuin/gopher-lua/parse" "github.com/yuin/gopher-lua/parse"
) )
@@ -36,7 +36,7 @@ func Compile(script string) (*Program, error) {
// TODO: Restore inline script support and re-enable the related tests. // TODO: Restore inline script support and re-enable the related tests.
return nil, errors.New("currently unavailable") return nil, errors.New("currently unavailable")
code, err = base64.Decode(script) code, err = base64.Decode(script)
} else if name, err = platform.ResolveLuaFile(script); err == nil { } else if name, err = filesystem.ResolveLuaFile(script); err == nil {
code, err = os.ReadFile(name) code, err = os.ReadFile(name)
// TODO: Restore privileged signature authorization and re-enable the related tests and CLI entries. // TODO: Restore privileged signature authorization and re-enable the related tests and CLI entries.
// if err == nil { // if err == nil {
+8 -11
View File
@@ -187,18 +187,19 @@ func TestCompileInlineDiagnostics(t *testing.T) {
*/ */
func TestCompileFileExtension(t *testing.T) { func TestCompileFileExtension(t *testing.T) {
for _, name := range []string{"script", "script.dat", "script.lua.txt", "script.LUA"} { for _, name := range []string{"script", "script.dat", "script.lua.txt"} {
if program, err := Compile(name); program != nil || err == nil || !strings.Contains(err.Error(), "Lua file extension must be .lua") {
t.Fatalf("Compile(%q) = %v, %v; want nil program and .lua extension error", name, program, err)
}
}
for _, name := range []string{"script.lua", "script.LUA", "script.Lua", "script.lUa"} {
path := filepath.Join(t.TempDir(), name) path := filepath.Join(t.TempDir(), name)
if err := os.WriteFile(path, []byte("value = 42"), 0o600); err != nil { if err := os.WriteFile(path, []byte("value = 42"), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if program, err := Compile(path); program != nil || err == nil || !strings.Contains(err.Error(), "Lua file extension must be .lua") {
t.Fatalf("Compile(%q) = %v, %v; want nil program and .lua extension error", path, program, err)
}
}
path := writeTestScript(t, "value = 42") + string(filepath.Separator) + "."
if program, err := Compile(path); program == nil || err != nil { if program, err := Compile(path); program == nil || err != nil {
t.Fatalf("Compile(%q) = %v, %v; want acceptance of the resolved .lua path", path, program, err) t.Fatalf("Compile(%q) = %v, %v; want successful compilation", path, program, err)
}
} }
} }
@@ -234,10 +235,6 @@ func writeTestScript(t *testing.T, code string) string {
if err := os.WriteFile(path, []byte(code), 0o600); err != nil { if err := os.WriteFile(path, []byte(code), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
path, err := filepath.EvalSymlinks(path)
if err != nil {
t.Fatal(err)
}
return path return path
} }
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: common/net/address.proto // source: common/net/address.proto
package net package net
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: common/net/destination.proto // source: common/net/destination.proto
package net package net
+12 -18
View File
@@ -4,7 +4,7 @@ package net
import ( import (
"bufio" "bufio"
"encoding/hex" "encoding/binary"
"fmt" "fmt"
"net" "net"
"os" "os"
@@ -45,10 +45,7 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
panic("Unsupported network type for process lookup.") panic("Unsupported network type for process lookup.")
} }
targetHexAddr, err := formatLittleEndianString(net.ParseIP(srcIP), Port(srcPort)) targetHexAddr := formatProcNetAddress(net.ParseIP(srcIP), Port(srcPort))
if err != nil {
return 0, "", "", errors.New("failed to format address: ", err)
}
portFallback := "" portFallback := ""
if network == "udp" { if network == "udp" {
@@ -87,24 +84,21 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
return pid, procName, absPath, nil return pid, procName, absPath, nil
} }
func formatLittleEndianString(addr net.IP, port Port) (string, error) { func formatProcNetAddress(addr net.IP, port Port) string {
ip := addr
var ipBytes []byte var ipBytes []byte
if ip.To4() != nil { if ipv4 := addr.To4(); ipv4 != nil {
ipBytes = ip.To4() ipBytes = ipv4
} else { } else {
ipBytes = ip.To16() ipBytes = addr.To16()
}
if ipBytes == nil {
return "", errors.New("invalid IP format for ", addr, ": ", ip)
} }
for i, j := 0, len(ipBytes)-1; i < j; i, j = i+1, j-1 { var formatted strings.Builder
ipBytes[i], ipBytes[j] = ipBytes[j], ipBytes[i] formatted.Grow(len(ipBytes)*2 + 5)
for i := 0; i < len(ipBytes); i += 4 {
fmt.Fprintf(&formatted, "%08X", binary.NativeEndian.Uint32(ipBytes[i:i+4]))
} }
portHex := fmt.Sprintf("%04X", uint16(port)) fmt.Fprintf(&formatted, ":%04X", uint16(port))
ipHex := strings.ToUpper(hex.EncodeToString(ipBytes)) return formatted.String()
return fmt.Sprintf("%s:%s", ipHex, portHex), nil
} }
func findInodeInFiles(filePaths []string, targetHexAddr, portFallback string) (string, error) { func findInodeInFiles(filePaths []string, targetHexAddr, portFallback string) (string, error) {
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: common/net/network.proto // source: common/net/network.proto
package net package net
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: common/net/port.proto // source: common/net/port.proto
package net package net
+24 -24
View File
@@ -5,6 +5,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/common/platform"
@@ -26,14 +27,6 @@ func ReadFile(path string) ([]byte, error) {
return buf.ReadAllToBytes(reader) return buf.ReadAllToBytes(reader)
} }
func ReadAsset(file string) ([]byte, error) {
path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return ReadFile(path)
}
func OpenAsset(file string) (io.ReadCloser, int64, error) { func OpenAsset(file string) (io.ReadCloser, int64, error) {
path, info, err := getAssetFileLocation(file) path, info, err := getAssetFileLocation(file)
if err != nil { if err != nil {
@@ -43,11 +36,6 @@ func OpenAsset(file string) (io.ReadCloser, int64, error) {
return r, info.Size(), err return r, info.Size(), err
} }
func StatAsset(file string) (os.FileInfo, error) {
_, info, err := getAssetFileLocation(file)
return info, err
}
func ResolveAsset(file string) (string, error) { func ResolveAsset(file string) (string, error) {
path, _, err := getAssetFileLocation(file) path, _, err := getAssetFileLocation(file)
return path, err return path, err
@@ -79,17 +67,29 @@ func ReadCert(file string) ([]byte, error) {
return ReadFile(platform.GetCertLocation(file)) return ReadFile(platform.GetCertLocation(file))
} }
func CopyFile(dst string, src string) error { // ResolveLuaFile finds a local .lua script and returns its absolute path.
bytes, err := ReadFile(src) // Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir.
if err != nil { func ResolveLuaFile(path string) (string, error) {
return err if !strings.EqualFold(filepath.Ext(path), ".lua") {
return "", errors.New("Lua file extension must be .lua")
} }
f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0o644) if filepath.IsAbs(path) {
if err != nil { return path, nil
return err
} }
defer f.Close() for _, dir := range []string{
platform.GetConfDirPath(),
_, err = f.Write(bytes) platform.NewEnvFlag(platform.ConfigLocation).GetValue(func() string { return "" }),
return err ".",
platform.GetExecutableDir(),
} {
if dir != "" {
resolved := filepath.Join(dir, path)
if _, err := os.Stat(resolved); err == nil {
return filepath.Abs(resolved)
} else if !os.IsNotExist(err) {
return "", err
}
}
}
return "", os.ErrNotExist
} }
+47 -2
View File
@@ -1,13 +1,17 @@
package filesystem_test package filesystem_test
import ( import (
"errors"
"os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/platform"
. "github.com/xtls/xray-core/common/platform/filesystem" . "github.com/xtls/xray-core/common/platform/filesystem"
) )
func TestStatAssetRejectsInvalidPath(t *testing.T) { func TestResolveAssetRejectsInvalidPath(t *testing.T) {
for _, file := range []string{ for _, file := range []string{
"", "",
".", ".",
@@ -25,8 +29,49 @@ func TestStatAssetRejectsInvalidPath(t *testing.T) {
`nested\..\geoip.dat`, `nested\..\geoip.dat`,
filepath.Join(t.TempDir(), "geoip.dat"), filepath.Join(t.TempDir(), "geoip.dat"),
} { } {
if _, err := StatAsset(file); err == nil { if _, err := ResolveAsset(file); err == nil {
t.Fatalf("expected error for %q", file) t.Fatalf("expected error for %q", file)
} }
} }
} }
func TestResolveLuaFile(t *testing.T) {
t.Chdir(t.TempDir())
workingDir, err := os.Getwd()
common.Must(err)
executable, err := os.Executable()
common.Must(err)
file, err := os.CreateTemp(filepath.Dir(executable), "lua-*.lua")
common.Must(err)
common.Must(file.Close())
defer os.Remove(file.Name())
name := filepath.Base(file.Name())
paths := []string{
filepath.Join(t.TempDir(), name),
filepath.Join(t.TempDir(), name),
filepath.Join(workingDir, name),
file.Name(),
}
t.Setenv(platform.ConfdirLocation, filepath.Dir(paths[0]))
t.Setenv(platform.ConfigLocation, filepath.Dir(paths[1]))
for _, path := range paths[:3] {
common.Must(os.WriteFile(path, nil, 0o600))
}
if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] {
t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2])
}
for i, want := range paths {
if i == 2 {
t.Setenv(platform.ConfdirLocation, "")
t.Setenv(platform.ConfigLocation, "")
}
if got, err := ResolveLuaFile(name); err != nil || got != want {
t.Fatalf("resolved path = %q, %v; want %q", got, err, want)
}
common.Must(os.Remove(want))
}
if _, err := ResolveLuaFile(name); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("missing file error = %v", err)
}
}
+2 -2
View File
@@ -14,7 +14,7 @@ func LineSeparator() string {
// GetAssetLocation searches for `file` in the env dir, the executable dir, and certain locations // GetAssetLocation searches for `file` in the env dir, the executable dir, and certain locations
func GetAssetLocation(file string) string { func GetAssetLocation(file string) string {
assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir) assetPath := NewEnvFlag(AssetLocation).GetValue(GetExecutableDir)
defPath := filepath.Join(assetPath, file) defPath := filepath.Join(assetPath, file)
for _, p := range []string{ for _, p := range []string{
defPath, defPath,
@@ -36,6 +36,6 @@ func GetAssetLocation(file string) string {
// GetCertLocation searches for `file` in the env dir and the executable dir // GetCertLocation searches for `file` in the env dir and the executable dir
func GetCertLocation(file string) string { func GetCertLocation(file string) string {
certPath := NewEnvFlag(CertLocation).GetValue(getExecutableDir) certPath := NewEnvFlag(CertLocation).GetValue(GetExecutableDir)
return filepath.Join(certPath, file) return filepath.Join(certPath, file)
} }
+2 -65
View File
@@ -1,8 +1,6 @@
package platform // import "github.com/xtls/xray-core/common/platform" package platform // import "github.com/xtls/xray-core/common/platform"
import ( import (
"errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -74,7 +72,7 @@ func NormalizeEnvName(name string) string {
return strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(name)), ".", "_") return strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(name)), ".", "_")
} }
func getExecutableDir() string { func GetExecutableDir() string {
exec, err := os.Executable() exec, err := os.Executable()
if err != nil { if err != nil {
return "" return ""
@@ -83,7 +81,7 @@ func getExecutableDir() string {
} }
func GetConfigurationPath() string { func GetConfigurationPath() string {
configPath := NewEnvFlag(ConfigLocation).GetValue(getExecutableDir) configPath := NewEnvFlag(ConfigLocation).GetValue(GetExecutableDir)
return filepath.Join(configPath, "config.json") return filepath.Join(configPath, "config.json")
} }
@@ -92,64 +90,3 @@ func GetConfDirPath() string {
configPath := NewEnvFlag(ConfdirLocation).GetValue(func() string { return "" }) configPath := NewEnvFlag(ConfdirLocation).GetValue(func() string { return "" })
return configPath return configPath
} }
// ResolveLuaFile finds a local .lua script and returns its resolved absolute path.
// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir.
func ResolveLuaFile(path string) (string, error) {
if path == "" {
return "", errors.New("Lua file path is empty")
}
paths := []string{path}
if !filepath.IsAbs(path) {
paths = nil
for _, dir := range []string{
GetConfDirPath(),
NewEnvFlag(ConfigLocation).GetValue(func() string { return "" }),
".",
getExecutableDir(),
} {
if dir != "" {
paths = append(paths, filepath.Join(dir, path))
}
}
}
path, err := resolveFile(paths)
if err != nil {
return "", err
}
return resolveLuaTarget(path)
}
func resolveLuaTarget(path string) (string, error) {
path, err := filepath.EvalSymlinks(path)
if err != nil {
return "", fmt.Errorf("failed to resolve Lua file symlinks: %w", err)
}
if filepath.Ext(path) != ".lua" {
return "", errors.New("Lua file extension must be .lua")
}
return path, nil
}
func resolveFile(paths []string) (string, error) {
var tried []string
for _, path := range paths {
path, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("failed to resolve file path: %w", err)
}
tried = append(tried, path)
info, err := os.Stat(path)
if errors.Is(err, os.ErrNotExist) {
continue
}
if err != nil {
return "", fmt.Errorf("failed to inspect file %q: %w", path, err)
}
if !info.Mode().IsRegular() {
return "", fmt.Errorf("file is not a regular file: %s", path)
}
return path, nil
}
return "", fmt.Errorf("file not found; tried %q: %w", tried, os.ErrNotExist)
}
-88
View File
@@ -1,11 +1,9 @@
package platform_test package platform_test
import ( import (
"errors"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"testing" "testing"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
@@ -66,89 +64,3 @@ func TestGetAssetLocation(t *testing.T) {
} }
} }
} }
func TestResolveLuaFile(t *testing.T) {
workingDir := t.TempDir()
t.Chdir(workingDir)
executable, err := os.Executable()
common.Must(err)
file, err := os.CreateTemp(filepath.Dir(executable), "lua-*.lua")
common.Must(err)
common.Must(file.Close())
defer os.Remove(file.Name())
name := filepath.Base(file.Name())
paths := []string{
filepath.Join(t.TempDir(), name),
filepath.Join(t.TempDir(), name),
filepath.Join(workingDir, name),
file.Name(),
}
t.Setenv(ConfdirLocation, filepath.Dir(paths[0]))
t.Setenv(ConfigLocation, filepath.Dir(paths[1]))
for _, path := range paths[:3] {
common.Must(os.WriteFile(path, nil, 0o600))
}
for i, path := range paths {
paths[i], err = filepath.EvalSymlinks(path)
common.Must(err)
}
if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] {
t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2])
}
for i, want := range paths {
if i == 2 {
t.Setenv(ConfdirLocation, "")
t.Setenv(ConfigLocation, "")
}
if got, err := ResolveLuaFile(name); err != nil || got != want {
t.Fatalf("resolved path = %q, %v; want %q", got, err, want)
}
common.Must(os.Remove(want))
}
if _, err := ResolveLuaFile(name); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("missing file error = %v", err)
}
t.Setenv(ConfdirLocation, filepath.Dir(paths[0]))
t.Setenv(ConfigLocation, filepath.Dir(paths[1]))
common.Must(os.Mkdir(paths[0], 0o700))
common.Must(os.WriteFile(paths[1], nil, 0o600))
for _, path := range []string{"", name, filepath.Join(t.TempDir(), name)} {
if _, err := ResolveLuaFile(path); err == nil {
t.Fatalf("accepted invalid path %q", path)
}
}
}
func TestResolveLuaFileSymlink(t *testing.T) {
for _, tc := range []struct {
link, target string
valid bool
}{
{"alias.lua", "data.dat", false},
{"alias.dat", "script.lua", true},
} {
t.Run(tc.link, func(t *testing.T) {
dir, err := filepath.EvalSymlinks(t.TempDir())
common.Must(err)
target := filepath.Join(dir, tc.target)
common.Must(os.WriteFile(target, nil, 0o600))
link := filepath.Join(dir, tc.link)
if err := os.Symlink(tc.target, link); err != nil {
if runtime.GOOS == "windows" {
t.Skipf("cannot create symlinks: %v", err)
}
t.Fatal(err)
}
got, err := ResolveLuaFile(link)
if tc.valid {
if err != nil || got != target {
t.Fatalf("ResolveLuaFile(%q) = %q, %v; want %q", link, got, err, target)
}
} else if err == nil || got != "" || !strings.Contains(err.Error(), "Lua file extension must be .lua") {
t.Fatalf("ResolveLuaFile(%q) = %q, %v; want .lua extension error", link, got, err)
}
})
}
}
+2 -2
View File
@@ -11,12 +11,12 @@ func LineSeparator() string {
// GetAssetLocation searches for `file` in the env dir and the executable dir // GetAssetLocation searches for `file` in the env dir and the executable dir
func GetAssetLocation(file string) string { func GetAssetLocation(file string) string {
assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir) assetPath := NewEnvFlag(AssetLocation).GetValue(GetExecutableDir)
return filepath.Join(assetPath, file) return filepath.Join(assetPath, file)
} }
// GetCertLocation searches for `file` in the env dir and the executable dir // GetCertLocation searches for `file` in the env dir and the executable dir
func GetCertLocation(file string) string { func GetCertLocation(file string) string {
certPath := NewEnvFlag(CertLocation).GetValue(getExecutableDir) certPath := NewEnvFlag(CertLocation).GetValue(GetExecutableDir)
return filepath.Join(certPath, file) return filepath.Join(certPath, file)
} }
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: common/protocol/headers.proto // source: common/protocol/headers.proto
package protocol package protocol
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: common/protocol/server_spec.proto // source: common/protocol/server_spec.proto
package protocol package protocol
+4
View File
@@ -101,6 +101,10 @@ func ReadClientHello(data []byte, h *SniffHeader) error {
return errNotClientHello return errNotClientHello
} }
if nameType == 0 { if nameType == 0 {
// RFC 6066 requires a non-empty HostName.
if nameLen == 0 {
return errNotClientHello
}
// QUIC separated across packets // QUIC separated across packets
// May cause the serverName to be incomplete // May cause the serverName to be incomplete
b := byte(0) b := byte(0)
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: common/protocol/user.proto // source: common/protocol/user.proto
package protocol package protocol
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: common/serial/typed_message.proto // source: common/serial/typed_message.proto
package serial package serial
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: core/config.proto // source: core/config.proto
package core package core
+1 -1
View File
@@ -20,7 +20,7 @@ import (
var ( var (
Version_x byte = 26 Version_x byte = 26
Version_y byte = 10 Version_y byte = 10
Version_z byte = 21 Version_z byte = 31
) )
var ( var (
-2
View File
@@ -1,5 +1,3 @@
package core package core
//go:generate go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
//go:generate go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
//go:generate go run ../infra/vprotogen/main.go -pwd ./.. //go:generate go run ../infra/vprotogen/main.go -pwd ./..
+10
View File
@@ -2,6 +2,7 @@ package localdns
import ( import (
"context" "context"
"net/netip"
"syscall" "syscall"
"time" "time"
@@ -100,6 +101,15 @@ func New() *Client {
if internet.IsSkippedDNSServer(address) { if internet.IsSkippedDNSServer(address) {
return nil, errors.New("skipped DNS server ", address) return nil, errors.New("skipped DNS server ", address)
} }
// A socket of the server's IP version only needs that version bound
// to the interface, which a dual-stack one would need both of.
if ap, err := netip.ParseAddrPort(address); err == nil && (network == "udp" || network == "tcp") {
if ap.Addr().Unmap().Is4() {
network += "4"
} else {
network += "6"
}
}
return d.DialContext(ctx, network, address) return d.DialContext(ctx, network, address)
}, },
} }
+11 -4
View File
@@ -4,6 +4,7 @@ go 1.27
require ( require (
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
github.com/bufbuild/protocompile v0.14.1
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1 github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.7.0
@@ -14,7 +15,7 @@ require (
github.com/miekg/dns v1.1.73 github.com/miekg/dns v1.1.73
github.com/pelletier/go-toml v1.9.5 github.com/pelletier/go-toml v1.9.5
github.com/pion/stun/v3 v3.1.7 github.com/pion/stun/v3 v3.1.7
github.com/pires/go-proxyproto v0.15.0 github.com/pires/go-proxyproto v0.16.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.12.1 github.com/stretchr/testify v1.12.1
@@ -25,13 +26,13 @@ require (
golang.org/x/crypto v0.58.0 golang.org/x/crypto v0.58.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.61.0 golang.org/x/net v0.62.0
golang.org/x/sync v0.24.0 golang.org/x/sync v0.24.0
golang.org/x/sys v0.49.0 golang.org/x/sys v0.49.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.1.1 golang.zx2c4.com/wireguard/windows v1.1.1
google.golang.org/grpc v1.84.0 google.golang.org/grpc v1.85.0
google.golang.org/protobuf v1.36.12 google.golang.org/protobuf v1.36.12
h12.io/socks v1.0.3 h12.io/socks v1.0.3
layeh.com/gopher-luar v1.0.11 layeh.com/gopher-luar v1.0.11
@@ -57,7 +58,13 @@ require (
go.yaml.in/yaml/v3 v3.0.5 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/text v0.43.0 // indirect golang.org/x/text v0.43.0 // indirect
golang.org/x/tools v0.51.0 // indirect golang.org/x/tools v0.51.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
) )
tool (
google.golang.org/grpc/cmd/protoc-gen-go-grpc
google.golang.org/protobuf/cmd/protoc-gen-go
)
+12 -8
View File
@@ -2,6 +2,8 @@ github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA
github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg= github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8= github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -59,8 +61,8 @@ github.com/pion/stun/v3 v3.1.7 h1:uRXMTlGLf89WgItGNyZ6aR5jMTX0NBbybXADpQCzn+E=
github.com/pion/stun/v3 v3.1.7/go.mod h1:Nq77RW4aRrSNrltf2ksUJLjxWeipj4lnlgdsYIxC8g8= github.com/pion/stun/v3 v3.1.7/go.mod h1:Nq77RW4aRrSNrltf2ksUJLjxWeipj4lnlgdsYIxC8g8=
github.com/pion/transport/v4 v4.1.0 h1:8S+nF2reM2cJuqC6g78OVy2BBgmbdns+acx3jA97BvQ= github.com/pion/transport/v4 v4.1.0 h1:8S+nF2reM2cJuqC6g78OVy2BBgmbdns+acx3jA97BvQ=
github.com/pion/transport/v4 v4.1.0/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= github.com/pion/transport/v4 v4.1.0/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI= github.com/pires/go-proxyproto v0.16.0 h1:9KYT/fM3wpjT8bdMEj/7FBcoiUG1KyzEjRYnPu+vz7A=
github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24= github.com/pires/go-proxyproto v0.16.0/go.mod h1:n7yhg8yIfNl3KgYDCwBzmeUHNpKTVnUhcWpc7rV4RW4=
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
@@ -107,8 +109,8 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
@@ -148,10 +150,12 @@ golang.zx2c4.com/wireguard/windows v1.1.1 h1:8/H97U1v1PNDNcBsMZgU3KFuND9MQdTsU2N
golang.zx2c4.com/wireguard/windows v1.1.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs= golang.zx2c4.com/wireguard/windows v1.1.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE= google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1 h1:utmqiUzZAEINbLmRyAt1QjVkMyLQ+lvIwCpLaB1rZfw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= google.golang.org/grpc v1.85.0 h1:BPBCfuVRQ1qvBhKDOq9uvQllMvO59rKsYhXpp7Dd8vw=
google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= google.golang.org/grpc v1.85.0/go.mod h1:0rQrOD58yjal/0EaVbGBANindixsCZNQPDV3VKtaKuQ=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 h1:rgSNvqscFZ1JgV/4wH5GOsZFSFkR2Eua9As3KIr2LlM=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2/go.mod h1:iMEtFwDlAhjDU9L5mY6U1XLwlIId/G3h+QcBHDIvrJ8=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+2
View File
@@ -91,6 +91,7 @@ func (c *FreedomConfig) Build() (proto.Message, error) {
} }
if c.Fragment != nil { if c.Fragment != nil {
errors.PrintDeprecatedFeatureWarning(`"fragment" in Freedom`, `"fragment" in "streamSettings.finalmask.tcp"`)
config.Fragment = new(freedom.Fragment) config.Fragment = new(freedom.Fragment)
switch strings.ToLower(c.Fragment.Packets) { switch strings.ToLower(c.Fragment.Packets) {
@@ -147,6 +148,7 @@ func (c *FreedomConfig) Build() (proto.Message, error) {
} }
if c.Noises != nil { if c.Noises != nil {
errors.PrintDeprecatedFeatureWarning(`"noises" in Freedom`, `"noise" in "streamSettings.finalmask.udp"`)
for _, n := range c.Noises { for _, n := range c.Noises {
NConfig, err := ParseNoise(n) NConfig, err := ParseNoise(n)
if err != nil { if err != nil {
-4
View File
@@ -6,7 +6,6 @@ import (
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"github.com/xtls/xray-core/app/geodata" "github.com/xtls/xray-core/app/geodata"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform/filesystem"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -19,9 +18,6 @@ func (c *GeodataAssetConfig) Build() (*geodata.Asset, error) {
if err := validateHTTPS(c.URL); err != nil { if err := validateHTTPS(c.URL); err != nil {
return nil, errors.New("invalid geodata asset url: ", c.URL).Base(err) return nil, errors.New("invalid geodata asset url: ", c.URL).Base(err)
} }
if _, err := filesystem.StatAsset(c.File); err != nil {
return nil, errors.New("invalid geodata asset file: ", c.File).Base(err)
}
return &geodata.Asset{ return &geodata.Asset{
Url: c.URL, Url: c.URL,
File: c.File, File: c.File,
-23
View File
@@ -1,7 +1,6 @@
package conf_test package conf_test
import ( import (
"path/filepath"
"testing" "testing"
"github.com/xtls/xray-core/app/geodata" "github.com/xtls/xray-core/app/geodata"
@@ -9,8 +8,6 @@ import (
) )
func TestGeodataConfig(t *testing.T) { func TestGeodataConfig(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
creator := func() Buildable { creator := func() Buildable {
return new(GeodataConfig) return new(GeodataConfig)
} }
@@ -38,27 +35,7 @@ func TestGeodataConfig(t *testing.T) {
}) })
} }
func TestGeodataAssetConfig(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
if _, err := (&GeodataAssetConfig{
URL: "https://example.com/geoip.dat",
File: "geoip.dat",
}).Build(); err != nil {
t.Fatal(err)
}
if _, err := (&GeodataAssetConfig{
URL: "https://example.com/geoip.dat",
File: "missing.dat",
}).Build(); err == nil {
t.Fatal("expected error")
}
}
func TestGeodataAssetConfigInvalidURL(t *testing.T) { func TestGeodataAssetConfigInvalidURL(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
for _, rawURL := range []string{ for _, rawURL := range []string{
"", "",
"http://example.com/geoip.dat", "http://example.com/geoip.dat",
+26 -1
View File
@@ -293,6 +293,7 @@ type SplitHTTPConfig struct {
ServerMaxHeaderBytes int32 `json:"serverMaxHeaderBytes"` ServerMaxHeaderBytes int32 `json:"serverMaxHeaderBytes"`
Xmux XmuxConfig `json:"xmux"` Xmux XmuxConfig `json:"xmux"`
DownloadSettings *StreamConfig `json:"downloadSettings"` DownloadSettings *StreamConfig `json:"downloadSettings"`
H2Params json.RawMessage `json:"h2Params"`
Extra json.RawMessage `json:"extra"` Extra json.RawMessage `json:"extra"`
} }
@@ -495,6 +496,7 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
ServerMaxHeaderBytes: c.ServerMaxHeaderBytes, ServerMaxHeaderBytes: c.ServerMaxHeaderBytes,
SessionIDTable: c.SessionIDTable, SessionIDTable: c.SessionIDTable,
SessionIDLength: newRangeConfig(c.SessionIDLength), SessionIDLength: newRangeConfig(c.SessionIDLength),
H2Params: c.H2Params,
Xmux: &splithttp.XmuxConfig{ Xmux: &splithttp.XmuxConfig{
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency), MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
MaxConnections: newRangeConfig(c.Xmux.MaxConnections), MaxConnections: newRangeConfig(c.Xmux.MaxConnections),
@@ -997,12 +999,20 @@ type XDriveConfig struct {
EagerWindowMs uint32 `json:"eagerWindowMs"` EagerWindowMs uint32 `json:"eagerWindowMs"`
HoleTimeoutMs uint32 `json:"holeTimeoutMs"` HoleTimeoutMs uint32 `json:"holeTimeoutMs"`
Template json.RawMessage `json:"template"` Template json.RawMessage `json:"template"`
S3 *XDriveS3Config `json:"s3"`
Xmux XmuxConfig `json:"xmux"` Xmux XmuxConfig `json:"xmux"`
// set by StreamConfig when "alpn" is ["h3"] // set by StreamConfig when "alpn" is ["h3"]
h3 bool h3 bool
} }
type XDriveS3Config struct {
Endpoint string `json:"endpoint"`
Region string `json:"region"`
Bucket string `json:"bucket"`
PathStyle *bool `json:"pathStyle"`
}
// Build implements Buildable. // Build implements Buildable.
func (c *XDriveConfig) Build() (proto.Message, error) { func (c *XDriveConfig) Build() (proto.Message, error) {
switch c.Service { switch c.Service {
@@ -1019,8 +1029,12 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
if len(c.Template) == 0 { if len(c.Template) == 0 {
return nil, errors.New(`service "template" needs a "template" object`) return nil, errors.New(`service "template" needs a "template" object`)
} }
case "s3":
if c.S3 == nil {
return nil, errors.New(`service "s3" needs an "s3" object`)
}
default: default:
return nil, errors.New(`unknown "service": "`, c.Service, `", use "google-drive", "template" or "local"`) return nil, errors.New(`unknown "service": "`, c.Service, `", use "google-drive", "template", "s3" or "local"`)
} }
if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 { if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency") return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
@@ -1059,5 +1073,16 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
HKeepAlivePeriod: c.Xmux.HKeepAlivePeriod, HKeepAlivePeriod: c.Xmux.HKeepAlivePeriod,
}, },
} }
if c.Service == "s3" {
config.S3 = &xdrive.S3Config{
Endpoint: c.S3.Endpoint,
Region: c.S3.Region,
Bucket: c.S3.Bucket,
PathStyle: c.S3.PathStyle == nil || *c.S3.PathStyle,
}
if err := xdrive.ValidateS3Config(config); err != nil {
return nil, err
}
}
return config, nil return config, nil
} }
+1 -1
View File
@@ -324,7 +324,7 @@ func TestXDriveRejectsUnknownService(t *testing.T) {
if err := json.Unmarshal([]byte(`{"remoteFolder": "/tmp/xdrive", "service": "Dropbox"}`), config); err != nil { if err := json.Unmarshal([]byte(`{"remoteFolder": "/tmp/xdrive", "service": "Dropbox"}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err) t.Fatalf("Unmarshal: %v", err)
} }
if _, err := config.Build(); err == nil || !strings.Contains(err.Error(), `use "google-drive", "template" or "local"`) { if _, err := config.Build(); err == nil || !strings.Contains(err.Error(), `use "google-drive", "template", "s3" or "local"`) {
t.Fatalf("expected an unknown service error, got %v", err) t.Fatalf("expected an unknown service error, got %v", err)
} }
} }
+10 -15
View File
@@ -6,9 +6,7 @@ import (
"math/big" "math/big"
"net" "net"
"runtime" "runtime"
"slices"
"strconv" "strconv"
"strings"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/proxy/tun" "github.com/xtls/xray-core/proxy/tun"
@@ -39,25 +37,22 @@ func (v *TunConfig) Build() (proto.Message, error) {
AutoSystemRoutingTable: v.AutoSystemRoutingTable, AutoSystemRoutingTable: v.AutoSystemRoutingTable,
AutoSystemDnsToGateway: v.AutoSystemDnsToGateway, AutoSystemDnsToGateway: v.AutoSystemDnsToGateway,
} }
for _, leak := range v.AutoSystemWfpBlockLeak { leaks, err := tun.ExpandWfpBlockLeak(v.AutoSystemWfpBlockLeak)
switch leak := strings.ToLower(leak); leak { if err != nil {
case "dns", "misconfigtun": return nil, err
config.AutoSystemWfpBlockLeak = append(config.AutoSystemWfpBlockLeak, leak)
default:
return nil, errors.New("unknown autoSystemWfpBlockLeak value: ", leak)
}
} }
config.AutoSystemWfpBlockLeak = leaks
// Each option needs other settings on the system it takes effect on: the // Each option needs other settings on the system it takes effect on: the
// filters go along with the routes of autoSystemRoutingTable, "dns" lets // filters go along with the routes of autoSystemRoutingTable, keeping the
// DNS through the TUN only, and autoSystemDnsToGateway points the system // system's DNS inside the TUN needs DNS servers on it, and
// DNS at the gateway. // autoSystemDnsToGateway points the system DNS at the gateway.
switch runtime.GOOS { switch runtime.GOOS {
case "windows": case "windows":
if len(config.AutoSystemWfpBlockLeak) > 0 && len(v.AutoSystemRoutingTable) == 0 { if len(leaks) > 0 && len(v.AutoSystemRoutingTable) == 0 {
return nil, errors.New("autoSystemWfpBlockLeak needs autoSystemRoutingTable to be set") return nil, errors.New("autoSystemWfpBlockLeak needs autoSystemRoutingTable to be set")
} }
if slices.Contains(config.AutoSystemWfpBlockLeak, "dns") && len(v.DNS) == 0 { if tun.BlocksSystemDNS(leaks) && len(v.DNS) == 0 {
return nil, errors.New(`autoSystemWfpBlockLeak "dns" needs dns to be set`) return nil, errors.New(`autoSystemWfpBlockLeak "dns", "udp/53", "tcp/53" or "windnsclient" needs dns to be set`)
} }
case "linux", "freebsd": case "linux", "freebsd":
if v.AutoSystemDnsToGateway && len(v.Gateway) == 0 { if v.AutoSystemDnsToGateway && len(v.Gateway) == 0 {
+14 -4
View File
@@ -29,12 +29,17 @@ func TestTunConfigAutoSystem(t *testing.T) {
{ {
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns", "misconfigtun"]}`, Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns", "misconfigtun"]}`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"dns", "misconfigtun"}}, Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853", "misconfigtun"}},
}, },
{ {
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["DNS"]}`, Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["DNS"]}`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"dns"}}, Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853"}},
},
{
Input: `{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["TCP/853", "udp/853", "tcp/0853"]}`,
Parser: loadJSON(creator),
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"tcp/853", "udp/853"}},
}, },
}) })
} }
@@ -49,6 +54,9 @@ func TestTunConfigAutoSystemNeeds(t *testing.T) {
{`{"name": "xray0", "autoSystemWfpBlockLeak": ["misconfigtun"]}`, []string{"windows"}}, {`{"name": "xray0", "autoSystemWfpBlockLeak": ["misconfigtun"]}`, []string{"windows"}},
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["misconfigtun"]}`, nil}, {`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["misconfigtun"]}`, nil},
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns"]}`, []string{"windows"}}, {`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns"]}`, []string{"windows"}},
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["udp/53"]}`, []string{"windows"}},
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["windnsclient"]}`, []string{"windows"}},
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["tcp/853", "udp/853"]}`, nil},
{`{"name": "xray0", "autoSystemDnsToGateway": true}`, []string{"linux", "freebsd"}}, {`{"name": "xray0", "autoSystemDnsToGateway": true}`, []string{"linux", "freebsd"}},
} { } {
config := new(TunConfig) config := new(TunConfig)
@@ -62,11 +70,13 @@ func TestTunConfigAutoSystemNeeds(t *testing.T) {
} }
func TestTunConfigAutoSystemWfpBlockLeakUnknown(t *testing.T) { func TestTunConfigAutoSystemWfpBlockLeakUnknown(t *testing.T) {
for _, value := range []string{"ip", "tcp/0", "icmp/1"} {
config := new(TunConfig) config := new(TunConfig)
if err := json.Unmarshal([]byte(`{"name": "xray0", "autoSystemWfpBlockLeak": ["dns", "ip"]}`), config); err != nil { if err := json.Unmarshal([]byte(`{"name": "xray0", "autoSystemWfpBlockLeak": ["dns", "`+value+`"]}`), config); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if _, err := config.Build(); err == nil { if _, err := config.Build(); err == nil {
t.Error("an unknown autoSystemWfpBlockLeak value was accepted") t.Errorf("autoSystemWfpBlockLeak value %q was accepted", value)
}
} }
} }
+6 -3
View File
@@ -237,10 +237,11 @@ type OutboundDetourConfig struct {
func requiresTransportSecurity(address *Address) bool { func requiresTransportSecurity(address *Address) bool {
if address == nil || address.Address == nil { if address == nil || address.Address == nil {
return false return true
} }
if address.Family().IsIP() { if address.Family().IsIP() {
return !geodata.GetPrivateIPMatcher().Match(address.IP()) ip := address.IP()
return !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast()
} }
domain := strings.TrimSuffix(strings.ToLower(address.Domain()), ".") domain := strings.TrimSuffix(strings.ToLower(address.Domain()), ".")
return !geodata.GetPrivateDomainMatcher().MatchAny(domain) return !geodata.GetPrivateDomainMatcher().MatchAny(domain)
@@ -608,7 +609,6 @@ func (c *Config) Build() (*core.Config, error) {
config := &core.Config{ config := &core.Config{
App: []*serial.TypedMessage{ App: []*serial.TypedMessage{
serial.ToTypedMessage(&dispatcher.Config{}), serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}), serial.ToTypedMessage(&proxyman.OutboundConfig{}),
}, },
} }
@@ -718,6 +718,9 @@ func (c *Config) Build() (*core.Config, error) {
config.App = append(config.App, serial.ToTypedMessage(r)) config.App = append(config.App, serial.ToTypedMessage(r))
} }
// Initialize the inbound module last to ensure inbound connections are handled correctly.
config.App = append(config.App, serial.ToTypedMessage(&proxyman.InboundConfig{}))
var inbounds []InboundDetourConfig var inbounds []InboundDetourConfig
if len(c.InboundConfigs) > 0 { if len(c.InboundConfigs) > 0 {
+1 -1
View File
@@ -90,7 +90,6 @@ func TestXrayConfig(t *testing.T) {
AccessLogPath: "/var/log/xray/access.log", AccessLogPath: "/var/log/xray/access.log",
}), }),
serial.ToTypedMessage(&dispatcher.Config{}), serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}), serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&router.Config{ serial.ToTypedMessage(&router.Config{
DomainStrategy: router.Config_AsIs, DomainStrategy: router.Config_AsIs,
@@ -111,6 +110,7 @@ func TestXrayConfig(t *testing.T) {
}, },
}, },
}), }),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
}, },
Inbound: []*core.InboundHandlerConfig{ Inbound: []*core.InboundHandlerConfig{
{ {
+177 -200
View File
@@ -1,149 +1,87 @@
package main package main
import ( import (
"errors" "bytes"
"context"
"flag" "flag"
"fmt" "fmt"
"go/build" "io/fs"
"io"
"net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp"
"runtime"
"strconv"
"strings" "strings"
"github.com/bufbuild/protocompile"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/pluginpb"
) )
var directory = flag.String("pwd", "", "Working directory of Xray vprotogen.") var (
directory = flag.String("pwd", "", "Working directory of Xray vprotogen.")
mode = flag.String("mode", "generate", "Execution mode. 'generate' (default) compiles and saves changes. 'check' reports out-of-date files and exits with non-zero.")
)
// envFile returns the name of the Go environment configuration file. // topoSort sorts file descriptors so each dependency appears before files importing it.
// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166 func topoSort(files []protoreflect.FileDescriptor) []*descriptorpb.FileDescriptorProto {
func envFile() (string, error) { fileMap := make(map[string]protoreflect.FileDescriptor)
if file := os.Getenv("GOENV"); file != "" { for _, f := range files {
if file == "off" { fileMap[f.Path()] = f
return "", errors.New("GOENV=off")
} }
return file, nil
visited := make(map[string]bool)
var sorted []*descriptorpb.FileDescriptorProto
var visit func(fd protoreflect.FileDescriptor)
visit = func(fd protoreflect.FileDescriptor) {
if visited[fd.Path()] {
return
} }
dir, err := os.UserConfigDir() visited[fd.Path()] = true
imports := fd.Imports()
for i := 0; i < imports.Len(); i++ {
if dep, ok := fileMap[imports.Get(i).Path()]; ok {
visit(dep)
}
}
sorted = append(sorted, protodesc.ToFileDescriptorProto(fd))
}
for _, f := range files {
visit(f)
}
return sorted
}
func runPlugin(pluginName, pwd string, req *pluginpb.CodeGeneratorRequest) (*pluginpb.CodeGeneratorResponse, error) {
reqBytes, err := proto.Marshal(req)
if err != nil { if err != nil {
return "", err return nil, err
}
if dir == "" {
return "", errors.New("missing user-config dir")
}
return filepath.Join(dir, "go", "env"), nil
} }
// GetRuntimeEnv returns the value of runtime environment variable, cmd := exec.Command("go", "tool", pluginName)
// that is set by running following command: `go env -w key=value`. cmd.Dir = pwd
func GetRuntimeEnv(key string) (string, error) { cmd.Stdin = bytes.NewReader(reqBytes)
file, err := envFile() cmd.Env = os.Environ()
if err != nil { var stdout, stderr bytes.Buffer
return "", err cmd.Stdout = &stdout
} cmd.Stderr = &stderr
if file == "" {
return "", errors.New("missing runtime env file") if err := cmd.Run(); err != nil {
} return nil, fmt.Errorf("%v: %s", err, stderr.String())
var data []byte
var runtimeEnv string
data, readErr := os.ReadFile(file)
if readErr != nil {
return "", readErr
}
envStrings := strings.Split(string(data), "\n")
for _, envItem := range envStrings {
envItem = strings.TrimSuffix(envItem, "\r")
envKeyValue := strings.Split(envItem, "=")
if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) {
runtimeEnv = strings.TrimSpace(envKeyValue[1])
}
}
return runtimeEnv, nil
} }
// GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty. var resp pluginpb.CodeGeneratorResponse
func GetGOBIN() string { if err := proto.Unmarshal(stdout.Bytes(), &resp); err != nil {
// The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command` return nil, fmt.Errorf("failed to unmarshal response: %w", err)
GOBIN := os.Getenv("GOBIN")
if GOBIN == "" {
var err error
// The one set by user by running `go env -w GOBIN=/path`
GOBIN, err = GetRuntimeEnv("GOBIN")
if err != nil {
// The default one that Golang uses
return filepath.Join(build.Default.GOPATH, "bin")
} }
if GOBIN == "" { if resp.GetError() != "" {
return filepath.Join(build.Default.GOPATH, "bin") return nil, fmt.Errorf("%s", resp.GetError())
} }
return GOBIN return &resp, nil
}
return GOBIN
}
func whichProtoc(suffix, targetedVersion string) (string, error) {
protoc := "protoc" + suffix
path, err := exec.LookPath(protoc)
if err != nil {
return "", fmt.Errorf(`
Command "%s" not found.
Make sure that %s is in your system path or current path.
Download %s v%s or later from https://github.com/protocolbuffers/protobuf/releases
`, protoc, protoc, protoc, targetedVersion)
}
return path, nil
}
func getProjectProtocVersion(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", errors.New("can not get the version of protobuf used in xray project")
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", errors.New("can not read from body")
}
versionRegexp := regexp.MustCompile(`\/\/\s*protoc\s*v\d+\.(\d+\.\d+)`)
matched := versionRegexp.FindStringSubmatch(string(body))
return matched[1], nil
}
func getInstalledProtocVersion(protocPath string) (string, error) {
cmd := exec.Command(protocPath, "--version")
cmd.Env = append(cmd.Env, os.Environ()...)
output, cmdErr := cmd.CombinedOutput()
if cmdErr != nil {
return "", cmdErr
}
versionRegexp := regexp.MustCompile(`protoc\s*(\d+\.\d+)`)
matched := versionRegexp.FindStringSubmatch(string(output))
return matched[1], nil
}
func parseVersion(s string, width int) int64 {
strList := strings.Split(s, ".")
format := fmt.Sprintf("%%s%%0%ds", width)
v := ""
for _, value := range strList {
v = fmt.Sprintf(format, v, value)
}
var result int64
var err error
if result, err = strconv.ParseInt(v, 10, 64); err != nil {
return 0
}
return result
}
func needToUpdate(targetedVersion, installedVersion string) bool {
vt := parseVersion(targetedVersion, 4)
vi := parseVersion(installedVersion, 4)
return vt > vi
} }
func main() { func main() {
@@ -154,106 +92,145 @@ func main() {
flag.Parse() flag.Parse()
if !filepath.IsAbs(*directory) { if !filepath.IsAbs(*directory) {
pwd, wdErr := os.Getwd() pwd, err := os.Getwd()
if wdErr != nil { if err != nil {
fmt.Println("Can not get current working directory.") fmt.Println("Can not get current working directory.")
os.Exit(1) os.Exit(1)
} }
*directory = filepath.Join(pwd, *directory) *directory = filepath.Join(pwd, *directory)
} }
pwd := *directory pwd := *directory
GOBIN := GetGOBIN()
binPath := os.Getenv("PATH")
pathSlice := []string{pwd, GOBIN, binPath}
binPath = strings.Join(pathSlice, string(os.PathListSeparator))
os.Setenv("PATH", binPath)
suffix := "" isCheck := false
if runtime.GOOS == "windows" { switch *mode {
suffix = ".exe" case "generate", "":
} isCheck = false
case "check":
/* isCheck = true
targetedVersion, err := getProjectProtocVersion("https://raw.githubusercontent.com/XTLS/Xray-core/HEAD/core/config.pb.go") default:
if err != nil { fmt.Fprintf(os.Stderr, "Unrecognized 'mode': %s. Use 'generate' or 'check'.\n", *mode)
fmt.Println(err)
os.Exit(1)
}
*/
targetedVersion := ""
protoc, err := whichProtoc(suffix, targetedVersion)
if err != nil {
fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
installedVersion, err := getInstalledProtocVersion(protoc) if isCheck {
if err != nil { fmt.Println("Checking for out-of-date Protobuf generated files...")
fmt.Println(err)
os.Exit(1)
} }
if needToUpdate(targetedVersion, installedVersion) { var protoFiles []string
fmt.Printf(` var existingPbFiles []string
You are using an old protobuf version, please update to v%s or later. err := filepath.WalkDir(pwd, func(path string, d fs.DirEntry, err error) error {
Download it from https://github.com/protocolbuffers/protobuf/releases if err != nil || d.IsDir() {
* Protobuf version used in xray project: v%s
* Protobuf version you have installed: v%s
`, targetedVersion, targetedVersion, installedVersion)
os.Exit(1)
}
protoFilesMap := make(map[string][]string)
walkErr := filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error {
if err != nil {
fmt.Println(err)
return err return err
} }
if strings.HasSuffix(d.Name(), ".proto") {
if info.IsDir() { rel, _ := filepath.Rel(pwd, path)
return nil protoFiles = append(protoFiles, filepath.ToSlash(rel))
} }
if strings.HasSuffix(d.Name(), ".pb.go") {
dir := filepath.Dir(path) existingPbFiles = append(existingPbFiles, path)
filename := filepath.Base(path)
if strings.HasSuffix(filename, ".proto") {
path = path[len(pwd)+1:]
protoFilesMap[dir] = append(protoFilesMap[dir], path)
} }
return nil return nil
}) })
if walkErr != nil { if err != nil {
fmt.Println(walkErr) fmt.Println("Walk error:", err)
os.Exit(1) os.Exit(1)
} }
for _, files := range protoFilesMap { compiler := protocompile.Compiler{
for _, relProtoFile := range files { Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{
args := []string{ ImportPaths: []string{pwd},
"--go_out", pwd, }),
"--go_opt", "paths=source_relative", SourceInfoMode: protocompile.SourceInfoStandard,
"--go-grpc_out", pwd,
"--go-grpc_opt", "paths=source_relative",
"--plugin", "protoc-gen-go=" + filepath.Join(GOBIN, "protoc-gen-go"+suffix),
"--plugin", "protoc-gen-go-grpc=" + filepath.Join(GOBIN, "protoc-gen-go-grpc"+suffix),
} }
args = append(args, relProtoFile)
cmd := exec.Command(protoc, args...) linkedFiles, err := compiler.Compile(context.Background(), protoFiles...)
cmd.Env = append(cmd.Env, os.Environ()...) if err != nil {
cmd.Dir = pwd fmt.Fprintf(os.Stderr, "Protobuf compilation failed:\n%v\n", err)
output, cmdErr := cmd.CombinedOutput() os.Exit(1)
if len(output) > 0 {
fmt.Println(string(output))
} }
if cmdErr != nil {
fmt.Println(cmdErr) var fdList []protoreflect.FileDescriptor
for _, f := range linkedFiles {
fdList = append(fdList, f)
}
req := &pluginpb.CodeGeneratorRequest{
FileToGenerate: protoFiles,
Parameter: new("paths=source_relative"),
ProtoFile: topoSort(fdList),
}
var diffCount int
generatedFiles := make(map[string]struct{})
for _, plugin := range []string{"protoc-gen-go", "protoc-gen-go-grpc"} {
resp, err := runPlugin(plugin, pwd, req)
if err != nil {
fmt.Fprintf(os.Stderr, "%s failed: %v\n", plugin, err)
os.Exit(1)
}
for _, f := range resp.File {
targetPath := filepath.Join(pwd, filepath.FromSlash(f.GetName()))
generatedFiles[targetPath] = struct{}{}
content := []byte(f.GetContent())
hasDiff := false
existing, err := os.ReadFile(targetPath)
if err != nil {
hasDiff = true
} else {
oldNorm := bytes.ReplaceAll(existing, []byte("\r\n"), []byte("\n"))
newNorm := bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n"))
if !bytes.Equal(oldNorm, newNorm) {
hasDiff = true
}
}
if hasDiff {
diffCount++
fmt.Println(targetPath)
if isCheck {
continue
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating directory: %v\n", err)
os.Exit(1)
}
mode := os.FileMode(0o644)
if stat, err := os.Stat(targetPath); err == nil {
mode = stat.Mode().Perm()
}
if err := os.WriteFile(targetPath, content, mode); err != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", targetPath, err)
os.Exit(1) os.Exit(1)
} }
} }
} }
} }
for _, path := range existingPbFiles {
if _, ok := generatedFiles[path]; !ok {
diffCount++
if isCheck {
fmt.Printf("Orphaned Protobuf file: %s\n", path)
} else {
fmt.Printf("Removing orphaned Protobuf file: %s\n", path)
if err := os.Remove(path); err != nil {
fmt.Fprintf(os.Stderr, "Error removing %s: %v\n", path, err)
os.Exit(1)
}
}
}
}
if isCheck {
if diffCount > 0 {
fmt.Println("Protobuf generated file problem(s) found.")
fmt.Println("Please run 'go run ./infra/vprotogen' to regenerate them.")
os.Exit(1)
}
fmt.Println("All Protobuf generated file check has been passed.")
}
}
+5 -4
View File
@@ -47,7 +47,8 @@ func executeBalancerInfo(cmd *base.Command, args []string) {
conn, ctx, close := dialAPIServer() conn, ctx, close := dialAPIServer()
defer close() defer close()
client := routerService.NewRoutingServiceClient(conn) client := routerService.NewRoutingServiceClient(conn)
r := &routerService.GetBalancerInfoRequest{Tag: unnamedArgs[0]} for _, tag := range unnamedArgs {
r := &routerService.GetBalancerInfoRequest{Tag: tag}
resp, err := client.GetBalancerInfo(ctx, r) resp, err := client.GetBalancerInfo(ctx, r)
if err != nil { if err != nil {
base.Fatalf("failed to get health information: %s", err) base.Fatalf("failed to get health information: %s", err)
@@ -55,11 +56,11 @@ func executeBalancerInfo(cmd *base.Command, args []string) {
if apiJSON { if apiJSON {
showJSONResponse(resp) showJSONResponse(resp)
return } else {
}
showBalancerInfo(resp.Balancer) showBalancerInfo(resp.Balancer)
} }
}
}
func showBalancerInfo(b *routerService.BalancerMsg) { func showBalancerInfo(b *routerService.BalancerMsg) {
const tableIndent = 4 const tableIndent = 4
+2
View File
@@ -92,6 +92,8 @@ func executeAddRules(cmd *base.Command, args []string) {
if err != nil { if err != nil {
base.Fatalf("failed to perform AddRule: %s", err) base.Fatalf("failed to perform AddRule: %s", err)
} }
// if multiple files are specified, use append to merge them instead of overriding previous one
shouldAppend = true
showJSONResponse(resp) showJSONResponse(resp)
} }
} }
+4 -11
View File
@@ -3,6 +3,7 @@ package lua
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
xlua "github.com/xtls/xray-core/common/lua" xlua "github.com/xtls/xray-core/common/lua"
"github.com/xtls/xray-core/main/commands/base" "github.com/xtls/xray-core/main/commands/base"
@@ -32,14 +33,13 @@ func executeSign(cmd *base.Command, args []string) {
cmd.Usage() cmd.Usage()
} }
if !strings.EqualFold(filepath.Ext(args[0]), ".lua") {
base.Fatalf("Lua file extension must be .lua")
}
info, err := os.Stat(args[0]) info, err := os.Stat(args[0])
if err != nil { if err != nil {
base.Fatalf("failed to inspect Lua file: %s", err) base.Fatalf("failed to inspect Lua file: %s", err)
} }
if !info.Mode().IsRegular() {
base.Fatalf("Lua input is not a regular file: %s", args[0])
}
code, err := os.ReadFile(args[0]) code, err := os.ReadFile(args[0])
if err != nil { if err != nil {
base.Fatalf("failed to read Lua file: %s", err) base.Fatalf("failed to read Lua file: %s", err)
@@ -67,14 +67,7 @@ func executeSign(cmd *base.Command, args []string) {
func writeSignedFile(path string, code []byte, mode os.FileMode) error { func writeSignedFile(path string, code []byte, mode os.FileMode) error {
if info, err := os.Stat(path); err == nil { if info, err := os.Stat(path); err == nil {
if !info.Mode().IsRegular() {
return &os.PathError{Op: "write", Path: path, Err: os.ErrInvalid}
}
mode = info.Mode().Perm() mode = info.Mode().Perm()
path, err = filepath.EvalSymlinks(path)
if err != nil {
return err
}
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
return err return err
} }
+2 -10
View File
@@ -58,18 +58,10 @@ func readConfigFile(file *os.File) ([]byte, error) {
return nil, err return nil, err
} }
if info.Mode().IsRegular() { if info.Mode().IsRegular() {
path, err := filepath.Abs(file.Name()) switch strings.ToLower(filepath.Ext(file.Name())) {
if err != nil {
return nil, err
}
path, err = filepath.EvalSymlinks(path)
if err != nil {
return nil, err
}
switch strings.ToLower(filepath.Ext(path)) {
case ".jsonc", ".json", ".yml", ".yaml", ".toml", ".pb", ".protobuf": case ".jsonc", ".json", ".yml", ".yaml", ".toml", ".pb", ".protobuf":
default: default:
return nil, errors.New("unsupported config file extension: ", path) return nil, errors.New("unsupported config file extension: ", file.Name())
} }
} }
return io.ReadAll(file) return io.ReadAll(file)
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/blackhole/config.proto // source: proxy/blackhole/config.proto
package blackhole package blackhole
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/dns/config.proto // source: proxy/dns/config.proto
package dns package dns
+7
View File
@@ -381,6 +381,13 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string,
errors.LogInfoInner(context.Background(), err, "pack message") errors.LogInfoInner(context.Background(), err, "pack message")
b.Release() b.Release()
timer.SetTimeout(0) timer.SetTimeout(0)
return
}
if len(msgBytes) > buf.Size {
b.Release()
timer.SetTimeout(0)
errors.LogError(context.Background(), "DNS response message too large")
return
} }
b.Resize(0, int32(len(msgBytes))) b.Resize(0, int32(len(msgBytes)))
+19 -19
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/dokodemo/config.proto // source: proxy/dokodemo/config.proto
package dokodemo package dokodemo
@@ -24,11 +24,11 @@ const (
type Config struct { type Config struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
// List of networks that the Dokodemo accepts.
AllowedNetworks []net.Network `protobuf:"varint,7,rep,packed,name=allowed_networks,json=allowedNetworks,proto3,enum=xray.common.net.Network" json:"allowed_networks,omitempty"`
RewriteAddress *net.IPOrDomain `protobuf:"bytes,1,opt,name=rewrite_address,json=rewriteAddress,proto3" json:"rewrite_address,omitempty"` RewriteAddress *net.IPOrDomain `protobuf:"bytes,1,opt,name=rewrite_address,json=rewriteAddress,proto3" json:"rewrite_address,omitempty"`
RewritePort uint32 `protobuf:"varint,2,opt,name=rewrite_port,json=rewritePort,proto3" json:"rewrite_port,omitempty"` RewritePort uint32 `protobuf:"varint,2,opt,name=rewrite_port,json=rewritePort,proto3" json:"rewrite_port,omitempty"`
PortMap map[string]string `protobuf:"bytes,3,rep,name=port_map,json=portMap,proto3" json:"port_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` PortMap map[string]string `protobuf:"bytes,3,rep,name=port_map,json=portMap,proto3" json:"port_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
// List of networks that the Dokodemo accepts.
AllowedNetworks []net.Network `protobuf:"varint,7,rep,packed,name=allowed_networks,json=allowedNetworks,proto3,enum=xray.common.net.Network" json:"allowed_networks,omitempty"`
FollowRedirect bool `protobuf:"varint,5,opt,name=follow_redirect,json=followRedirect,proto3" json:"follow_redirect,omitempty"` FollowRedirect bool `protobuf:"varint,5,opt,name=follow_redirect,json=followRedirect,proto3" json:"follow_redirect,omitempty"`
UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@@ -65,6 +65,13 @@ func (*Config) Descriptor() ([]byte, []int) {
return file_proxy_dokodemo_config_proto_rawDescGZIP(), []int{0} return file_proxy_dokodemo_config_proto_rawDescGZIP(), []int{0}
} }
func (x *Config) GetAllowedNetworks() []net.Network {
if x != nil {
return x.AllowedNetworks
}
return nil
}
func (x *Config) GetRewriteAddress() *net.IPOrDomain { func (x *Config) GetRewriteAddress() *net.IPOrDomain {
if x != nil { if x != nil {
return x.RewriteAddress return x.RewriteAddress
@@ -86,13 +93,6 @@ func (x *Config) GetPortMap() map[string]string {
return nil return nil
} }
func (x *Config) GetAllowedNetworks() []net.Network {
if x != nil {
return x.AllowedNetworks
}
return nil
}
func (x *Config) GetFollowRedirect() bool { func (x *Config) GetFollowRedirect() bool {
if x != nil { if x != nil {
return x.FollowRedirect return x.FollowRedirect
@@ -112,11 +112,11 @@ var File_proxy_dokodemo_config_proto protoreflect.FileDescriptor
const file_proxy_dokodemo_config_proto_rawDesc = "" + const file_proxy_dokodemo_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x1bproxy/dokodemo/config.proto\x12\x13xray.proxy.dokodemo\x1a\x18common/net/address.proto\x1a\x18common/net/network.proto\"\xff\x02\n" + "\x1bproxy/dokodemo/config.proto\x12\x13xray.proxy.dokodemo\x1a\x18common/net/address.proto\x1a\x18common/net/network.proto\"\xff\x02\n" +
"\x06Config\x12D\n" + "\x06Config\x12C\n" +
"\x10allowed_networks\x18\a \x03(\x0e2\x18.xray.common.net.NetworkR\x0fallowedNetworks\x12D\n" +
"\x0frewrite_address\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x0erewriteAddress\x12!\n" + "\x0frewrite_address\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x0erewriteAddress\x12!\n" +
"\frewrite_port\x18\x02 \x01(\rR\vrewritePort\x12C\n" + "\frewrite_port\x18\x02 \x01(\rR\vrewritePort\x12C\n" +
"\bport_map\x18\x03 \x03(\v2(.xray.proxy.dokodemo.Config.PortMapEntryR\aportMap\x12C\n" + "\bport_map\x18\x03 \x03(\v2(.xray.proxy.dokodemo.Config.PortMapEntryR\aportMap\x12'\n" +
"\x10allowed_networks\x18\a \x03(\x0e2\x18.xray.common.net.NetworkR\x0fallowedNetworks\x12'\n" +
"\x0ffollow_redirect\x18\x05 \x01(\bR\x0efollowRedirect\x12\x1d\n" + "\x0ffollow_redirect\x18\x05 \x01(\bR\x0efollowRedirect\x12\x1d\n" +
"\n" + "\n" +
"user_level\x18\x06 \x01(\rR\tuserLevel\x1a:\n" + "user_level\x18\x06 \x01(\rR\tuserLevel\x1a:\n" +
@@ -141,13 +141,13 @@ var file_proxy_dokodemo_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_proxy_dokodemo_config_proto_goTypes = []any{ var file_proxy_dokodemo_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.proxy.dokodemo.Config (*Config)(nil), // 0: xray.proxy.dokodemo.Config
nil, // 1: xray.proxy.dokodemo.Config.PortMapEntry nil, // 1: xray.proxy.dokodemo.Config.PortMapEntry
(*net.IPOrDomain)(nil), // 2: xray.common.net.IPOrDomain (net.Network)(0), // 2: xray.common.net.Network
(net.Network)(0), // 3: xray.common.net.Network (*net.IPOrDomain)(nil), // 3: xray.common.net.IPOrDomain
} }
var file_proxy_dokodemo_config_proto_depIdxs = []int32{ var file_proxy_dokodemo_config_proto_depIdxs = []int32{
2, // 0: xray.proxy.dokodemo.Config.rewrite_address:type_name -> xray.common.net.IPOrDomain 2, // 0: xray.proxy.dokodemo.Config.allowed_networks:type_name -> xray.common.net.Network
1, // 1: xray.proxy.dokodemo.Config.port_map:type_name -> xray.proxy.dokodemo.Config.PortMapEntry 3, // 1: xray.proxy.dokodemo.Config.rewrite_address:type_name -> xray.common.net.IPOrDomain
3, // 2: xray.proxy.dokodemo.Config.allowed_networks:type_name -> xray.common.net.Network 1, // 2: xray.proxy.dokodemo.Config.port_map:type_name -> xray.proxy.dokodemo.Config.PortMapEntry
3, // [3:3] is the sub-list for method output_type 3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type 3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name 3, // [3:3] is the sub-list for extension type_name
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/freedom/config.proto // source: proxy/freedom/config.proto
package freedom package freedom
+10 -5
View File
@@ -229,14 +229,19 @@ func (h *Handler) blockDelay(rule *FinalRule) time.Duration {
func (h *Handler) blackhole(ctx context.Context, input buf.Reader, output buf.Writer, rule *FinalRule, dest *net.Destination) error { func (h *Handler) blackhole(ctx context.Context, input buf.Reader, output buf.Writer, rule *FinalRule, dest *net.Destination) error {
delay := h.blockDelay(rule) delay := h.blockDelay(rule)
errors.LogInfo(ctx, "blocked target: ", *dest, ", blackholing connection for ", delay) errors.LogInfo(ctx, "blocked target: ", *dest, ", blackholing connection for ", delay)
timer := time.AfterFunc(delay, func() { defer common.Close(output)
done := make(chan struct{})
go func() { // a DispatchLink inbound's reader ignores Interrupt, so don't wait for this copy
_ = buf.Copy(input, buf.Discard)
close(done)
}()
select {
case <-done:
case <-time.After(delay):
common.Interrupt(input) common.Interrupt(input)
common.Interrupt(output) common.Interrupt(output)
errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *dest) errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *dest)
}) }
defer timer.Stop()
defer common.Close(output)
_ = buf.Copy(input, buf.Discard)
return nil return nil
} }
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/http/config.proto // source: proxy/http/config.proto
package http package http
+3 -2
View File
@@ -312,8 +312,9 @@ func (s *Server) handlePlainHTTP(ctx context.Context, request *http.Request, wri
// Sometimes, server might send 1xx response to client // Sometimes, server might send 1xx response to client
// it should not be processed by http proxy handler, just forward it to client // it should not be processed by http proxy handler, just forward it to client
func readResponseAndHandle100Continue(r *bufio.Reader, req *http.Request, writer io.Writer) (*http.Response, error) { func readResponseAndHandle100Continue(r *bufio.Reader, req *http.Request, writer io.Writer) (*http.Response, error) {
// have a little look of response // Wait for the HTTP/1.x status prefix before inspecting the buffered response.
peekBytes, err := r.Peek(56) r.Peek(12)
peekBytes, err := r.Peek(min(56, r.Buffered()))
if err == nil || err == bufio.ErrBufferFull { if err == nil || err == bufio.ErrBufferFull {
str := string(peekBytes) str := string(peekBytes)
ResponseLine := strings.Split(str, "\r\n")[0] ResponseLine := strings.Split(str, "\r\n")[0]
+2 -1
View File
@@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"testing" "testing"
"testing/iotest"
) )
// A malformed upstream response containing a bare '\n' before the real // A malformed upstream response containing a bare '\n' before the real
@@ -29,7 +30,7 @@ func TestReadResponseAndHandle100ContinueDoesNotPanicOnEarlyNewline(t *testing.T
func TestReadResponseAndHandle100ContinueForwardsAndParsesFinalResponse(t *testing.T) { func TestReadResponseAndHandle100ContinueForwardsAndParsesFinalResponse(t *testing.T) {
payload := "HTTP/1.1 100 Continue\r\n\r\n" + payload := "HTTP/1.1 100 Continue\r\n\r\n" +
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello" "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
r := bufio.NewReader(bytes.NewReader([]byte(payload))) r := bufio.NewReader(iotest.OneByteReader(bytes.NewReader([]byte(payload))))
req, err := http.NewRequest("GET", "http://example.com/", nil) req, err := http.NewRequest("GET", "http://example.com/", nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/hysteria/account/config.proto // source: proxy/hysteria/account/config.proto
package account package account
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/hysteria/config.proto // source: proxy/hysteria/config.proto
package hysteria package hysteria
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/loopback/config.proto // source: proxy/loopback/config.proto
package loopback package loopback
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/masque/config.proto // source: proxy/masque/config.proto
package masque package masque
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/shadowsocks/config.proto // source: proxy/shadowsocks/config.proto
package shadowsocks package shadowsocks
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/shadowsocks_2022/config.proto // source: proxy/shadowsocks_2022/config.proto
package shadowsocks_2022 package shadowsocks_2022
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/socks/config.proto // source: proxy/socks/config.proto
package socks package socks
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/trojan/config.proto // source: proxy/trojan/config.proto
package trojan package trojan
+15 -4
View File
@@ -23,6 +23,10 @@ For more advanced routing policies or rules, OS level configuration can still ma
This keeps complex system level routing and rules in a single place of responsibility - the OS itself. \ This keeps complex system level routing and rules in a single place of responsibility - the OS itself. \
Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-networkd) can be found at the end of this README. Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-networkd) can be found at the end of this README.
Packets are handled by Xray-core's own lightweight TCP/IP stack, which replaced gVisor (XTLS/Xray-core#7130).
With `autoSystemRoutingTable`, Xray's own connections are kept out of the TUN through `autoOutboundsInterface` (then `"auto"` by default): each of their sockets is bound to another interface, the one named there, or with `"auto"` the one of the default route (Android needs the name). Linux and Android bind with `SO_BINDTODEVICE`, Windows with `IP_UNICAST_IF` and `IPV6_UNICAST_IF`, macOS and iOS with `IP_BOUND_IF` and `IPV6_BOUND_IF`, and FreeBSD switches them to another routing table (see [FreeBSD SUPPORT](#freebsd-support)). Connections to loopback addresses, TCP and UDP, stay unbound. If a socket cannot be bound, the connection fails rather than leave unbound, where it could loop back into the TUN (XTLS/Xray-core#7173).
### SYSTEM DNS ON LINUX AND FREEBSD (`autoSystemDnsToGateway`) ### SYSTEM DNS ON LINUX AND FREEBSD (`autoSystemDnsToGateway`)
On Linux and FreeBSD, setting `autoSystemDnsToGateway` to `true` lets the inbound point the system resolver at the tun interface, so name lookups resolve through Xray instead of going out over the physical link. It is off by default, and it exists only on those two systems. This section describes Linux; the FreeBSD differences follow in [SYSTEM DNS ON FREEBSD](#system-dns-on-freebsd). On Linux and FreeBSD, setting `autoSystemDnsToGateway` to `true` lets the inbound point the system resolver at the tun interface, so name lookups resolve through Xray instead of going out over the physical link. It is off by default, and it exists only on those two systems. This section describes Linux; the FreeBSD differences follow in [SYSTEM DNS ON FREEBSD](#system-dns-on-freebsd).
@@ -215,12 +219,16 @@ All in all systemd-networkd and its derivatives (like netplan or NetworkManager)
Windows version of the same functionality is implemented through Wintun library. \ Windows version of the same functionality is implemented through Wintun library. \
To make it start, wintun.dll specific for your Windows/arch must be present next to Xray.exe binary. To make it start, wintun.dll specific for your Windows/arch must be present next to Xray.exe binary.
After the start network adapter with the name you chose in the config will be created in the system, and exist while Xray is running. After the start network adapter with the name you chose in the config will be created in the system, and exist while Xray is running. A Wintun adapter that already has that name is reused.
With IPv6 disabled system-wide (the `DisabledComponents` registry value), the adapter has no IPv6, and the TUN starts with IPv4 only, unless `gateway` or `autoSystemRoutingTable` has IPv6 entries, which then keep it from starting.
When `dns` is set, those servers are applied to the adapter. Windows is kept from registering the TUN's addresses in DNS, and its DNS cache is flushed when the TUN starts and stops. When `dns` is set, those servers are applied to the adapter. Windows is kept from registering the TUN's addresses in DNS, and its DNS cache is flushed when the TUN starts and stops.
With `autoSystemWfpBlockLeak`, which needs `autoSystemRoutingTable` (the config is rejected otherwise), Xray also adds Windows Filtering Platform filters that keep two kinds of traffic of every program but Xray itself from leaving outside the TUN, each chosen by a value in the list, e.g. `"autoSystemWfpBlockLeak": ["dns", "misconfigtun"]`: With `autoSystemWfpBlockLeak`, which needs `autoSystemRoutingTable` (the config is rejected otherwise), Xray also adds Windows Filtering Platform filters that keep traffic of every program but Xray itself from leaving outside the TUN, each kind chosen by a value in the list, e.g. `"autoSystemWfpBlockLeak": ["dns", "misconfigtun"]`:
- `"dns"` (needs `dns`, the config is rejected otherwise): DNS (port 53) only goes through the TUN. Windows keeps sending name queries to the DNS servers of the other interfaces as well, out through those interfaces whatever the routes say, and other programs reach a resolver on the local network (e.g. `192.168.1.1` handed out by DHCP) through its more specific LAN route instead of the TUN. On Windows 11 and Server 2022 and later, where those queries may also go over HTTPS or TLS, Windows' DNS Client service cannot connect outside the TUN at all, except for name resolution on the local network (LLMNR, mDNS). The `dns` servers therefore have to lie within `gateway` or `autoSystemRoutingTable` (a warning is logged otherwise), and DNS servers that should be reached directly belong in Xray's own `dns` settings. - `"tcp/<port>"`, `"udp/<port>"`: connections to that TCP or UDP port only go through the TUN, e.g. `"tcp/853"`.
- `"windnsclient"`: Windows' DNS Client service cannot connect outside the TUN at all, except for name resolution on the local network (LLMNR, mDNS). On Windows 11 and Server 2022 and later, it may send name queries over HTTPS or TLS, to any port. Earlier versions only query port 53, so there it does nothing.
- `"dns"`: stands for `"udp/53"`, `"tcp/53"`, `"windnsclient"`, `"tcp/853"` (DNS over TLS) and `"udp/853"` (DNS over QUIC). Windows keeps sending name queries to the DNS servers of the other interfaces as well, out through those interfaces whatever the routes say, and other programs reach a resolver on the local network (e.g. `192.168.1.1` handed out by DHCP) through its more specific LAN route instead of the TUN. Keeping the system's DNS inside the TUN like this, with `"dns"`, `"udp/53"`, `"tcp/53"` or `"windnsclient"`, needs `dns` (the config is rejected otherwise). The `dns` servers then have to lie within `gateway` or `autoSystemRoutingTable` (a warning is logged otherwise), and DNS servers that should be reached directly belong in Xray's own `dns` settings.
- `"misconfigtun"`: an IP version without routes in `autoSystemRoutingTable`, IPv4 or IPv6, is blocked entirely, in both directions, as it would bypass the TUN. Only loopback and what Windows itself needs on the local link (DHCP, and for IPv6 neighbor and multicast listener discovery) remain allowed. An address of that version in `gateway` is not needed: without one, Windows gives the TUN link-local addresses itself, an IPv6 one at once and an IPv4 one from `169.254.0.0/16` after some seconds (until then, IPv4 routed to the TUN is unreachable), and what is routed to the TUN goes through it with those. - `"misconfigtun"`: an IP version without routes in `autoSystemRoutingTable`, IPv4 or IPv6, is blocked entirely, in both directions, as it would bypass the TUN. Only loopback and what Windows itself needs on the local link (DHCP, and for IPv6 neighbor and multicast listener discovery) remain allowed. An address of that version in `gateway` is not needed: without one, Windows gives the TUN link-local addresses itself, an IPv6 one at once and an IPv4 one from `169.254.0.0/16` after some seconds (until then, IPv4 routed to the TUN is unreachable), and what is routed to the TUN goes through it with those.
With the filters in place, Xray's own connections out also get past Windows Firewall's block rules (other firewalls may still block them), while connections to Xray's inbounds stay subject to them. With the filters in place, Xray's own connections out also get past Windows Firewall's block rules (other firewalls may still block them), while connections to Xray's inbounds stay subject to them.
@@ -233,6 +241,8 @@ If the filters cannot be added, Xray does not start. They are removed when Xray
`autoOutboundsInterface` (the default with `autoSystemRoutingTable`) keeps Xray's own connections out of the TUN by binding them to another interface, which Windows only honors while that interface has weak host send and forwarding off for the IP versions routed to the TUN. Otherwise, Windows sends them into the TUN, from that interface's address, and they stall. While the TUN runs, Xray therefore turns weak host send off on that interface, and on again when it stops or another interface takes over. Forwarding cannot be turned off this way, as Mobile Hotspot and Internet Connection Sharing need it, so a warning is logged while it is on. Having the hotspot share the TUN instead of that interface (Settings, Mobile hotspot, Share my internet connection from) moves forwarding to the TUN, where it does no harm, and sends the hotspot's devices through Xray as well. `autoOutboundsInterface` (the default with `autoSystemRoutingTable`) keeps Xray's own connections out of the TUN by binding them to another interface, which Windows only honors while that interface has weak host send and forwarding off for the IP versions routed to the TUN. Otherwise, Windows sends them into the TUN, from that interface's address, and they stall. While the TUN runs, Xray therefore turns weak host send off on that interface, and on again when it stops or another interface takes over. Forwarding cannot be turned off this way, as Mobile Hotspot and Internet Connection Sharing need it, so a warning is logged while it is on. Having the hotspot share the TUN instead of that interface (Settings, Mobile hotspot, Share my internet connection from) moves forwarding to the TUN, where it does no harm, and sends the hotspot's devices through Xray as well.
An outbound interface with only IPv4 or only IPv6 works as well: a UDP socket opened for both IP versions, which such an interface cannot be bound to, is opened again for the destination's version only, and IPv6 TCP is bound for IPv6 alone, so connections of the missing version fail rather than leave through another interface (XTLS/Xray-core#7214).
You can give the adapter ip address manually, you can live Windows to give it autogenerated ip address (which take few seconds), it doesn't matter, the traffic going _through_ the interface will be forwarded into the app for proxying. \ You can give the adapter ip address manually, you can live Windows to give it autogenerated ip address (which take few seconds), it doesn't matter, the traffic going _through_ the interface will be forwarded into the app for proxying. \
Minimal configuration that will work for local machine is routing passing the traffic on-link through the interface. Minimal configuration that will work for local machine is routing passing the traffic on-link through the interface.
You will need the interface id for that, unfortunately it is going to change with every Xray start due to implementation ambiguity between Xray and wintun driver. You will need the interface id for that, unfortunately it is going to change with every Xray start due to implementation ambiguity between Xray and wintun driver.
@@ -307,6 +317,7 @@ sudo route add -inet6 -host 2606:4700:4700::1111 -iface utun10
sudo route add -inet6 -host 2606:4700:4700::1001 -iface utun10 sudo route add -inet6 -host 2606:4700:4700::1001 -iface utun10
``` ```
Alternatively, configure `autoSystemRoutingTable` and Xray will add and remove those system routes while it is running. Alternatively, configure `autoSystemRoutingTable` and Xray will add and remove those system routes while it is running.
Xray's own connections are then kept out of the TUN through `autoOutboundsInterface`: an IPv4 socket is bound with `IP_BOUND_IF`, an IPv6 or dual-stack one with `IPV6_BOUND_IF`, which covers the IPv4 traffic of a dual-stack socket as well, since macOS rejects `IP_BOUND_IF` on IPv6 sockets (XTLS/Xray-core#7230).
Important to remember that everything written above about Linux routing concept, also apply to Mac OS X. If you simply route default route through utun interface, that will result network loop and immediate network failure. Important to remember that everything written above about Linux routing concept, also apply to Mac OS X. If you simply route default route through utun interface, that will result network loop and immediate network failure.
## ANDROID SUPPORT ## ANDROID SUPPORT
@@ -338,7 +349,7 @@ let tunFd = ((0 ... 1024).first { (_ fd: Int32) -> Bool in var len = socklen_t(b
}! }!
``` ```
Set the environment variable `xray.tun.fd` (or `XRAY_TUN_FD`) to the fd number before starting Xray. This can be done from Swift/Objective-C or by exposing a Go function via gomobile bindings. Set the environment variable `xray.tun.fd` (or `XRAY_TUN_FD`) to the fd number before starting Xray. This can be done from Swift/Objective-C or by exposing a Go function via gomobile bindings. Xray works on a duplicate of the fd and closes only that, so the fd itself stays NetworkExtension's (XTLS/Xray-core#6883).
Build using gomobile for iOS framework integration: Build using gomobile for iOS framework integration:
``` ```
+4 -5
View File
@@ -1,18 +1,17 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/tun/config.proto // source: proxy/tun/config.proto
package tun package tun
import ( import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
sync "sync" sync "sync"
unsafe "unsafe" unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
) )
const ( const (
+4 -7
View File
@@ -558,19 +558,16 @@ func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
} }
func setinterface(network, address string, fd uintptr, iface *net.Interface) error { func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
var err1, err2 error
switch network { switch network {
case "tcp6", "udp6", "ip6": case "tcp6", "udp6", "ip6":
err1 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index) // IPV6_BOUND_IF binds the IPv4 traffic of a dual-stack socket as
fallthrough // well, and an IPv6 socket rejects IP_BOUND_IF with EINVAL.
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index)
case "tcp4", "udp4", "ip4": case "tcp4", "udp4", "ip4":
err2 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index) return unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index)
default: default:
panic(network + " " + address) panic(network + " " + address)
} }
return errors.Join(err1, err2)
} }
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
+40
View File
@@ -3,7 +3,10 @@
package tun package tun
import ( import (
"context"
"net"
"sync" "sync"
"syscall"
"testing" "testing"
"time" "time"
@@ -231,3 +234,40 @@ func TestWaitKqueueReportsPersistentFailure(t *testing.T) {
} }
} }
} }
// TestSetinterfaceSocketFamilies binds each kind of socket Xray's dialers
// open to lo0, including the dual-stack one Go opens for UDP.
func TestSetinterfaceSocketFamilies(t *testing.T) {
lo, err := net.InterfaceByName("lo0")
if err != nil {
t.Skip(err)
}
lc := net.ListenConfig{Control: func(network, address string, c syscall.RawConn) error {
var err error
if controlErr := c.Control(func(fd uintptr) {
err = setinterface(network, address, fd, lo)
}); controlErr != nil {
return controlErr
}
return err
}}
for _, s := range []struct{ network, address string }{
{"udp", "0.0.0.0:0"}, // dual-stack
{"udp4", "127.0.0.1:0"},
{"udp6", "[::1]:0"},
{"tcp4", "127.0.0.1:0"},
{"tcp6", "[::1]:0"},
} {
var closer interface{ Close() error }
if s.network[:3] == "udp" {
closer, err = lc.ListenPacket(context.Background(), s.network, s.address)
} else {
closer, err = lc.Listen(context.Background(), s.network, s.address)
}
if err != nil {
t.Errorf("%s %s: %v", s.network, s.address, err)
continue
}
closer.Close()
}
}
+46 -21
View File
@@ -247,30 +247,31 @@ startOver:
} }
// With autoSystemWfpBlockLeak, once the system routes lead to the TUN, // With autoSystemWfpBlockLeak, once the system routes lead to the TUN,
// keep DNS ("dns", if dns is set), and an IP version no route of which // keep connections to the ports given ("dns" stands for those of DNS),
// leads to the TUN ("misconfigtun"), from leaving through the other // Windows' DNS Client service, and an IP version no route of which leads
// interfaces. Addresses do not matter: without one of a version in // to the TUN ("misconfigtun"), from leaving through the other interfaces.
// gateway, Windows gives the TUN a link-local one. // Addresses do not matter: without one of a version in gateway, Windows
leaks := t.options.AutoSystemWfpBlockLeak // gives the TUN a link-local one.
blockDNS := slices.Contains(leaks, "dns") && len(dns) > 0 leaks, err := ExpandWfpBlockLeak(t.options.AutoSystemWfpBlockLeak)
blockIPv4 := slices.Contains(leaks, "misconfigtun") && !route4 if err != nil {
blockIPv6 := slices.Contains(leaks, "misconfigtun") && !route6 return err
if (route4 || route6) && (blockDNS || blockIPv4 || blockIPv6) { }
if t.wfp, err = blockLeaks(t.luid, blockDNS, blockIPv4, blockIPv6); err != nil { block := newLeakFilters(leaks, route4, route6)
var blocked []string if route4 || route6 {
for _, b := range []struct { if slices.Contains(leaks, "windnsclient") && !block.dnsClient {
on bool errors.LogInfo(context.Background(), "[tun] windnsclient skipped: before Windows 11 and Server 2022, the DNS Client service only queries port 53")
what string }
}{{blockDNS, "DNS"}, {blockIPv4, "IPv4"}, {blockIPv6, "IPv6"}} { if block.systemDNS() && len(dns) == 0 {
if b.on { return errors.New("keeping the system's DNS inside the TUN needs dns to be set")
blocked = append(blocked, b.what)
} }
} }
if blocked := block.String(); (route4 || route6) && blocked != "" {
if t.wfp, err = blockLeaks(t.luid, block); err != nil {
// Rather no TUN than a leaking one. // Rather no TUN than a leaking one.
return errors.New("unable to block ", strings.Join(blocked, " and "), " outside the TUN (remove autoSystemWfpBlockLeak to run without)").Base(err) return errors.New("unable to block ", blocked, " outside the TUN (remove autoSystemWfpBlockLeak to run without)").Base(err)
} }
errors.LogInfo(context.Background(), "[tun] outside the TUN, blocked DNS: ", blockDNS, ", blocked IPv4: ", blockIPv4, ", blocked IPv6: ", blockIPv6) errors.LogInfo(context.Background(), "[tun] blocked outside the TUN: ", blocked)
if blockDNS { if block.systemDNS() {
covered := slices.Clone(addresses) covered := slices.Clone(addresses)
for _, route := range routesData { for _, route := range routesData {
covered = append(covered, route.Destination) covered = append(covered, route.Destination)
@@ -401,6 +402,15 @@ func resolveOnOwn() *savedResolver {
if internet.IsSkippedDNSServer(address) { if internet.IsSkippedDNSServer(address) {
return nil, errors.New("skipped DNS server ", address) return nil, errors.New("skipped DNS server ", address)
} }
// A socket of the server's IP version only needs that version bound
// to the interface, which a dual-stack one would need both of.
if ap, err := netip.ParseAddrPort(address); err == nil && (network == "udp" || network == "tcp") {
if ap.Addr().Unmap().Is4() {
network += "4"
} else {
network += "6"
}
}
return dialer.DialContext(ctx, network, address) return dialer.DialContext(ctx, network, address)
} }
net.DefaultResolver.PreferGo = true net.DefaultResolver.PreferGo = true
@@ -558,10 +568,22 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
switch network { switch network {
case "tcp6", "udp6", "ip6": case "tcp6", "udp6", "ip6":
dualStack := false
if network == "udp6" {
ipv6Only, err := windows.GetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_V6ONLY)
if err != nil {
return internet.NewBindError(err)
}
dualStack = ipv6Only == 0
}
err1 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_UNICAST_IF, iface.Index) err1 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_UNICAST_IF, iface.Index)
if network == "udp6" { if network == "udp6" {
err2 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_MULTICAST_IF, iface.Index) err2 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_MULTICAST_IF, iface.Index)
} }
// Only dual-stack UDP can send to both IPv4 and IPv6 destinations.
if !dualStack {
break
}
fallthrough fallthrough
case "tcp4", "udp4", "ip4": case "tcp4", "udp4", "ip4":
err3 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, IP_UNICAST_IF, *(*int)(unsafe.Pointer(&index[0]))) err3 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, IP_UNICAST_IF, *(*int)(unsafe.Pointer(&index[0])))
@@ -572,7 +594,10 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
panic(network + " " + address) panic(network + " " + address)
} }
return errors.Combine(err1, err2, err3, err4) if err := errors.Combine(err1, err2, err3, err4); err != nil {
return internet.NewBindError(err)
}
return nil
} }
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
+103 -38
View File
@@ -7,6 +7,7 @@ import (
"os" "os"
"runtime" "runtime"
"slices" "slices"
"strings"
"unsafe" "unsafe"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
@@ -170,19 +171,79 @@ func condition(field *windows.GUID, typ uint32, value uintptr) fwpmFilterConditi
} }
} }
// leakFilters is what blockLeaks keeps from leaving outside the TUN.
type leakFilters struct {
ports []leakPort // connections to these ports
dnsClient bool // Windows' DNS Client service
ipv4, ipv6 bool // an IP version altogether
}
// newLeakFilters turns expanded autoSystemWfpBlockLeak values into the filters
// for a TUN that the routes of route4 (IPv4) and route6 (IPv6) lead to.
func newLeakFilters(leaks []string, route4, route6 bool) leakFilters {
var block leakFilters
for _, leak := range leaks {
switch leak {
case "windnsclient":
block.dnsClient = dnsClientUsesAnyPort()
case "misconfigtun":
block.ipv4, block.ipv6 = !route4, !route6
default:
if port, ok := parseLeakPort(leak); ok {
block.ports = append(block.ports, port)
}
}
}
return block
}
// String lists what block keeps inside the TUN, or is empty for nothing.
func (block leakFilters) String() string {
var blocked []string
for _, port := range block.ports {
blocked = append(blocked, port.String())
}
for _, b := range []struct {
on bool
what string
}{{block.dnsClient, "windnsclient"}, {block.ipv4, "IPv4"}, {block.ipv6, "IPv6"}} {
if b.on {
blocked = append(blocked, b.what)
}
}
return strings.Join(blocked, ", ")
}
// systemDNS tells whether block keeps the system's DNS inside the TUN.
func (block leakFilters) systemDNS() bool {
return block.dnsClient || slices.ContainsFunc(block.ports, func(port leakPort) bool { return port.port == 53 })
}
// dnsClientUsesAnyPort tells whether Windows' DNS Client service may send its
// queries over HTTPS or TLS, and so to any port, which it does since Windows 11
// and Server 2022 (build 20348). Earlier versions only query port 53, and may
// run the service in one process with others, which its filters would catch
// as well.
func dnsClientUsesAnyPort() bool {
_, _, build := windows.RtlGetNtVersionNumbers()
return build >= 20348
}
// blockLeaks keeps traffic from leaving through interfaces other than tun, // blockLeaks keeps traffic from leaving through interfaces other than tun,
// for every program but Xray itself, whose outbounds (DNS included) use the // for every program but Xray itself, whose outbounds (DNS included) use the
// other interfaces on purpose: // other interfaces on purpose:
// //
// - dns: DNS (port 53) may only go through the TUN. Windows sends a name // - ports: connections to these TCP or UDP ports may only go through the
// query to the DNS servers of all interfaces, not only to those of the TUN: // TUN, those of DNS (53), DNS over TLS (TCP 853) and DNS over QUIC (UDP
// to the first server of each interface, then to all of them when no answer // 853) with "dns". Windows sends a name query to the DNS servers of all
// arrives within a second or two. It sends the queries for the servers of // interfaces, not only to those of the TUN: to the first server of each
// an interface out through that interface, whatever the routes say, and // interface, then to all of them when no answer arrives within a second or
// other programs reach an on-link resolver, like 192.168.1.1 from DHCP, // two. It sends the queries for the servers of an interface out through
// through its LAN route, which is more specific than the TUN's default // that interface, whatever the routes say, and other programs reach an
// route. Since Windows 11 and Server 2022, Windows may also send its // on-link resolver, like 192.168.1.1 from DHCP, through its LAN route,
// queries over HTTPS or TLS, so there its DNS Client service may not // which is more specific than the TUN's default route.
// - dnsClient: since Windows 11 and Server 2022, Windows may also send its
// queries over HTTPS or TLS, to any port, so its DNS Client service may not
// connect outside the TUN at all, except for name resolution on the local // connect outside the TUN at all, except for name resolution on the local
// link (mDNS, LLMNR). // link (mDNS, LLMNR).
// - ipv4, ipv6: no IPv4, or no IPv6, at all, in either direction, for a TUN // - ipv4, ipv6: no IPv4, or no IPv6, at all, in either direction, for a TUN
@@ -196,7 +257,7 @@ func condition(field *windows.GUID, typ uint32, value uintptr) fwpmFilterConditi
// //
// The filters live in a dynamic WFP session: closing the returned engine handle // The filters live in a dynamic WFP session: closing the returned engine handle
// with closeWFPEngine deletes them, and so does Windows when the process dies. // with closeWFPEngine deletes them, and so does Windows when the process dies.
func blockLeaks(tun winipcfg.LUID, dns, ipv4, ipv6 bool) (windows.Handle, error) { func blockLeaks(tun winipcfg.LUID, block leakFilters) (windows.Handle, error) {
engine, err := openWFPEngine() engine, err := openWFPEngine()
if err != nil { if err != nil {
return 0, err return 0, err
@@ -205,7 +266,7 @@ func blockLeaks(tun winipcfg.LUID, dns, ipv4, ipv6 bool) (windows.Handle, error)
closeWFPEngine(engine) closeWFPEngine(engine)
return 0, errors.New("FwpmTransactionBegin0 failed").Base(err) return 0, errors.New("FwpmTransactionBegin0 failed").Base(err)
} }
err = addLeakFilters(engine, tun, dns, ipv4, ipv6) err = addLeakFilters(engine, tun, block)
if err == nil { if err == nil {
if err = fwpmResult(procFwpmTransactionCommit0.Call(uintptr(engine))); err != nil { if err = fwpmResult(procFwpmTransactionCommit0.Call(uintptr(engine))); err != nil {
err = errors.New("FwpmTransactionCommit0 failed").Base(err) err = errors.New("FwpmTransactionCommit0 failed").Base(err)
@@ -242,7 +303,7 @@ func closeWFPEngine(engine windows.Handle) {
// addLeakFilters adds the filters of blockLeaks in a sublayer of their own. // addLeakFilters adds the filters of blockLeaks in a sublayer of their own.
// blockLeaks runs it in a transaction, so that they take effect all at once. // blockLeaks runs it in a transaction, so that they take effect all at once.
func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, dns, ipv4, ipv6 bool) error { func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters) error {
exe, err := os.Executable() exe, err := os.Executable()
if err != nil { if err != nil {
return err return err
@@ -285,51 +346,55 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, dns, ipv4, ipv6 bo
// hard permit. Connections from outside to Xray get an ordinary one, so // hard permit. Connections from outside to Xray get an ordinary one, so
// that firewalls keep guarding its inbounds. // that firewalls keep guarding its inbounds.
self := condition(&fwpmConditionALEAppID, fwpByteBlobType, uintptr(unsafe.Pointer(appID))) self := condition(&fwpmConditionALEAppID, fwpByteBlobType, uintptr(unsafe.Pointer(appID)))
dns53 := condition(&fwpmConditionIPRemotePort, fwpUint16, 53) // A connection goes through the TUN when its local address is the TUN's,
// DNS goes through the TUN when its local address is the TUN's, and it // and it also leaves, or arrives, through the TUN. The local address alone
// also leaves, or arrives, through the TUN. The local address alone
// decides by default, but with weak host sending or receiving enabled, // decides by default, but with weak host sending or receiving enabled,
// packets of the TUN's address can use other interfaces. (The next hop, // packets of the TUN's address can use other interfaces. (The next hop,
// the interface replies would leave by, is not known for arriving ones.) // the interface replies would leave by, is not known for arriving ones.)
onTUN := func(field *windows.GUID) fwpmFilterCondition0 { onTUN := func(field *windows.GUID) fwpmFilterCondition0 {
return condition(field, fwpUint64, uintptr(unsafe.Pointer(tunLUID))) return condition(field, fwpUint64, uintptr(unsafe.Pointer(tunLUID)))
} }
out := []fwpmFilterCondition0{dns53, onTUN(&fwpmConditionIPLocalInterface), onTUN(&fwpmConditionIPNexthopInterface)}
in := []fwpmFilterCondition0{dns53, onTUN(&fwpmConditionIPLocalInterface), onTUN(&fwpmConditionIPArrivalInterface)}
for _, layer := range []struct { for _, layer := range []struct {
key *windows.GUID key *windows.GUID
selfFlags uint32 selfFlags uint32
throughTUN []fwpmFilterCondition0 via *windows.GUID // the interface that has to be the TUN as well
}{ }{
{&fwpmLayerALEAuthConnectV4, fwpmFilterFlagClearActionRight, out}, {&fwpmLayerALEAuthConnectV4, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface},
{&fwpmLayerALEAuthRecvAcceptV4, 0, in}, {&fwpmLayerALEAuthRecvAcceptV4, 0, &fwpmConditionIPArrivalInterface},
{&fwpmLayerALEAuthConnectV6, fwpmFilterFlagClearActionRight, out}, {&fwpmLayerALEAuthConnectV6, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface},
{&fwpmLayerALEAuthRecvAcceptV6, 0, in}, {&fwpmLayerALEAuthRecvAcceptV6, 0, &fwpmConditionIPArrivalInterface},
} { } {
if err := add(layer.key, "permit Xray", layer.selfFlags, fwpActionPermit, 4, self); err != nil { if err := add(layer.key, "permit Xray", layer.selfFlags, fwpActionPermit, 4, self); err != nil {
return err return err
} }
if dns { for _, port := range block.ports {
if err := add(layer.key, "permit DNS through the TUN", 0, fwpActionPermit, 3, layer.throughTUN...); err != nil { protocol := uintptr(windows.IPPROTO_UDP)
if port.tcp {
protocol = windows.IPPROTO_TCP
}
toPort := []fwpmFilterCondition0{
condition(&fwpmConditionIPProtocol, fwpUint8, protocol),
condition(&fwpmConditionIPRemotePort, fwpUint16, uintptr(port.port)),
}
throughTUN := slices.Concat(toPort, []fwpmFilterCondition0{onTUN(&fwpmConditionIPLocalInterface), onTUN(layer.via)})
if err := add(layer.key, "permit "+port.String()+" through the TUN", 0, fwpActionPermit, 3, throughTUN...); err != nil {
return err return err
} }
if err := add(layer.key, "block DNS", 0, fwpActionBlock, 2, dns53); err != nil { if err := add(layer.key, "block "+port.String(), 0, fwpActionBlock, 2, toPort...); err != nil {
return err return err
} }
} }
} }
// Since Windows 11 and Server 2022 (build 20348), the DNS Client service // Since Windows 11 and Server 2022, the DNS Client service may also send
// may also send the queries for an interface's servers over HTTPS or TLS, // the queries for an interface's servers over HTTPS or TLS, out through
// out through that interface and to any port. So there it may only // that interface and to any port. So there it may only connect through
// connect through the TUN, except for mDNS and LLMNR, which stay on the // the TUN, except for mDNS and LLMNR, which stay on the local link (over
// local link (over an IP version only while it is not blocked altogether). // an IP version only while it is not blocked altogether). Like Windows
// Earlier versions only query port 53, and may run the service in one // Firewall's rules for it, the filters recognize the service by its SID,
// process with others, which the filters would catch as well. Like
// Windows Firewall's rules for it, they recognize the service by its SID,
// which Windows puts in the token of its process: the security descriptor // which Windows puts in the token of its process: the security descriptor
// grants that SID the right to match (FWP_ACTRL_MATCH_FILTER, CC in SDDL). // grants that SID the right to match (FWP_ACTRL_MATCH_FILTER, CC in SDDL).
if _, _, build := windows.RtlGetNtVersionNumbers(); dns && build >= 20348 { if block.dnsClient {
sd, err := windows.SecurityDescriptorFromString("O:SYG:SYD:(A;;CCRC;;;" + dnsClientSID + ")") sd, err := windows.SecurityDescriptorFromString("O:SYG:SYD:(A;;CCRC;;;" + dnsClientSID + ")")
if err != nil { if err != nil {
return err return err
@@ -343,8 +408,8 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, dns, ipv4, ipv6 bo
key *windows.GUID key *windows.GUID
localLink bool localLink bool
}{ }{
{&fwpmLayerALEAuthConnectV4, !ipv4}, {&fwpmLayerALEAuthConnectV4, !block.ipv4},
{&fwpmLayerALEAuthConnectV6, !ipv6}, {&fwpmLayerALEAuthConnectV6, !block.ipv6},
} { } {
if err := add(layer.key, "permit the DNS Client service through the TUN", 0, fwpActionPermit, 3, dnsClient, onTUN(&fwpmConditionIPLocalInterface), onTUN(&fwpmConditionIPNexthopInterface)); err != nil { if err := add(layer.key, "permit the DNS Client service through the TUN", 0, fwpActionPermit, 3, dnsClient, onTUN(&fwpmConditionIPLocalInterface), onTUN(&fwpmConditionIPNexthopInterface)); err != nil {
return err return err
@@ -367,7 +432,7 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, dns, ipv4, ipv6 bo
matchType: fwpMatchFlagsAllSet, matchType: fwpMatchFlagsAllSet,
conditionValue: fwpValue0{typ: fwpUint32, value: fwpConditionFlagIsLoopback}, conditionValue: fwpValue0{typ: fwpUint32, value: fwpConditionFlagIsLoopback},
} }
if ipv4 { if block.ipv4 {
// DHCP keeps the addresses of the other interfaces, which Xray's own // DHCP keeps the addresses of the other interfaces, which Xray's own
// connections use. // connections use.
dhcp := []fwpmFilterCondition0{ dhcp := []fwpmFilterCondition0{
@@ -387,7 +452,7 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, dns, ipv4, ipv6 bo
} }
} }
} }
if ipv6 { if block.ipv6 {
// Neighbor and multicast listener discovery, ICMPv6 130-137 and 143, // Neighbor and multicast listener discovery, ICMPv6 130-137 and 143,
// whose type and code sit where the local and remote port are. // whose type and code sit where the local and remote port are.
discovery := []fwpmFilterCondition0{condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_ICMPV6)} discovery := []fwpmFilterCondition0{condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_ICMPV6)}
+28 -1
View File
@@ -114,11 +114,38 @@ func TestLeakFiltersAccepted(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := addLeakFilters(engine, loopback, true, true, true); err != nil { leaks, err := ExpandWfpBlockLeak([]string{"dns", "misconfigtun", "tcp/443"})
if err != nil {
t.Fatal(err)
}
block := newLeakFilters(leaks, false, false)
block.dnsClient = true // whatever the Windows version
if err := addLeakFilters(engine, loopback, block); err != nil {
skipUnlessElevated(err) skipUnlessElevated(err)
} }
} }
func TestNewLeakFilters(t *testing.T) {
leaks, err := ExpandWfpBlockLeak([]string{"dns", "misconfigtun"})
if err != nil {
t.Fatal(err)
}
block := newLeakFilters(leaks, true, false)
want := "udp/53, tcp/53, tcp/853, udp/853, IPv6"
if dnsClientUsesAnyPort() {
want = "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv6"
}
if got := block.String(); got != want {
t.Errorf("String() = %q, want %q", got, want)
}
if !block.systemDNS() {
t.Error("systemDNS() = false with dns")
}
if block := newLeakFilters([]string{"tcp/853", "udp/853", "misconfigtun"}, true, true); block.String() != "tcp/853, udp/853" || block.systemDNS() {
t.Errorf("tcp/853, udp/853: %q, systemDNS() = %v", block.String(), block.systemDNS())
}
}
func TestDNSClientSID(t *testing.T) { func TestDNSClientSID(t *testing.T) {
sid, _, _, err := windows.LookupSID("", `NT SERVICE\Dnscache`) sid, _, _, err := windows.LookupSID("", `NT SERVICE\Dnscache`)
if err != nil { if err != nil {
+79
View File
@@ -0,0 +1,79 @@
package tun
import (
"slices"
"strconv"
"strings"
"github.com/xtls/xray-core/common/errors"
)
// dnsLeaks is what "dns" in autoSystemWfpBlockLeak stands for: DNS over UDP and
// TCP, Windows' DNS Client service, which may also use other protocols and
// ports, DNS over TLS, and DNS over QUIC.
var dnsLeaks = []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853"}
// leakPort is a "tcp/<port>" or "udp/<port>" value of autoSystemWfpBlockLeak:
// connections to that port may only go through the TUN.
type leakPort struct {
tcp bool
port uint16
}
func (p leakPort) String() string {
if p.tcp {
return "tcp/" + strconv.Itoa(int(p.port))
}
return "udp/" + strconv.Itoa(int(p.port))
}
// parseLeakPort parses a lower case "tcp/<port>" or "udp/<port>" value.
func parseLeakPort(value string) (leakPort, bool) {
protocol, port, ok := strings.Cut(value, "/")
if !ok || protocol != "tcp" && protocol != "udp" {
return leakPort{}, false
}
n, err := strconv.ParseUint(port, 10, 16)
if err != nil || n == 0 {
return leakPort{}, false
}
return leakPort{tcp: protocol == "tcp", port: uint16(n)}, true
}
// ExpandWfpBlockLeak checks the values of autoSystemWfpBlockLeak and returns
// them in lower case, in order and without duplicates, with "dns" replaced by
// what it stands for.
func ExpandWfpBlockLeak(values []string) ([]string, error) {
var leaks []string
add := func(leak string) {
if !slices.Contains(leaks, leak) {
leaks = append(leaks, leak)
}
}
for _, value := range values {
switch value = strings.ToLower(value); value {
case "dns":
for _, leak := range dnsLeaks {
add(leak)
}
case "windnsclient", "misconfigtun":
add(value)
default:
port, ok := parseLeakPort(value)
if !ok {
return nil, errors.New("unknown autoSystemWfpBlockLeak value: ", value)
}
add(port.String())
}
}
return leaks, nil
}
// BlocksSystemDNS tells whether expanded autoSystemWfpBlockLeak values keep the
// system's DNS (port 53, or Windows' DNS Client service) inside the TUN, which
// then needs DNS servers of its own.
func BlocksSystemDNS(leaks []string) bool {
return slices.ContainsFunc(leaks, func(leak string) bool {
return leak == "udp/53" || leak == "tcp/53" || leak == "windnsclient"
})
}
+40
View File
@@ -0,0 +1,40 @@
package tun
import (
"slices"
"testing"
)
func TestExpandWfpBlockLeak(t *testing.T) {
for _, c := range []struct {
values []string
leaks []string
systemDNS bool
}{
{nil, nil, false},
{[]string{"dns"}, []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853"}, true},
{[]string{"DNS", "MisconfigTUN"}, []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853", "misconfigtun"}, true},
{[]string{"tcp/853", "dns", "TCP/0853"}, []string{"tcp/853", "udp/53", "tcp/53", "windnsclient", "udp/853"}, true},
{[]string{"tcp/853", "udp/853", "misconfigtun"}, []string{"tcp/853", "udp/853", "misconfigtun"}, false},
{[]string{"udp/65535", "tcp/1"}, []string{"udp/65535", "tcp/1"}, false},
{[]string{"tcp/53"}, []string{"tcp/53"}, true},
{[]string{"windnsclient"}, []string{"windnsclient"}, true},
} {
leaks, err := ExpandWfpBlockLeak(c.values)
if err != nil {
t.Errorf("%v: %v", c.values, err)
continue
}
if !slices.Equal(leaks, c.leaks) {
t.Errorf("%v: got %v, want %v", c.values, leaks, c.leaks)
}
if got := BlocksSystemDNS(leaks); got != c.systemDNS {
t.Errorf("%v: BlocksSystemDNS() = %v", c.values, got)
}
}
for _, value := range []string{"", "ip", "tcp", "tcp/", "tcp/0", "udp/65536", "tcp/-1", "tcp/+53", "tcp/53/1", "udp/abc", "icmp/1", "tcp:53", " dns"} {
if _, err := ExpandWfpBlockLeak([]string{value}); err == nil {
t.Errorf("%q was accepted", value)
}
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/vless/account.proto // source: proxy/vless/account.proto
package vless package vless
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.12
// protoc v6.33.5 // protoc (unknown)
// source: proxy/vless/encoding/addons.proto // source: proxy/vless/encoding/addons.proto
package encoding package encoding

Some files were not shown because too many files have changed in this diff Show More