Compare commits

..
5 Commits
Author SHA1 Message Date
Fangliding fa58459817 Refactor 2026-10-21 13:20:30 +08:00
Fangliding 8d0deebf86 simple clear 2026-10-18 22:30:54 +08:00
Fangliding 0396b34eff test 2026-10-18 22:10:39 +08:00
Roman Rublev 83147cf3a7 SS2022 inbound: Don't end a UDP session when its first source address expires
The relay session context was derived from the inbound connection of the
source address that created the session. After a client changes its source
address (NAT rebinding), the UDP worker of the old address is cleaned up after
~2 minutes of inactivity and cancels that context, which tears down the
dispatched outbound flow; the next packet then creates a brand-new session and
outbound flow (new local port on the server, reset replay window).

Sessions are routed by client session ID (SIP022 3.2.4), so detach the session
context from the originating connection's cancellation and let the session's
own inactivity timer govern its lifetime.
2026-10-18 16:28:31 +03:00
Roman Rublev 8d0a7cf856 SS2022 inbound: Fix data races and duplicate acceptance when decoding one UDP session concurrently
Packets of one client session can be processed by two inbound workers at
the same time (e.g. the client's old and new source address after a NAT
rebinding). The sliding window was checked and updated in two separate
critical sections, and clientBodyCipher and the session's User were read
without the lock, so the same packet ID could be accepted more than once
and go test -race reported data races.

Re-check the window and publish the cached body cipher in one critical
section after the packet is fully validated, and always initialize the
session's User under the lock.
2026-10-18 16:28:31 +03:00
182 changed files with 1517 additions and 6034 deletions
+11 -5
View File
@@ -9,8 +9,8 @@ ARG TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main
# Download geodat into a staging directory # Download geodat into a staging directory
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geoip/geoip-full.dat /tmp/geodat/geoip.dat ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat /tmp/geodat/geoip.dat
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geosite/geosite-expand.dat /tmp/geodat/geosite.dat ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat /tmp/geodat/geosite.dat
RUN mkdir -p /tmp/empty RUN mkdir -p /tmp/empty
@@ -40,15 +40,21 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files
RUN mkdir -p /tmp/var/log/xray && touch \
/tmp/var/log/xray/access.log \
/tmp/var/log/xray/error.log
# Build finally image # Build finally image
FROM gcr.io/distroless/static:nonroot FROM gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/ COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:65532 --chmod=750 /tmp/empty /usr/local/etc/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:65532 --chmod=640 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/ COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=65532:65532 --chmod=700 /tmp/empty /var/log/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
COPY --from=build --chown=65532:65532 --chmod=600 /tmp/var/log/xray/*.log /var/log/xray/
VOLUME /usr/local/etc/xray VOLUME /usr/local/etc/xray
VOLUME /var/log/xray VOLUME /var/log/xray
+11 -5
View File
@@ -9,8 +9,8 @@ ARG TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main
# Download geodat into a staging directory # Download geodat into a staging directory
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geoip/geoip-full.dat /tmp/geodat/geoip.dat ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat /tmp/geodat/geoip.dat
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geosite/geosite-expand.dat /tmp/geodat/geosite.dat ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat /tmp/geodat/geosite.dat
RUN mkdir -p /tmp/empty RUN mkdir -p /tmp/empty
@@ -40,6 +40,11 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files
RUN mkdir -p /tmp/var/log/xray && touch \
/tmp/var/log/xray/access.log \
/tmp/var/log/xray/error.log
# Build finally image # Build finally image
# Note on Distroless Base Image and Architecture Support: # Note on Distroless Base Image and Architecture Support:
# - The official 'gcr.io/distroless/static' image provided by Google only supports a limited set of architectures for Linux: # - The official 'gcr.io/distroless/static' image provided by Google only supports a limited set of architectures for Linux:
@@ -55,9 +60,10 @@ FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/ COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:65532 --chmod=750 /tmp/empty /usr/local/etc/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:65532 --chmod=640 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/ COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=65532:65532 --chmod=700 /tmp/empty /var/log/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
COPY --from=build --chown=65532:65532 --chmod=600 /tmp/var/log/xray/*.log /var/log/xray/
VOLUME /usr/local/etc/xray VOLUME /usr/local/etc/xray
VOLUME /var/log/xray VOLUME /var/log/xray
+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.7.0 uses: docker/login-action@v4.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -40,7 +40,7 @@ jobs:
max_attempts: 60 max_attempts: 60
command: | command: |
[ -d 'resources' ] || mkdir resources [ -d 'resources' ] || mkdir resources
LIST=('XTLS Xray-rules-dat geoip/geoip-lite geoip' 'XTLS Xray-rules-dat geosite/geosite geosite') LIST=('Loyalsoldier v2ray-rules-dat geoip geoip' 'Loyalsoldier v2ray-rules-dat geosite geosite')
for i in "${LIST[@]}" for i in "${LIST[@]}"
do do
INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3,$4}')) INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3,$4}'))
+18 -3
View File
@@ -35,7 +35,24 @@ jobs:
if: steps.check-assets.outputs.missing == 'true' if: steps.check-assets.outputs.missing == 'true'
run: sleep 90 run: sleep 90
check-format-and-proto: check-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:
@@ -51,8 +68,6 @@ 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// source: app/dns/fakedns/fakedns.proto // source: app/dns/fakedns/fakedns.proto
package fakedns package fakedns
+4 -4
View File
@@ -128,7 +128,7 @@ func TestDNSScriptGeoIPFallback(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
script := ` script := `
local servers = require("xray.dns").Servers local servers = require("xray.dns").Servers
local ru_ips = require("xray.geodata").BuildIPMatcher("geoip:ru") local us_ips = require("xray.geodata").BuildIPMatcher("geoip:us")
local by_id = {} local by_id = {}
for _, server in ipairs(servers) do for _, server in ipairs(servers) do
@@ -138,7 +138,7 @@ assert(by_id.primary and by_id.fallback, "primary and fallback DNS servers are r
function HandleDNSQuery(domain, ipv4, ipv6, fake) function HandleDNSQuery(domain, ipv4, ipv6, fake)
local ips, ttl, err = by_id.primary:Query(domain, ipv4, ipv6, fake) local ips, ttl, err = by_id.primary:Query(domain, ipv4, ipv6, fake)
if not err and ru_ips:AnyMatch(ips) then if not err and us_ips:AnyMatch(ips) then
return ips, ttl, nil return ips, ttl, nil
end end
return by_id.fallback:Query(domain, ipv4, ipv6, fake) return by_id.fallback:Query(domain, ipv4, ipv6, fake)
@@ -152,7 +152,7 @@ end
primary := &scriptNameServer{ primary := &scriptNameServer{
name: "primary", name: "primary",
answers: map[string]net.IP{ answers: map[string]net.IP{
"ru.example": net.ParseIP("2a02:6b8::feed:0ff"), "us.example": net.ParseIP("2001:4860:4860::8888"),
"other.example": net.ParseIP("127.0.0.1"), "other.example": net.ParseIP("127.0.0.1"),
}, },
ttl: 30, ttl: 30,
@@ -191,7 +191,7 @@ end
ip net.IP ip net.IP
ttl uint32 ttl uint32
}{ }{
{"Ru.Example.", net.ParseIP("2a02:6b8::feed:0ff"), 30}, {"Us.Example.", net.ParseIP("2001:4860:4860::8888"), 30},
{"other.example", net.ParseIP("9.9.9.9"), 60}, {"other.example", net.ParseIP("9.9.9.9"), 60},
} { } {
ips, ttl, err := server.LookupIP(tc.domain, option) ips, ttl, err := server.LookupIP(tc.domain, option)
+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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// source: app/geodata/config.proto // source: app/geodata/config.proto
package geodata package geodata
+36 -16
View File
@@ -21,10 +21,7 @@ import (
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
const ( const idleTimeout = 30 * time.Second
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"
@@ -36,6 +33,7 @@ type stage struct {
type downloader struct { type downloader struct {
ctx context.Context ctx context.Context
httpClient *http.Client
httpsClient *http.Client httpsClient *http.Client
} }
@@ -63,11 +61,12 @@ 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,
httpsClient: newClient(ctx, dispatcher, outbound), httpClient: newClient(ctx, dispatcher, outbound, false),
httpsClient: newClient(ctx, dispatcher, outbound, true),
} }
} }
func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string) *http.Client { func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string, isHTTPS bool) *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 {
@@ -92,6 +91,7 @@ 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,12 +114,31 @@ 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) >= 20 { if len(via) >= 10 {
return errors.New("stopped after 20 redirects") return errors.New("stopped after 10 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) {
@@ -184,26 +203,27 @@ func (d *downloader) fetch(rawURL string, writer io.Writer) error {
} }
utils.TryDefaultHeadersWith(req.Header, "nav") utils.TryDefaultHeadersWith(req.Header, "nav")
resp, err := d.httpsClient.Do(req) var client *http.Client
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, io.LimitReader(resp.Body, maxDownloadSize+1)) n, err := io.Copy(writer, resp.Body)
if err != nil { if err != nil {
return err 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")
} }
+2 -55
View File
@@ -7,13 +7,10 @@ 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"
) )
@@ -34,11 +31,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.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { server := httptest.NewServer(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(), httpsClient: server.Client()} d := &downloader{ctx: context.Background(), httpClient: 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)
@@ -74,53 +71,3 @@ 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)
}
})
}
}
+3 -23
View File
@@ -2,16 +2,12 @@ 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"
) )
@@ -29,24 +25,6 @@ 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,
@@ -66,7 +44,9 @@ 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),
) )
g.tasker.Schedule(schedule, cron.FuncJob(g.execute)) if _, err := g.tasker.AddFunc(config.Cron, g.execute); err != nil {
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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.2 // - protoc-gen-go-grpc v1.6.0
// - protoc (unknown) // - protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.2 // - protoc-gen-go-grpc v1.6.0
// - protoc (unknown) // - protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.2 // - protoc-gen-go-grpc v1.6.0
// - protoc (unknown) // - protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// source: app/router/command/command.proto // source: app/router/command/command.proto
package command package command
-1
View File
@@ -46,7 +46,6 @@ 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.2 // - protoc-gen-go-grpc v1.6.0
// - protoc (unknown) // - protoc v6.33.5
// source: app/router/command/command.proto // source: app/router/command/command.proto
package command package command
+1 -1
View File
@@ -415,7 +415,7 @@ func BenchmarkMphDomainMatcher(b *testing.B) {
func BenchmarkMultiGeoIPMatcher(b *testing.B) { func BenchmarkMultiGeoIPMatcher(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules, err := geodata.ParseIPRules([]string{"geoip:cn", "geoip:ir", "geoip:ru"}) rules, err := geodata.ParseIPRules([]string{"geoip:cn", "geoip:jp", "geoip:ca", "geoip:us"})
common.Must(err) common.Must(err)
matcher, err := NewIPMatcher(rules, MatcherAsType_Target) matcher, err := NewIPMatcher(rules, MatcherAsType_Target)
+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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.2 // - protoc-gen-go-grpc v1.6.0
// - protoc (unknown) // - protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// source: app/version/config.proto // source: app/version/config.proto
package version package version
+17 -59
View File
@@ -8,65 +8,23 @@ 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{
// https://www.iana.org/assignments/iana-ipv4-special-registry "0.0.0.0/8",
"0.0.0.0/8", // "This network" "10.0.0.0/8",
"0.0.0.0/32", // "This host on this network" "100.64.0.0/10",
"10.0.0.0/8", // Private-Use "127.0.0.0/8",
"100.64.0.0/10", // Shared Address Space "169.254.0.0/16",
"127.0.0.0/8", // Loopback "172.16.0.0/12",
"169.254.0.0/16", // Link Local "192.0.0.0/24",
"172.16.0.0/12", // Private-Use "192.0.2.0/24",
// "192.0.0.0/24", // IETF Protocol Assignments "192.88.99.0/24",
"192.0.0.0/29", // IPv4 Service Continuity Prefix "192.168.0.0/16",
"192.0.0.8/32", // IPv4 dummy address "198.18.0.0/15",
"192.0.0.9/32", // Port Control Protocol Anycast "198.51.100.0/24",
"192.0.0.10/32", // Traversal Using Relays around NAT Anycast "203.0.113.0/24",
"192.0.0.170/32", // NAT64/DNS64 Discovery "224.0.0.0/3",
"192.0.0.171/32", // NAT64/DNS64 Discovery "::/127",
"192.0.2.0/24", // Documentation (TEST-NET-1) "fc00::/7",
// "192.31.196.0/24", // AS112-v4 "fe80::/10",
"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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// source: common/geodata/geodat.proto // source: common/geodata/geodat.proto
package geodata package geodata
+7 -7
View File
@@ -400,13 +400,13 @@ func TestIPMatcher4CN(t *testing.T) {
} }
} }
func TestIPMatcher6RU(t *testing.T) { func TestIPMatcher6US(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:ru") matcher := buildIPMatcher("geoip:us")
if !matcher.Match(xnet.ParseAddress("2a02:6b8::feed:0ff").IP()) { if !matcher.Match(xnet.ParseAddress("2001:4860:4860::8888").IP()) {
t.Error("expect RU geoip contain 2a02:6b8::feed:0ff, but actually not") t.Error("expect US geoip contain 2001:4860:4860::8888, but actually not")
} }
} }
@@ -423,11 +423,11 @@ func BenchmarkIPMatcher4CN(b *testing.B) {
} }
} }
func BenchmarkIPMatcher6RU(b *testing.B) { func BenchmarkIPMatcher6US(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:ru") matcher := buildIPMatcher("geoip:us")
ip := xnet.ParseAddress("2a02:6b8::feed:0ff").IP() ip := xnet.ParseAddress("2001:4860:4860::8888").IP()
b.ResetTimer() b.ResetTimer()
+3 -3
View File
@@ -11,7 +11,7 @@ func TestParseIPRules(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules := []string{ rules := []string{
"geoip:ru", "geoip:us",
"geoip:cn", "geoip:cn",
"!geoip:cn", "!geoip:cn",
"!!geoip:cn", "!!geoip:cn",
@@ -20,9 +20,9 @@ func TestParseIPRules(t *testing.T) {
"!geoip:!cn", "!geoip:!cn",
"ext:geoip.dat:!cn", "ext:geoip.dat:!cn",
"ext:geoip.dat:!!cn", "ext:geoip.dat:!!cn",
"ext:geoip.dat:ir", "ext:geoip.dat:ca",
"ext-ip:geoip.dat:!cn", "ext-ip:geoip.dat:!cn",
"ext-ip:geoip.dat:!ir", "ext-ip:geoip.dat:!ca",
"192.168.0.0/24", "192.168.0.0/24",
"!192.168.0.0/24", "!192.168.0.0/24",
"!!192.168.0.0/24", "!!192.168.0.0/24",
+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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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/filesystem" "github.com/xtls/xray-core/common/platform"
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 = filesystem.ResolveLuaFile(script); err == nil { } else if name, err = platform.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 {
+11 -8
View File
@@ -187,20 +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"} { for _, name := range []string{"script", "script.dat", "script.lua.txt", "script.LUA"} {
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 { 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 successful compilation", path, program, err) 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 {
t.Fatalf("Compile(%q) = %v, %v; want acceptance of the resolved .lua path", path, program, err)
}
} }
func TestCompileRejectsReadFailure(t *testing.T) { func TestCompileRejectsReadFailure(t *testing.T) {
@@ -235,6 +234,10 @@ 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// source: common/net/destination.proto // source: common/net/destination.proto
package net package net
+33 -44
View File
@@ -4,7 +4,7 @@ package net
import ( import (
"bufio" "bufio"
"encoding/binary" "encoding/hex"
"fmt" "fmt"
"net" "net"
"os" "os"
@@ -26,38 +26,36 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
panic("Unsupported network type for process lookup.") panic("Unsupported network type for process lookup.")
} }
var procFiles []string var procFile string
switch network { switch network {
case "tcp": case "tcp":
if net.ParseIP(srcIP).To4() != nil { if net.ParseIP(srcIP).To4() != nil {
procFiles = []string{"/proc/net/tcp"} procFile = "/proc/net/tcp"
} else { } else {
procFiles = []string{"/proc/net/tcp6"} procFile = "/proc/net/tcp6"
} }
case "udp": case "udp":
if net.ParseIP(srcIP).To4() != nil { if net.ParseIP(srcIP).To4() != nil {
procFiles = []string{"/proc/net/udp", "/proc/net/udp6"} procFile = "/proc/net/udp"
} else { } else {
procFiles = []string{"/proc/net/udp6", "/proc/net/udp"} procFile = "/proc/net/udp6"
} }
default: default:
panic("Unsupported network type for process lookup.") panic("Unsupported network type for process lookup.")
} }
targetHexAddr := formatProcNetAddress(net.ParseIP(srcIP), Port(srcPort)) targetHexAddr, err := formatLittleEndianString(net.ParseIP(srcIP), Port(srcPort))
if err != nil {
portFallback := "" return 0, "", "", errors.New("failed to format address: ", err)
if network == "udp" {
portFallback = fmt.Sprintf(":%04X", srcPort)
} }
inode, err := findInodeInFiles(procFiles, targetHexAddr, portFallback) inode, err := findInodeInFile(procFile, targetHexAddr)
if err != nil { if err != nil {
return 0, "", "", errors.New("could not search in ", strings.Join(procFiles, " and ")).Base(err) return 0, "", "", errors.New("could not search in ", procFile).Base(err)
} }
if inode == "" { if inode == "" {
return 0, "", "", errors.New("connection for ", srcIP, ":", srcPort, " not found in ", strings.Join(procFiles, " or ")) return 0, "", "", errors.New("connection for ", srcIP, ":", srcPort, " not found in ", procFile)
} }
pidStr, err := findPidByInode(inode) pidStr, err := findPidByInode(inode)
@@ -84,60 +82,51 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
return pid, procName, absPath, nil return pid, procName, absPath, nil
} }
func formatProcNetAddress(addr net.IP, port Port) string { func formatLittleEndianString(addr net.IP, port Port) (string, error) {
ip := addr
var ipBytes []byte var ipBytes []byte
if ipv4 := addr.To4(); ipv4 != nil { if ip.To4() != nil {
ipBytes = ipv4 ipBytes = ip.To4()
} else { } else {
ipBytes = addr.To16() ipBytes = ip.To16()
}
if ipBytes == nil {
return "", errors.New("invalid IP format for ", addr, ": ", ip)
} }
var formatted strings.Builder for i, j := 0, len(ipBytes)-1; i < j; i, j = i+1, j-1 {
formatted.Grow(len(ipBytes)*2 + 5) ipBytes[i], ipBytes[j] = ipBytes[j], ipBytes[i]
for i := 0; i < len(ipBytes); i += 4 {
fmt.Fprintf(&formatted, "%08X", binary.NativeEndian.Uint32(ipBytes[i:i+4]))
} }
fmt.Fprintf(&formatted, ":%04X", uint16(port)) portHex := fmt.Sprintf("%04X", uint16(port))
return formatted.String() ipHex := strings.ToUpper(hex.EncodeToString(ipBytes))
return fmt.Sprintf("%s:%s", ipHex, portHex), nil
} }
func findInodeInFiles(filePaths []string, targetHexAddr, portFallback string) (string, error) { func findInodeInFile(filePath, targetHexAddr string) (string, error) {
onlyMatchedPort := ""
for i, filePath := range filePaths {
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
if i > 0 && os.IsNotExist(err) {
continue
}
return "", err return "", err
} }
defer file.Close() defer file.Close()
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
fields := strings.Fields(scanner.Text()) line := scanner.Text()
fields := strings.Fields(line)
if len(fields) < 10 { if len(fields) < 10 {
continue continue
} }
localAddress := fields[1] localAddress := fields[1]
if localAddress == targetHexAddr { if localAddress == targetHexAddr {
return fields[9], nil inode := fields[9]
} return inode, nil
if portFallback != "" && strings.HasSuffix(localAddress, portFallback) {
wildcardIP := strings.TrimSuffix(localAddress, portFallback)
if (len(wildcardIP) == 8 || len(wildcardIP) == 32) && strings.Trim(wildcardIP, "0") == "" {
onlyMatchedPort = fields[9]
}
}
}
if err := scanner.Err(); err != nil {
return "", err
} }
} }
return onlyMatchedPort, nil return "", scanner.Err()
} }
func findPidByInode(inode string) (string, error) { func findPidByInode(inode 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// source: common/net/port.proto // source: common/net/port.proto
package net package net
+24 -24
View File
@@ -5,7 +5,6 @@ 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"
@@ -27,6 +26,14 @@ 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 {
@@ -36,6 +43,11 @@ 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
@@ -67,29 +79,17 @@ func ReadCert(file string) ([]byte, error) {
return ReadFile(platform.GetCertLocation(file)) return ReadFile(platform.GetCertLocation(file))
} }
// ResolveLuaFile finds a local .lua script and returns its absolute path. func CopyFile(dst string, src string) error {
// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir. bytes, err := ReadFile(src)
func ResolveLuaFile(path string) (string, error) { if err != nil {
if !strings.EqualFold(filepath.Ext(path), ".lua") { return err
return "", errors.New("Lua file extension must be .lua")
} }
if filepath.IsAbs(path) { f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0o644)
return path, nil if err != nil {
return err
} }
for _, dir := range []string{ defer f.Close()
platform.GetConfDirPath(),
platform.NewEnvFlag(platform.ConfigLocation).GetValue(func() string { return "" }), _, err = f.Write(bytes)
".", 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
} }
+2 -47
View File
@@ -1,17 +1,13 @@
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 TestResolveAssetRejectsInvalidPath(t *testing.T) { func TestStatAssetRejectsInvalidPath(t *testing.T) {
for _, file := range []string{ for _, file := range []string{
"", "",
".", ".",
@@ -29,49 +25,8 @@ func TestResolveAssetRejectsInvalidPath(t *testing.T) {
`nested\..\geoip.dat`, `nested\..\geoip.dat`,
filepath.Join(t.TempDir(), "geoip.dat"), filepath.Join(t.TempDir(), "geoip.dat"),
} { } {
if _, err := ResolveAsset(file); err == nil { if _, err := StatAsset(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)
} }
+65 -2
View File
@@ -1,6 +1,8 @@
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"
@@ -72,7 +74,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 ""
@@ -81,7 +83,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")
} }
@@ -90,3 +92,64 @@ 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,9 +1,11 @@
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"
@@ -64,3 +66,89 @@ 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// source: common/protocol/server_spec.proto // source: common/protocol/server_spec.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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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 = 29 Version_z byte = 17
) )
var ( var (
+2
View File
@@ -1,3 +1,5 @@
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,7 +2,6 @@ package localdns
import ( import (
"context" "context"
"net/netip"
"syscall" "syscall"
"time" "time"
@@ -101,15 +100,6 @@ 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)
}, },
} }
+3 -10
View File
@@ -4,18 +4,17 @@ 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
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/klauspost/cpuid/v2 v2.4.1 github.com/klauspost/cpuid/v2 v2.4.0
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138 github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138
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.16.0 github.com/pires/go-proxyproto v0.15.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
@@ -24,7 +23,7 @@ require (
github.com/yuin/gopher-lua v1.1.2 github.com/yuin/gopher-lua v1.1.2
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
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-20261005185213-c3db4df58582
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.61.0
golang.org/x/sync v0.24.0 golang.org/x/sync v0.24.0
@@ -59,12 +58,6 @@ require (
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-20260706201446-f0a921348800 // 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
)
+6 -10
View File
@@ -2,8 +2,6 @@ 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=
@@ -35,8 +33,8 @@ github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/cpuid/v2 v2.4.1 h1:I9KxadAkvo+QjtcEE2f2tyfEvmIla/T0m+14PrZNl0U= github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.1/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -61,8 +59,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.16.0 h1:9KYT/fM3wpjT8bdMEj/7FBcoiUG1KyzEjRYnPu+vz7A= github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
github.com/pires/go-proxyproto v0.16.0/go.mod h1:n7yhg8yIfNl3KgYDCwBzmeUHNpKTVnUhcWpc7rV4RW4= github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
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=
@@ -99,8 +97,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a h1:V5oicZXS0oqZx30gY/apgSVD7YvNBfrIOdcMnlelJY4= golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 h1:wjDBrGbfLuifgrVLFEWUBJYAfh5Q1wkMc5t0FY0tCbs=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E= golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -154,8 +152,6 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:
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-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
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,7 +91,6 @@ 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) {
@@ -148,7 +147,6 @@ 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,6 +6,7 @@ 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"
) )
@@ -18,6 +19,9 @@ 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,6 +1,7 @@
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"
@@ -8,6 +9,8 @@ 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)
} }
@@ -35,7 +38,27 @@ 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",
-21
View File
@@ -15,11 +15,9 @@ import (
googleuuid "github.com/google/uuid" googleuuid "github.com/google/uuid"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"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/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/finalmask/filter"
"github.com/xtls/xray-core/transport/internet/finalmask/fragment" "github.com/xtls/xray-core/transport/internet/finalmask/fragment"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom" "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
@@ -76,7 +74,6 @@ var (
customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
"filter": func() interface{} { return new(Filter) },
"header-custom": func() interface{} { return new(HeaderCustomTCP) }, "header-custom": func() interface{} { return new(HeaderCustomTCP) },
"fragment": func() interface{} { return new(FragmentMask) }, "fragment": func() interface{} { return new(FragmentMask) },
"sudoku": func() interface{} { return new(Sudoku) }, "sudoku": func() interface{} { return new(Sudoku) },
@@ -84,7 +81,6 @@ var (
}, "type", "settings") }, "type", "settings")
udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
"filter": func() interface{} { return new(Filter) },
"header-custom": func() interface{} { return new(HeaderCustomUDP) }, "header-custom": func() interface{} { return new(HeaderCustomUDP) },
"mkcp-legacy": func() interface{} { return new(MkcpLegacy) }, "mkcp-legacy": func() interface{} { return new(MkcpLegacy) },
"noise": func() interface{} { return new(NoiseMask) }, "noise": func() interface{} { return new(NoiseMask) },
@@ -97,23 +93,6 @@ var (
}, "type", "settings") }, "type", "settings")
) )
type Filter struct {
BlockedSourceIPs []string `json:"blockedSourceIPs"`
AllowedSourceIPs []string `json:"allowedSourceIPs"`
}
func (c *Filter) Build() (proto.Message, error) {
rules, err := geodata.ParseIPRules(c.BlockedSourceIPs)
if err != nil {
return nil, errors.New(`invalid "blockedSourceIPs"`).Base(err)
}
allowed, err := geodata.ParseIPRules(c.AllowedSourceIPs)
if err != nil {
return nil, errors.New(`invalid "allowedSourceIPs"`).Base(err)
}
return &filter.Config{BlockedSourceIps: rules, AllowedSourceIps: allowed}, nil
}
type TCPItem struct { type TCPItem struct {
Delay Int32Range `json:"delay"` Delay Int32Range `json:"delay"`
Rand int32 `json:"rand"` Rand int32 `json:"rand"`
@@ -1,79 +0,0 @@
package conf_test
import (
"encoding/json"
"fmt"
"net"
"strings"
"testing"
"github.com/xtls/xray-core/common/geodata"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/finalmask/filter"
)
func TestFinalMaskFilter(t *testing.T) {
for _, network := range []string{"tcp", "udp"} {
for _, test := range []struct {
name, settings string
blocked, allowed int
}{
{"blacklist", `{"blockedSourceIPs":["192.0.2.0/24"]}`, 1, 0},
{"whitelist", `{"allowedSourceIPs":["192.0.2.0/24"]}`, 0, 1},
{"both", `{"blockedSourceIPs":["192.0.2.0/24"],"allowedSourceIPs":["192.0.2.0/24"]}`, 1, 1},
{"empty_whitelist", `{"allowedSourceIPs":[]}`, 0, 0},
{"empty_blacklist", `{"blockedSourceIPs":[]}`, 0, 0},
} {
t.Run(network+"/"+test.name, func(t *testing.T) {
input := fmt.Sprintf(`{"finalmask":{"%s":[{"type":"filter","settings":%s}]}}`, network, test.settings)
var config StreamConfig
if err := json.Unmarshal([]byte(input), &config); err != nil {
t.Fatal(err)
}
built, err := config.Build()
if err != nil {
t.Fatal(err)
}
masks := built.Tcpmasks
if network == "udp" {
masks = built.Udpmasks
}
if len(masks) != 1 {
t.Fatalf("expected one mask, got %d", len(masks))
}
instance, err := masks[0].GetInstance()
if err != nil {
t.Fatal(err)
}
filter, ok := instance.(*filter.Config)
if !ok {
t.Fatalf("unexpected mask type: %T", instance)
}
if len(filter.BlockedSourceIps) != test.blocked || len(filter.AllowedSourceIps) != test.allowed {
t.Fatalf("unexpected filter policy: %v", filter)
}
for _, rules := range [][]*geodata.IPRule{filter.BlockedSourceIps, filter.AllowedSourceIps} {
for _, rule := range rules {
cidr := rule.GetCustom().GetCidr()
if cidr.GetPrefix() != 24 || !net.IP(cidr.GetIp()).Equal(net.IPv4(192, 0, 2, 0)) {
t.Fatalf("unexpected IP rule: %v", cidr)
}
}
}
if _, err := internet.ToMemoryStreamConfig(built); err != nil {
t.Fatalf("failed to initialize runtime finalmask: %v", err)
}
})
}
}
for _, field := range []string{"blockedSourceIPs", "allowedSourceIPs"} {
var config Filter
if err := json.Unmarshal([]byte(fmt.Sprintf(`{"%s":["192.0.2.0/33"]}`, field)), &config); err != nil {
t.Fatal(err)
}
if _, err := config.Build(); err == nil || !strings.Contains(err.Error(), field) {
t.Errorf("expected invalid %s, got %v", field, err)
}
}
}
-18
View File
@@ -90,7 +90,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
config.ProtocolName = protocol config.ProtocolName = protocol
} }
var xdriveH3 bool
switch strings.ToLower(c.Security) { switch strings.ToLower(c.Security) {
case "", "none": case "", "none":
case "tls": case "tls":
@@ -105,7 +104,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
tm := serial.ToTypedMessage(ts) tm := serial.ToTypedMessage(ts)
config.SecuritySettings = append(config.SecuritySettings, tm) config.SecuritySettings = append(config.SecuritySettings, tm)
config.SecurityType = tm.Type config.SecurityType = tm.Type
xdriveH3 = config.ProtocolName == "xdrive" && tlsSettings.ALPN != nil && slices.Equal(*tlsSettings.ALPN, StringList{"h3"})
if config.ProtocolName == "xdrive" && tlsSettings.ALPN != nil && slices.Equal(*tlsSettings.ALPN, StringList{"http/1.1"}) && tlsSettings.ECHConfigList == "" { if config.ProtocolName == "xdrive" && tlsSettings.ALPN != nil && slices.Equal(*tlsSettings.ALPN, StringList{"http/1.1"}) && tlsSettings.ECHConfigList == "" {
// a fingerprint puts its own ALPN into the hello, except Go TLS and the // a fingerprint puts its own ALPN into the hello, except Go TLS and the
// randomized ones, which take it from the config or send none // randomized ones, which take it from the config or send none
@@ -221,7 +219,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
}) })
} }
if c.XDRIVESettings != nil { if c.XDRIVESettings != nil {
c.XDRIVESettings.h3 = xdriveH3
xs, err := c.XDRIVESettings.Build() xs, err := c.XDRIVESettings.Build()
if err != nil { if err != nil {
return nil, errors.New("Failed to build XDRIVE config.").Base(err) return nil, errors.New("Failed to build XDRIVE config.").Base(err)
@@ -263,20 +260,5 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
} }
} }
if xdriveH3 && c.FinalMask != nil {
if c.XDRIVESettings != nil && c.XDRIVESettings.Service == "google-drive" {
// noise only sends datagrams of its own ahead of the QUIC ones, the
// other masks change those or need a peer that undoes them
for _, mask := range c.FinalMask.Udp {
if strings.ToLower(mask.Type) != "noise" {
return nil, errors.New(`XDRIVE: over h3 to Google Drive the only "udp" mask that works is "noise", not "`, mask.Type, `"`)
}
}
}
if config.QuicParams != nil && (config.QuicParams.Congestion == "brutal" || config.QuicParams.Congestion == "force-brutal") {
return nil, errors.New(`XDRIVE: brutal is for Hysteria servers, over h3 "congestion" is "bbr" or "reno", not "`, config.QuicParams.Congestion, `"`)
}
}
return config, nil return config, nil
} }
+1 -51
View File
@@ -997,18 +997,6 @@ 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"`
// set by StreamConfig when "alpn" is ["h3"]
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.
@@ -1027,27 +1015,8 @@ 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", "s3" or "local"`) return nil, errors.New(`unknown "service": "`, c.Service, `", use "google-drive", "template" or "local"`)
}
if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
}
if c.Xmux == (XmuxConfig{}) {
c.Xmux.MaxConnections.From = 1
c.Xmux.MaxConnections.To = 1
if c.Service == "google-drive" && !c.h3 {
// Google gives an h2 connection 1 MiB of window, which a few sessions on
// a long rtt would share, an h3 connection has no such limit
c.Xmux.MaxConnections.From = 4
c.Xmux.MaxConnections.To = 4
}
c.Xmux.HMaxReusableSecs.From = 1800
c.Xmux.HMaxReusableSecs.To = 3000
} }
config := &xdrive.Config{ config := &xdrive.Config{
RemoteFolder: c.RemoteFolder, RemoteFolder: c.RemoteFolder,
@@ -1062,25 +1031,6 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
EagerWindowMs: c.EagerWindowMs, EagerWindowMs: c.EagerWindowMs,
HoleTimeoutMs: c.HoleTimeoutMs, HoleTimeoutMs: c.HoleTimeoutMs,
Template: string(c.Template), Template: string(c.Template),
Xmux: &splithttp.XmuxConfig{
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
MaxConnections: newRangeConfig(c.Xmux.MaxConnections),
CMaxReuseTimes: newRangeConfig(c.Xmux.CMaxReuseTimes),
HMaxRequestTimes: newRangeConfig(c.Xmux.HMaxRequestTimes),
HMaxReusableSecs: newRangeConfig(c.Xmux.HMaxReusableSecs),
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 -124
View File
@@ -9,7 +9,6 @@ import (
. "github.com/xtls/xray-core/infra/conf" . "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
"github.com/xtls/xray-core/transport/internet/splithttp"
"github.com/xtls/xray-core/transport/internet/xdrive" "github.com/xtls/xray-core/transport/internet/xdrive"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -324,7 +323,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", "s3" or "local"`) { if _, err := config.Build(); err == nil || !strings.Contains(err.Error(), `use "google-drive", "template" or "local"`) {
t.Fatalf("expected an unknown service error, got %v", err) t.Fatalf("expected an unknown service error, got %v", err)
} }
} }
@@ -425,81 +424,6 @@ func TestXDriveHTTP1ALPNWarning(t *testing.T) {
} }
} }
// over h3 Google Drive takes only the noise mask, and no service takes brutal
func TestXDriveH3Checks(t *testing.T) {
const (
drive = `"method": "xdrive", "xdriveSettings": {"remoteFolder": "f", "service": "google-drive", "secrets": ["a", "b", "c"]}`
local = `"method": "xdrive", "xdriveSettings": {"remoteFolder": "/tmp/xdrive", "service": "local"}`
noise = `{"type": "Noise", "settings": {"noise": [{"rand": "50-100"}]}}`
salamander = `{"type": "salamander", "settings": {"password": "p"}}`
forceBrutal = `{"quicParams": {"congestion": "force-brutal", "brutalUp": "10 mbps"}}`
)
for _, c := range []struct {
alpn, transport, finalmask string
ok bool
}{
{`["h3"]`, drive, `{"udp": [` + noise + `]}`, true},
{`["h3"]`, drive, `{"udp": [` + salamander + `]}`, false},
{`["h3"]`, drive, `{"udp": [` + noise + `, ` + salamander + `]}`, false},
{`["h3"]`, local, `{"udp": [` + salamander + `]}`, true},
{`["h3"]`, `"method": "xdrive"`, `{"udp": [` + salamander + `]}`, true},
{`["h2"]`, drive, `{"udp": [` + salamander + `]}`, true},
{`null`, drive, `{"udp": [` + salamander + `]}`, true},
{`["h3"]`, drive, `null`, true},
{`["h3"]`, drive, `{"quicParams": {"congestion": "bbr"}}`, true},
{`["h3"]`, drive, `{"quicParams": {"congestion": "brutal"}}`, false},
{`["h3"]`, local, forceBrutal, false},
{`["h3", "h2"]`, drive, `{"quicParams": {"congestion": "brutal"}}`, true},
{`["h3"]`, `"method": "xhttp"`, forceBrutal, true},
} {
config := new(StreamConfig)
if err := json.Unmarshal([]byte(`{
`+c.transport+`,
"security": "tls",
"tlsSettings": {"alpn": `+c.alpn+`},
"finalmask": `+c.finalmask+`
}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if _, err := config.Build(); (err == nil) != c.ok {
t.Errorf("alpn %s, %s, finalmask %s: %v", c.alpn, c.transport, c.finalmask, err)
}
}
}
// one h3 connection is not held back by a window like h2, so h3 takes one by default
func TestXDriveH3Connections(t *testing.T) {
for _, c := range []struct {
alpn string
want int32
}{
{`["h3"]`, 1},
{`["h2"]`, 4},
{`null`, 4},
} {
config := new(StreamConfig)
if err := json.Unmarshal([]byte(`{
"method": "xdrive",
"xdriveSettings": {"remoteFolder": "f", "service": "google-drive", "secrets": ["a", "b", "c"]},
"security": "tls",
"tlsSettings": {"alpn": `+c.alpn+`}
}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
built, err := config.Build()
if err != nil {
t.Fatalf("alpn %s: %v", c.alpn, err)
}
xs, err := built.TransportSettings[0].Settings.GetInstance()
if err != nil {
t.Fatalf("alpn %s: %v", c.alpn, err)
}
if got := xs.(*xdrive.Config).Xmux.MaxConnections; got.From != c.want || got.To != c.want {
t.Errorf("alpn %s: maxConnections is %d-%d, want %d", c.alpn, got.From, got.To, c.want)
}
}
}
func TestXDriveTemplateNeedsTemplate(t *testing.T) { func TestXDriveTemplateNeedsTemplate(t *testing.T) {
config := new(XDriveConfig) config := new(XDriveConfig)
if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil { if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil {
@@ -589,50 +513,3 @@ func TestXDriveInboundConfig(t *testing.T) {
} }
} }
} }
func TestXDriveXmux(t *testing.T) {
defaults := func(connections int32) *splithttp.XmuxConfig {
return &splithttp.XmuxConfig{
MaxConcurrency: &splithttp.RangeConfig{},
MaxConnections: &splithttp.RangeConfig{From: connections, To: connections},
CMaxReuseTimes: &splithttp.RangeConfig{},
HMaxRequestTimes: &splithttp.RangeConfig{},
HMaxReusableSecs: &splithttp.RangeConfig{From: 1800, To: 3000},
}
}
for _, c := range []struct {
input string
want *splithttp.XmuxConfig
}{
{`{"service": "google-drive", "secrets": ["a", "b", "c"]}`, defaults(4)},
{`{"service": "template", "template": {}}`, defaults(1)},
{`{"service": "google-drive", "secrets": ["a", "b", "c"], "xmux": {"maxConcurrency": "1-2", "hMaxRequestTimes": 100, "hKeepAlivePeriod": -1}}`, &splithttp.XmuxConfig{
MaxConcurrency: &splithttp.RangeConfig{From: 1, To: 2},
MaxConnections: &splithttp.RangeConfig{},
CMaxReuseTimes: &splithttp.RangeConfig{},
HMaxRequestTimes: &splithttp.RangeConfig{From: 100, To: 100},
HMaxReusableSecs: &splithttp.RangeConfig{},
HKeepAlivePeriod: -1,
}},
} {
config := new(XDriveConfig)
if err := json.Unmarshal([]byte(c.input), config); err != nil {
t.Fatalf("%s: Unmarshal: %v", c.input, err)
}
built, err := config.Build()
if err != nil {
t.Fatalf("%s: Build: %v", c.input, err)
}
if got := built.(*xdrive.Config).Xmux; !proto.Equal(got, c.want) {
t.Errorf("%s: xmux is %v, want %v", c.input, got, c.want)
}
}
config := new(XDriveConfig)
if err := json.Unmarshal([]byte(`{"service": "template", "template": {}, "xmux": {"maxConnections": 2, "maxConcurrency": 8}}`), config); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if _, err := config.Build(); err == nil {
t.Error("Build accepted maxConnections with maxConcurrency")
}
}
+3 -6
View File
@@ -237,11 +237,10 @@ 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 true return false
} }
if address.Family().IsIP() { if address.Family().IsIP() {
ip := address.IP() return !geodata.GetPrivateIPMatcher().Match(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)
@@ -609,6 +608,7 @@ 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,9 +718,6 @@ 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,6 +90,7 @@ 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,
@@ -110,7 +111,6 @@ func TestXrayConfig(t *testing.T) {
}, },
}, },
}), }),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
}, },
Inbound: []*core.InboundHandlerConfig{ Inbound: []*core.InboundHandlerConfig{
{ {
+201 -178
View File
@@ -1,87 +1,149 @@
package main package main
import ( import (
"bytes" "errors"
"context"
"flag" "flag"
"fmt" "fmt"
"io/fs" "go/build"
"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 ( var directory = flag.String("pwd", "", "Working directory of Xray vprotogen.")
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.")
)
// topoSort sorts file descriptors so each dependency appears before files importing it. // envFile returns the name of the Go environment configuration file.
func topoSort(files []protoreflect.FileDescriptor) []*descriptorpb.FileDescriptorProto { // Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166
fileMap := make(map[string]protoreflect.FileDescriptor) func envFile() (string, error) {
for _, f := range files { if file := os.Getenv("GOENV"); file != "" {
fileMap[f.Path()] = f if file == "off" {
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
} }
visited[fd.Path()] = true dir, err := os.UserConfigDir()
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 nil, err return "", err
}
if dir == "" {
return "", errors.New("missing user-config dir")
}
return filepath.Join(dir, "go", "env"), nil
} }
cmd := exec.Command("go", "tool", pluginName) // GetRuntimeEnv returns the value of runtime environment variable,
cmd.Dir = pwd // that is set by running following command: `go env -w key=value`.
cmd.Stdin = bytes.NewReader(reqBytes) func GetRuntimeEnv(key string) (string, error) {
cmd.Env = os.Environ() file, err := envFile()
var stdout, stderr bytes.Buffer if err != nil {
cmd.Stdout = &stdout return "", err
cmd.Stderr = &stderr }
if file == "" {
if err := cmd.Run(); err != nil { return "", errors.New("missing runtime env file")
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
} }
var resp pluginpb.CodeGeneratorResponse // GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty.
if err := proto.Unmarshal(stdout.Bytes(), &resp); err != nil { func GetGOBIN() string {
return nil, fmt.Errorf("failed to unmarshal response: %w", err) // The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command`
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 resp.GetError() != "" { if GOBIN == "" {
return nil, fmt.Errorf("%s", resp.GetError()) return filepath.Join(build.Default.GOPATH, "bin")
} }
return &resp, nil return GOBIN
}
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() {
@@ -92,145 +154,106 @@ func main() {
flag.Parse() flag.Parse()
if !filepath.IsAbs(*directory) { if !filepath.IsAbs(*directory) {
pwd, err := os.Getwd() pwd, wdErr := os.Getwd()
if err != nil { if wdErr != 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
isCheck := false pwd := *directory
switch *mode { GOBIN := GetGOBIN()
case "generate", "": binPath := os.Getenv("PATH")
isCheck = false pathSlice := []string{pwd, GOBIN, binPath}
case "check": binPath = strings.Join(pathSlice, string(os.PathListSeparator))
isCheck = true os.Setenv("PATH", binPath)
default:
fmt.Fprintf(os.Stderr, "Unrecognized 'mode': %s. Use 'generate' or 'check'.\n", *mode) suffix := ""
if runtime.GOOS == "windows" {
suffix = ".exe"
}
/*
targetedVersion, err := getProjectProtocVersion("https://raw.githubusercontent.com/XTLS/Xray-core/HEAD/core/config.pb.go")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
*/
targetedVersion := ""
protoc, err := whichProtoc(suffix, targetedVersion)
if err != nil {
fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
if isCheck { installedVersion, err := getInstalledProtocVersion(protoc)
fmt.Println("Checking for out-of-date Protobuf generated files...") if err != nil {
fmt.Println(err)
os.Exit(1)
} }
var protoFiles []string if needToUpdate(targetedVersion, installedVersion) {
var existingPbFiles []string fmt.Printf(`
err := filepath.WalkDir(pwd, func(path string, d fs.DirEntry, err error) error { You are using an old protobuf version, please update to v%s or later.
if err != nil || d.IsDir() { Download it from https://github.com/protocolbuffers/protobuf/releases
* 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") {
rel, _ := filepath.Rel(pwd, path) if info.IsDir() {
protoFiles = append(protoFiles, filepath.ToSlash(rel)) return nil
} }
if strings.HasSuffix(d.Name(), ".pb.go") {
existingPbFiles = append(existingPbFiles, path) dir := filepath.Dir(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 err != nil { if walkErr != nil {
fmt.Println("Walk error:", err) fmt.Println(walkErr)
os.Exit(1) os.Exit(1)
} }
compiler := protocompile.Compiler{ for _, files := range protoFilesMap {
Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{ for _, relProtoFile := range files {
ImportPaths: []string{pwd}, args := []string{
}), "--go_out", pwd,
SourceInfoMode: protocompile.SourceInfoStandard, "--go_opt", "paths=source_relative",
"--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)
linkedFiles, err := compiler.Compile(context.Background(), protoFiles...) cmd := exec.Command(protoc, args...)
if err != nil { cmd.Env = append(cmd.Env, os.Environ()...)
fmt.Fprintf(os.Stderr, "Protobuf compilation failed:\n%v\n", err) cmd.Dir = pwd
os.Exit(1) output, cmdErr := cmd.CombinedOutput()
if len(output) > 0 {
fmt.Println(string(output))
} }
if cmdErr != nil {
var fdList []protoreflect.FileDescriptor fmt.Println(cmdErr)
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.")
}
}
-2
View File
@@ -92,8 +92,6 @@ 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)
} }
} }
+11 -4
View File
@@ -3,7 +3,6 @@ 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"
@@ -33,13 +32,14 @@ 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,7 +67,14 @@ 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
} }
+10 -2
View File
@@ -58,10 +58,18 @@ func readConfigFile(file *os.File) ([]byte, error) {
return nil, err return nil, err
} }
if info.Mode().IsRegular() { if info.Mode().IsRegular() {
switch strings.ToLower(filepath.Ext(file.Name())) { path, err := filepath.Abs(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: ", file.Name()) return nil, errors.New("unsupported config file extension: ", path)
} }
} }
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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// source: proxy/dns/config.proto // source: proxy/dns/config.proto
package dns package dns
-7
View File
@@ -381,13 +381,6 @@ 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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,13 +65,6 @@ 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
@@ -93,6 +86,13 @@ 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\x12C\n" + "\x06Config\x12D\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\x12'\n" + "\bport_map\x18\x03 \x03(\v2(.xray.proxy.dokodemo.Config.PortMapEntryR\aportMap\x12C\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.Network)(0), // 2: xray.common.net.Network (*net.IPOrDomain)(nil), // 2: xray.common.net.IPOrDomain
(*net.IPOrDomain)(nil), // 3: xray.common.net.IPOrDomain (net.Network)(0), // 3: xray.common.net.Network
} }
var file_proxy_dokodemo_config_proto_depIdxs = []int32{ var file_proxy_dokodemo_config_proto_depIdxs = []int32{
2, // 0: xray.proxy.dokodemo.Config.allowed_networks:type_name -> xray.common.net.Network 2, // 0: xray.proxy.dokodemo.Config.rewrite_address:type_name -> xray.common.net.IPOrDomain
3, // 1: xray.proxy.dokodemo.Config.rewrite_address:type_name -> xray.common.net.IPOrDomain 1, // 1: xray.proxy.dokodemo.Config.port_map:type_name -> xray.proxy.dokodemo.Config.PortMapEntry
1, // 2: xray.proxy.dokodemo.Config.port_map:type_name -> xray.proxy.dokodemo.Config.PortMapEntry 3, // 2: xray.proxy.dokodemo.Config.allowed_networks:type_name -> xray.common.net.Network
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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// source: proxy/freedom/config.proto // source: proxy/freedom/config.proto
package freedom package freedom
+5 -10
View File
@@ -229,19 +229,14 @@ 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)
defer common.Close(output) timer := time.AfterFunc(delay, func() {
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 -30
View File
@@ -10,7 +10,6 @@ import (
"net/url" "net/url"
"sync" "sync"
"text/template" "text/template"
"time"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
@@ -87,13 +86,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
user := server.User user := server.User
var conn stat.Connection var conn stat.Connection
var mbuf buf.MultiBuffer mbuf, _ := link.Reader.ReadMultiBuffer()
if reader, ok := link.Reader.(buf.TimeoutReader); ok {
// Don't wait forever: server-first protocols (SMTP, FTP, MySQL...) send nothing until the server greets.
mbuf, _ = reader.ReadMultiBufferTimeout(time.Millisecond * 500)
} else {
mbuf, _ = link.Reader.ReadMultiBuffer()
}
len := mbuf.Len() len := mbuf.Len()
firstPayload := bytespool.Alloc(len) firstPayload := bytespool.Alloc(len)
mbuf, _ = buf.SplitBytes(mbuf, firstPayload) mbuf, _ = buf.SplitBytes(mbuf, firstPayload)
@@ -241,8 +234,7 @@ func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, u
return nil, err return nil, err
} }
reader := bufio.NewReader(rawConn) resp, err := http.ReadResponse(bufio.NewReader(rawConn), req)
resp, err := http.ReadResponse(reader, req)
if err != nil { if err != nil {
rawConn.Close() rawConn.Close()
return nil, err return nil, err
@@ -253,11 +245,6 @@ func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, u
rawConn.Close() rawConn.Close()
return nil, errors.New("Proxy responded with non 200 code: " + resp.Status) return nil, errors.New("Proxy responded with non 200 code: " + resp.Status)
} }
// Tunneled bytes (e.g. a server-first greeting) may arrive in the same read as the 200 response.
if n := reader.Buffered(); n > 0 {
early, _ := reader.Peek(n)
return &earlyDataConn{Conn: rawConn, early: early}, nil
}
return rawConn, nil return rawConn, nil
} }
@@ -388,21 +375,6 @@ func (h *http2Conn) Close() error {
return h.out.Close() return h.out.Close()
} }
// earlyDataConn returns the bytes read ahead together with the CONNECT response before reading from Conn.
type earlyDataConn struct {
net.Conn
early []byte
}
func (c *earlyDataConn) Read(p []byte) (int, error) {
if len(c.early) > 0 {
n := copy(p, c.early)
c.early = c.early[n:]
return n, nil
}
return c.Conn.Read(p)
}
func init() { func init() {
common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return NewClient(ctx, config.(*ClientConfig)) return NewClient(ctx, config.(*ClientConfig))
+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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// source: proxy/http/config.proto // source: proxy/http/config.proto
package http package http
+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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// 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.12 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc v6.33.5
// source: proxy/shadowsocks_2022/config.proto // source: proxy/shadowsocks_2022/config.proto
package shadowsocks_2022 package shadowsocks_2022
+8 -14
View File
@@ -53,7 +53,12 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Inbound, error) {
return nil, err return nil, err
} }
udpCodec, err := NewUDPServerCodec(method, psk, 500*time.Second) user := &protocol.MemoryUser{
Email: config.Email,
Level: uint32(config.Level),
}
udpCodec, err := NewUDPServerCodec(method, psk, user, 500*time.Second)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -64,10 +69,7 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Inbound, error) {
method: method, method: method,
psk: psk, psk: psk,
saltFilter: antireplay.NewMapFilter[[32]byte](60), saltFilter: antireplay.NewMapFilter[[32]byte](60),
user: &protocol.MemoryUser{ user: user,
Email: config.Email,
Level: uint32(config.Level),
},
udpCodec: udpCodec, udpCodec: udpCodec,
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
}, nil }, nil
@@ -155,20 +157,12 @@ func (i *Inbound) processUDP(ctx context.Context, conn stat.Connection, dispatch
} }
for _, b := range mb { for _, b := range mb {
decoded, err := i.udpCodec.DecodePacket(b.Bytes()) sessionItem, decoded, err := i.udpCodec.DecodePacket(b.Bytes())
b.Release() b.Release()
if err != nil || decoded.HeaderType != HeaderTypeClient { if err != nil || decoded.HeaderType != HeaderTypeClient {
continue continue
} }
sessionItem := i.udpCodec.GetSession(decoded.SessionID)
if sessionItem.User == nil {
sessionItem.Lock()
if sessionItem.User == nil {
sessionItem.User = i.user
}
sessionItem.Unlock()
}
link := sessionItem.link.Load() link := sessionItem.link.Load()
if link != nil { if link != nil {
sessionItem.UpdateConn(conn) sessionItem.UpdateConn(conn)
+12 -62
View File
@@ -2,8 +2,6 @@ package shadowsocks_2022
import ( import (
"context" "context"
"crypto/cipher"
"encoding/binary"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -41,8 +39,7 @@ type MultiUserInbound struct {
usersByEmail *utils.TypedSyncMap[string, *protocol.MemoryUser] usersByEmail *utils.TypedSyncMap[string, *protocol.MemoryUser]
userCount atomic.Int64 userCount atomic.Int64
saltFilter *antireplay.ReplayFilter[[32]byte] saltFilter *antireplay.ReplayFilter[[32]byte]
udpSessions *UDPSessionManager udpCodec *UDPMultiServerCodec
udpMasterCipher cipher.Block
policyManager policy.Manager policyManager policy.Manager
} }
@@ -68,7 +65,7 @@ func NewMultiServer(ctx context.Context, config *MultiUserServerConfig) (*MultiU
return nil, err return nil, err
} }
masterBlock, err := method.NewBlock(masterPSK) udpCodec, err := NewUDPMultiServerCodec(method, masterPSK, 500*time.Second)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -81,8 +78,7 @@ func NewMultiServer(ctx context.Context, config *MultiUserServerConfig) (*MultiU
usersByHash: utils.NewTypedSyncMap[[AESBlockSize]byte, *protocol.MemoryUser](), usersByHash: utils.NewTypedSyncMap[[AESBlockSize]byte, *protocol.MemoryUser](),
usersByEmail: utils.NewTypedSyncMap[string, *protocol.MemoryUser](), usersByEmail: utils.NewTypedSyncMap[string, *protocol.MemoryUser](),
saltFilter: antireplay.NewMapFilter[[32]byte](60), saltFilter: antireplay.NewMapFilter[[32]byte](60),
udpSessions: NewUDPSessionManager(500 * time.Second), udpCodec: udpCodec,
udpMasterCipher: masterBlock,
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
} }
@@ -281,68 +277,26 @@ func (i *MultiUserInbound) processUDP(ctx context.Context, conn stat.Connection,
} }
for _, b := range mb { for _, b := range mb {
// In multi-user UDP: sessionItem, decoded, user, err := i.udpCodec.DecodePacket(b.Bytes(), func(hash [AESBlockSize]byte) (*protocol.MemoryUser, []byte, bool) {
// Packet header is 16 bytes: Encrypted(SessionID + PacketID) u, ok := i.usersByHash.Load(hash)
// Followed by 16 bytes EIH
packetBytes := b.Bytes()
if len(packetBytes) < 32+1+8+2 {
b.Release()
continue
}
var rawHeader [16]byte
i.udpMasterCipher.Decrypt(rawHeader[:], packetBytes[:16])
sessionID := binary.BigEndian.Uint64(rawHeader[:8])
packetID := binary.BigEndian.Uint64(rawHeader[8:16])
sessionItem := i.udpSessions.GetOrCreate(sessionID)
if !sessionItem.CheckPacketID(packetID) {
b.Release()
continue
}
var userPSK []byte
var currentUser *protocol.MemoryUser
sessionItem.Lock()
currentUser = sessionItem.User
userPSK = sessionItem.UserPSK
sessionItem.Unlock()
if currentUser == nil {
// Decrypt EIH
decryptedHash := DecryptUDPEIH(i.udpMasterCipher, rawHeader[:], packetBytes[16:32])
user, ok := i.usersByHash.Load(decryptedHash)
if !ok { if !ok {
return nil, nil, false
}
return u, u.Account.(*MemoryAccount).Key, true
})
b.Release() b.Release()
if err != nil || decoded.HeaderType != HeaderTypeClient {
continue continue
} }
currentUser = user
userPSK = user.Account.(*MemoryAccount).Key
}
decoded, err := sessionItem.DecryptAESPayload(i.method, userPSK, sessionID, packetID, rawHeader[:], packetBytes[32:])
b.Release()
if err != nil {
continue
}
sessionItem.Lock()
if sessionItem.User == nil {
sessionItem.User = currentUser
sessionItem.UserPSK = userPSK
}
sessionItem.Unlock()
link := sessionItem.link.Load() link := sessionItem.link.Load()
if link != nil { if link != nil {
sessionItem.UpdateConn(conn) sessionItem.UpdateConn(conn)
} else { } else {
userPSK := user.Account.(*MemoryAccount).Key
var err error var err error
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) { link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) {
return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload) return i.udpCodec.EncodeServerPacket(decoded.SessionID, userPSK, replyDest, payload)
}) })
if err != nil { if err != nil {
continue continue
@@ -356,7 +310,3 @@ func (i *MultiUserInbound) processUDP(ctx context.Context, conn stat.Connection,
} }
} }
} }
func (i *MultiUserInbound) encodeServerUDPPacket(clientSessionID uint64, userPSK []byte, dest net.Destination, payload []byte) ([]byte, error) {
return i.udpSessions.EncodeServerPacket(i.method, userPSK, clientSessionID, dest, payload)
}
-2
View File
@@ -243,7 +243,6 @@ func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dis
dest.Network = net.Network_UDP dest.Network = net.Network_UDP
sessionItem := i.udpSessions.GetOrCreate(sessionID) sessionItem := i.udpSessions.GetOrCreate(sessionID)
if sessionItem.User == nil {
sessionItem.Lock() sessionItem.Lock()
if sessionItem.User == nil { if sessionItem.User == nil {
sessionItem.User = &protocol.MemoryUser{ sessionItem.User = &protocol.MemoryUser{
@@ -252,7 +251,6 @@ func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dis
} }
} }
sessionItem.Unlock() sessionItem.Unlock()
}
link := sessionItem.link.Load() link := sessionItem.link.Load()
if link != nil { if link != nil {
sessionItem.UpdateConn(conn) sessionItem.UpdateConn(conn)
+3 -3
View File
@@ -30,7 +30,7 @@ type Outbound struct {
method *CipherMethod method *CipherMethod
pskList [][]byte pskList [][]byte
finalPSK []byte finalPSK []byte
udpCodec *UDPPacketCodec udpCodec *UDPClientCodec
policyManager policy.Manager policyManager policy.Manager
} }
@@ -50,9 +50,9 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) {
} }
finalPSK := pskList[len(pskList)-1] finalPSK := pskList[len(pskList)-1]
udpCodec, err := NewUDPPacketCodec(method, pskList) udpCodec, err := NewUDPClientCodec(method, pskList)
if err != nil { if err != nil {
return nil, errors.New("failed to create udp packet codec").Base(err) return nil, errors.New("failed to create udp client codec").Base(err)
} }
v := core.MustFromContext(ctx) v := core.MustFromContext(ctx)
+323 -123
View File
@@ -7,33 +7,78 @@ import (
"io" "io"
"math" "math"
mrand "math/rand/v2" mrand "math/rand/v2"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
) )
type UDPCodec struct { type UDPClientCodec struct {
method *CipherMethod method *CipherMethod
pskList [][]byte pskList [][]byte
psk []byte psk []byte
blockCipher cipher.Block blockCipher cipher.Block
blockCiphers []cipher.Block blockCiphers []cipher.Block
chachaCipher cipher.AEAD chachaCipher cipher.AEAD
}
func NewUDPClientCodec(method *CipherMethod, pskList [][]byte) (*UDPClientCodec, error) {
if len(pskList) == 0 {
return nil, errors.New("empty psk list")
}
if method.IsChaCha && len(pskList) > 1 {
return nil, errors.New("multi-key is not supported for chacha20-poly1305")
}
finalPSK := pskList[len(pskList)-1]
c := &UDPClientCodec{
method: method,
pskList: pskList,
psk: finalPSK,
}
var err error
if method.IsChaCha {
c.chachaCipher, err = method.NewUDPCipher(finalPSK)
} else {
c.blockCipher, err = method.NewBlock(finalPSK)
}
if err != nil {
return nil, err
}
if len(pskList) > 1 {
c.blockCiphers = make([]cipher.Block, len(pskList))
for i, psk := range pskList {
c.blockCiphers[i], err = method.NewBlock(psk)
if err != nil {
return nil, err
}
}
}
return c, nil
}
func NewUDPPacketCodec(method *CipherMethod, pskList [][]byte) (*UDPClientCodec, error) {
return NewUDPClientCodec(method, pskList)
}
type UDPServerCodec struct {
method *CipherMethod
psk []byte
user *protocol.MemoryUser
blockCipher cipher.Block
chachaCipher cipher.AEAD
sessions *UDPSessionManager sessions *UDPSessionManager
} }
type ( func NewUDPServerCodec(method *CipherMethod, psk []byte, user *protocol.MemoryUser, sessionTimeout time.Duration) (*UDPServerCodec, error) {
UDPPacketCodec = UDPCodec c := &UDPServerCodec{
UDPServerCodec = UDPCodec
)
func newUDPCodec(method *CipherMethod, psk []byte) (*UDPCodec, error) {
c := &UDPCodec{
method: method, method: method,
psk: psk, psk: psk,
user: user,
sessions: NewUDPSessionManager(sessionTimeout),
} }
var err error var err error
if method.IsChaCha { if method.IsChaCha {
@@ -47,46 +92,129 @@ func newUDPCodec(method *CipherMethod, psk []byte) (*UDPCodec, error) {
return c, nil return c, nil
} }
func NewUDPPacketCodec(method *CipherMethod, pskList [][]byte) (*UDPCodec, error) { func (c *UDPServerCodec) Sessions() *UDPSessionManager {
if method.IsChaCha && len(pskList) > 1 {
return nil, errors.New("multi-key is not supported for chacha20-poly1305")
}
finalPSK := pskList[len(pskList)-1]
c, err := newUDPCodec(method, finalPSK)
if err != nil {
return nil, err
}
c.pskList = pskList
if len(pskList) > 1 {
c.blockCiphers = make([]cipher.Block, len(pskList))
for i, psk := range pskList {
c.blockCiphers[i], err = method.NewBlock(psk)
if err != nil {
return nil, err
}
}
}
return c, nil
}
func NewUDPServerCodec(method *CipherMethod, psk []byte, sessionTimeout time.Duration) (*UDPCodec, error) {
c, err := newUDPCodec(method, psk)
if err != nil {
return nil, err
}
c.sessions = NewUDPSessionManager(sessionTimeout)
return c, nil
}
func (c *UDPCodec) Sessions() *UDPSessionManager {
return c.sessions return c.sessions
} }
func (c *UDPCodec) GetSession(sessionID uint64) *ServerUDPSession { func (c *UDPServerCodec) GetSession(sessionID uint64) *ServerUDPSession {
if c.sessions == nil { if c.sessions == nil {
return nil return nil
} }
return c.sessions.GetOrCreate(sessionID) return c.sessions.Get(sessionID)
}
type UDPMultiServerCodec struct {
method *CipherMethod
masterPSK []byte
masterBlock cipher.Block
sessions *UDPSessionManager
}
func NewUDPMultiServerCodec(method *CipherMethod, masterPSK []byte, sessionTimeout time.Duration) (*UDPMultiServerCodec, error) {
if method.IsChaCha {
return nil, errors.New("multi-user is not supported for chacha20-poly1305")
}
masterBlock, err := method.NewBlock(masterPSK)
if err != nil {
return nil, err
}
return &UDPMultiServerCodec{
method: method,
masterPSK: masterPSK,
masterBlock: masterBlock,
sessions: NewUDPSessionManager(sessionTimeout),
}, nil
}
func (c *UDPMultiServerCodec) Sessions() *UDPSessionManager {
return c.sessions
}
func (c *UDPMultiServerCodec) GetSession(sessionID uint64) *ServerUDPSession {
if c.sessions == nil {
return nil
}
return c.sessions.Get(sessionID)
}
type UserLookupFunc func(hash [AESBlockSize]byte) (*protocol.MemoryUser, []byte, bool)
func (c *UDPMultiServerCodec) DecodePacket(data []byte, userLookup UserLookupFunc) (*ServerUDPSession, DecodedUDPPacket, *protocol.MemoryUser, error) {
if len(data) < 2*AESBlockSize+AEADTagSize+RequestHeaderFixedChunkLength {
return nil, DecodedUDPPacket{}, nil, ErrPacketTooShort
}
var rawHeader [16]byte
c.masterBlock.Decrypt(rawHeader[:], data[:16])
sessionID := binary.BigEndian.Uint64(rawHeader[:8])
packetID := binary.BigEndian.Uint64(rawHeader[8:16])
sessionItem := c.sessions.Get(sessionID)
var currentUser *protocol.MemoryUser
var userPSK []byte
var bodyAead cipher.AEAD
if sessionItem != nil {
sessionItem.Lock()
currentUser = sessionItem.User
userPSK = sessionItem.UserPSK
bodyAead = sessionItem.clientBodyCipher
sessionItem.Unlock()
if currentUser != nil {
if !sessionItem.CheckPacketID(packetID) {
return nil, DecodedUDPPacket{}, nil, ErrPacketIdNotUnique
}
}
}
if currentUser == nil {
decryptedHash := DecryptUDPEIH(c.masterBlock, rawHeader[:], data[16:32])
user, psk, ok := userLookup(decryptedHash)
if !ok {
return nil, DecodedUDPPacket{}, nil, errors.New("user not found")
}
currentUser = user
userPSK = psk
}
if bodyAead == nil {
bodyKey := DeriveSessionSubKey(userPSK, rawHeader[:8], c.method.KeySaltLength)
var err error
bodyAead, err = c.method.NewAEAD(bodyKey)
if err != nil {
return nil, DecodedUDPPacket{}, nil, err
}
}
bodyNonce := rawHeader[4:16]
bodyCipher := data[32:]
bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil)
if err != nil {
return nil, DecodedUDPPacket{}, nil, errors.New("failed to decrypt aes udp body").Base(err)
}
decoded, err := parsePlainUDPPacket(sessionID, packetID, bodyPlain)
if err != nil {
return nil, DecodedUDPPacket{}, nil, err
}
if decoded.HeaderType != HeaderTypeClient {
return nil, DecodedUDPPacket{}, nil, ErrBadHeaderType
}
if sessionItem == nil {
sessionItem = c.sessions.GetOrCreate(sessionID)
}
if !sessionItem.Commit(packetID, bodyAead, currentUser, userPSK) {
return nil, DecodedUDPPacket{}, nil, ErrPacketIdNotUnique
}
return sessionItem, decoded, currentUser, nil
}
func (c *UDPMultiServerCodec) EncodeServerPacket(clientSessionID uint64, userPSK []byte, dest net.Destination, payload []byte) ([]byte, error) {
return c.sessions.EncodeServerPacket(c.method, userPSK, clientSessionID, dest, payload)
} }
type DecodedUDPPacket struct { type DecodedUDPPacket struct {
@@ -193,44 +321,42 @@ func parsePlainUDPPacket(sessionID, packetID uint64, bodyPlain []byte) (DecodedU
}, nil }, nil
} }
func (c *UDPCodec) DecodePacket(data []byte) (DecodedUDPPacket, error) { func (c *UDPServerCodec) DecodePacket(data []byte) (*ServerUDPSession, DecodedUDPPacket, error) {
if len(data) < PacketMinimalHeaderSize { if len(data) < PacketMinimalHeaderSize {
return DecodedUDPPacket{}, ErrPacketTooShort return nil, DecodedUDPPacket{}, ErrPacketTooShort
} }
if c.method.IsChaCha { if c.method.IsChaCha {
if len(data) < PacketNonceSize+AEADTagSize { if len(data) < PacketNonceSize+AEADTagSize {
return DecodedUDPPacket{}, ErrPacketTooShort return nil, DecodedUDPPacket{}, ErrPacketTooShort
} }
nonce := data[:PacketNonceSize] nonce := data[:PacketNonceSize]
ciphertext := data[PacketNonceSize:] ciphertext := data[PacketNonceSize:]
plain, err := c.chachaCipher.Open(nil, nonce, ciphertext, nil) plain, err := c.chachaCipher.Open(nil, nonce, ciphertext, nil)
if err != nil { if err != nil {
return DecodedUDPPacket{}, errors.New("failed to decrypt chacha udp packet").Base(err) return nil, DecodedUDPPacket{}, errors.New("failed to decrypt chacha udp packet").Base(err)
} }
if len(plain) < 16+1+8+2 { if len(plain) < 16+1+8+2 {
return DecodedUDPPacket{}, ErrPacketTooShort return nil, DecodedUDPPacket{}, ErrPacketTooShort
} }
sessionID := binary.BigEndian.Uint64(plain[:8]) sessionID := binary.BigEndian.Uint64(plain[:8])
packetID := binary.BigEndian.Uint64(plain[8:16]) packetID := binary.BigEndian.Uint64(plain[8:16])
sessionItem := c.sessions.GetOrCreate(sessionID)
if !sessionItem.CheckPacketID(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
decoded, err := parsePlainUDPPacket(sessionID, packetID, plain[16:]) decoded, err := parsePlainUDPPacket(sessionID, packetID, plain[16:])
if err != nil { if err != nil {
return DecodedUDPPacket{}, err return nil, DecodedUDPPacket{}, err
} }
if decoded.HeaderType != HeaderTypeClient { if decoded.HeaderType != HeaderTypeClient {
return DecodedUDPPacket{}, ErrBadHeaderType return nil, DecodedUDPPacket{}, ErrBadHeaderType
} }
sessionItem.AddPacketID(packetID) sessionItem := c.sessions.GetOrCreate(sessionID)
return decoded, nil if !sessionItem.Commit(packetID, nil, c.user, c.psk) {
return nil, DecodedUDPPacket{}, ErrPacketIdNotUnique
}
return sessionItem, decoded, nil
} }
// AES mode // AES mode
@@ -239,49 +365,48 @@ func (c *UDPCodec) DecodePacket(data []byte) (DecodedUDPPacket, error) {
sessionID := binary.BigEndian.Uint64(rawHeader[:8]) sessionID := binary.BigEndian.Uint64(rawHeader[:8])
packetID := binary.BigEndian.Uint64(rawHeader[8:16]) packetID := binary.BigEndian.Uint64(rawHeader[8:16])
sessionItem := c.sessions.GetOrCreate(sessionID) sessionItem := c.sessions.Get(sessionID)
var bodyAead cipher.AEAD
if sessionItem != nil {
if !sessionItem.CheckPacketID(packetID) { if !sessionItem.CheckPacketID(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique return nil, DecodedUDPPacket{}, ErrPacketIdNotUnique
}
sessionItem.Lock()
bodyAead = sessionItem.clientBodyCipher
sessionItem.Unlock()
} }
return sessionItem.DecryptAESPayload(c.method, c.psk, sessionID, packetID, rawHeader[:], data[16:])
}
func (s *ServerUDPSession) DecryptAESPayload(method *CipherMethod, psk []byte, sessionID, packetID uint64, rawHeader, bodyCipher []byte) (DecodedUDPPacket, error) {
bodyAead := s.clientBodyCipher
isNewCipher := false
if bodyAead == nil { if bodyAead == nil {
bodyKey := DeriveSessionSubKey(psk, rawHeader[:8], method.KeySaltLength) bodyKey := DeriveSessionSubKey(c.psk, rawHeader[:8], c.method.KeySaltLength)
var err error var err error
bodyAead, err = method.NewAEAD(bodyKey) bodyAead, err = c.method.NewAEAD(bodyKey)
if err != nil { if err != nil {
return DecodedUDPPacket{}, err return nil, DecodedUDPPacket{}, err
} }
isNewCipher = true
} }
bodyNonce := rawHeader[4:16] bodyNonce := rawHeader[4:16]
bodyCipher := data[16:]
bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil) bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil)
if err != nil { if err != nil {
return DecodedUDPPacket{}, errors.New("failed to decrypt aes udp body").Base(err) return nil, DecodedUDPPacket{}, errors.New("failed to decrypt aes udp body").Base(err)
} }
decoded, err := parsePlainUDPPacket(sessionID, packetID, bodyPlain) decoded, err := parsePlainUDPPacket(sessionID, packetID, bodyPlain)
if err != nil { if err != nil {
return DecodedUDPPacket{}, err return nil, DecodedUDPPacket{}, err
} }
if decoded.HeaderType != HeaderTypeClient { if decoded.HeaderType != HeaderTypeClient {
return DecodedUDPPacket{}, ErrBadHeaderType return nil, DecodedUDPPacket{}, ErrBadHeaderType
} }
s.AddPacketID(packetID) if sessionItem == nil {
sessionItem = c.sessions.GetOrCreate(sessionID)
if isNewCipher {
s.clientBodyCipher = bodyAead
} }
if !sessionItem.Commit(packetID, bodyAead, c.user, c.psk) {
return decoded, nil return nil, DecodedUDPPacket{}, ErrPacketIdNotUnique
}
return sessionItem, decoded, nil
} }
func (s *ServerUDPSession) EnsureServerState(method *CipherMethod, psk []byte) error { func (s *ServerUDPSession) EnsureServerState(method *CipherMethod, psk []byte) error {
@@ -303,6 +428,9 @@ func (s *ServerUDPSession) EnsureServerState(method *CipherMethod, psk []byte) e
if method.IsChaCha { if method.IsChaCha {
var err error var err error
s.serverChaCha, err = method.NewUDPCipher(psk) s.serverChaCha, err = method.NewUDPCipher(psk)
if err != nil {
s.ServerSessionID = 0
}
return err return err
} }
@@ -387,11 +515,12 @@ func (s *ServerUDPSession) EncodeServerPacket(method *CipherMethod, clientSessio
return res, nil return res, nil
} }
func (c *UDPCodec) EncodeServerPacket(clientSessionID uint64, dest net.Destination, payload []byte) ([]byte, error) { func (c *UDPServerCodec) EncodeServerPacket(clientSessionID uint64, dest net.Destination, payload []byte) ([]byte, error) {
return c.sessions.EncodeServerPacket(c.method, c.psk, clientSessionID, dest, payload) return c.sessions.EncodeServerPacket(c.method, c.psk, clientSessionID, dest, payload)
} }
type serverSessionState struct { type serverSessionState struct {
sync.Mutex
sessionID uint64 sessionID uint64
window *SlidingWindow window *SlidingWindow
cipher cipher.AEAD cipher cipher.AEAD
@@ -399,21 +528,20 @@ type serverSessionState struct {
} }
func (st *serverSessionState) check(packetID uint64) bool { func (st *serverSessionState) check(packetID uint64) bool {
if st.window == nil { st.Lock()
st.window = new(SlidingWindow) defer st.Unlock()
}
return st.window.Check(packetID) return st.window.Check(packetID)
} }
func (st *serverSessionState) add(packetID uint64) { func (st *serverSessionState) commit(packetID uint64) bool {
if st.window == nil { st.Lock()
st.window = new(SlidingWindow) defer st.Unlock()
} return st.window.CheckAndAdd(packetID)
st.window.Add(packetID)
} }
type ClientUDPSession struct { type ClientUDPSession struct {
codec *UDPCodec mu sync.Mutex
codec *UDPClientCodec
clientSessionID uint64 clientSessionID uint64
nextPacketID atomic.Uint64 nextPacketID atomic.Uint64
clientBodyCipher cipher.AEAD clientBodyCipher cipher.AEAD
@@ -421,7 +549,7 @@ type ClientUDPSession struct {
old atomic.Pointer[serverSessionState] old atomic.Pointer[serverSessionState]
} }
func (c *UDPCodec) NewClientSession() (*ClientUDPSession, error) { func (c *UDPClientCodec) NewClientSession() (*ClientUDPSession, error) {
var sessID [8]byte var sessID [8]byte
if _, err := io.ReadFull(rand.Reader, sessID[:]); err != nil { if _, err := io.ReadFull(rand.Reader, sessID[:]); err != nil {
return nil, err return nil, err
@@ -446,7 +574,7 @@ func (c *UDPCodec) NewClientSession() (*ClientUDPSession, error) {
}, nil }, nil
} }
func (s *ClientUDPSession) getServerSession(sessionID uint64, now int64) (*serverSessionState, error) { func (s *ClientUDPSession) getOrTransitionServerSession(sessionID uint64, cipher cipher.AEAD, now int64) (*serverSessionState, error) {
cur := s.current.Load() cur := s.current.Load()
if cur != nil && cur.sessionID == sessionID { if cur != nil && cur.sessionID == sessionID {
return cur, nil return cur, nil
@@ -455,39 +583,42 @@ func (s *ClientUDPSession) getServerSession(sessionID uint64, now int64) (*serve
old := s.old.Load() old := s.old.Load()
if old != nil && old.sessionID == sessionID { if old != nil && old.sessionID == sessionID {
if now-old.lastSeen.Load() > 60 { if now-old.lastSeen.Load() > 60 {
s.old.CompareAndSwap(old, nil)
return nil, errors.New("old server session expired") return nil, errors.New("old server session expired")
} }
return old, nil return old, nil
} }
// New server session: s.mu.Lock()
defer s.mu.Unlock()
cur = s.current.Load()
if cur != nil && cur.sessionID == sessionID {
return cur, nil
}
old = s.old.Load()
if old != nil && old.sessionID == sessionID {
if now-old.lastSeen.Load() > 60 {
return nil, errors.New("old server session expired")
}
return old, nil
}
// Spec §3.2.4: reject newer server sessions when the last packet received from the old session is less than 1 minute old. // Spec §3.2.4: reject newer server sessions when the last packet received from the old session is less than 1 minute old.
if old != nil && now-old.lastSeen.Load() < 60 { if old != nil && now-old.lastSeen.Load() < 60 {
return nil, errors.New("newer server session rejected: old session is less than 1 minute old") return nil, errors.New("newer server session rejected: old session is less than 1 minute old")
} }
var bodyAead cipher.AEAD
if !s.codec.method.IsChaCha {
var sessBytes [8]byte
binary.BigEndian.PutUint64(sessBytes[:], sessionID)
bodyKey := DeriveSessionSubKey(s.codec.psk, sessBytes[:], s.codec.method.KeySaltLength)
var err error
bodyAead, err = s.codec.method.NewAEAD(bodyKey)
if err != nil {
return nil, err
}
}
newState := &serverSessionState{ newState := &serverSessionState{
sessionID: sessionID, sessionID: sessionID,
cipher: bodyAead, window: new(SlidingWindow),
cipher: cipher,
} }
newState.lastSeen.Store(now) newState.lastSeen.Store(now)
if cur == nil { if cur == nil {
s.current.CompareAndSwap(nil, newState) s.current.Store(newState)
return s.current.Load(), nil return newState, nil
} }
s.old.Store(cur) s.old.Store(cur)
@@ -637,15 +768,6 @@ func (s *ClientUDPSession) DecodePacket(data []byte) (DecodedUDPPacket, error) {
sessionID := binary.BigEndian.Uint64(plain[:8]) sessionID := binary.BigEndian.Uint64(plain[:8])
packetID := binary.BigEndian.Uint64(plain[8:16]) packetID := binary.BigEndian.Uint64(plain[8:16])
now := time.Now().Unix()
st, err := s.getServerSession(sessionID, now)
if err != nil {
return DecodedUDPPacket{}, err
}
if !st.check(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
decoded, err := parsePlainUDPPacket(sessionID, packetID, plain[16:]) decoded, err := parsePlainUDPPacket(sessionID, packetID, plain[16:])
if err != nil { if err != nil {
return DecodedUDPPacket{}, err return DecodedUDPPacket{}, err
@@ -658,7 +780,16 @@ func (s *ClientUDPSession) DecodePacket(data []byte) (DecodedUDPPacket, error) {
return DecodedUDPPacket{}, errors.New("client session ID mismatch") return DecodedUDPPacket{}, errors.New("client session ID mismatch")
} }
st.add(packetID) now := time.Now().Unix()
st, err := s.getOrTransitionServerSession(sessionID, nil, now)
if err != nil {
return DecodedUDPPacket{}, err
}
// Atomically commit after all validations succeed
if !st.commit(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
st.lastSeen.Store(now) st.lastSeen.Store(now)
return decoded, nil return decoded, nil
@@ -671,15 +802,30 @@ func (s *ClientUDPSession) DecodePacket(data []byte) (DecodedUDPPacket, error) {
packetID := binary.BigEndian.Uint64(rawHeader[8:16]) packetID := binary.BigEndian.Uint64(rawHeader[8:16])
now := time.Now().Unix() now := time.Now().Unix()
st, err := s.getServerSession(sessionID, now)
if err != nil { // 1. Fast path for known active session
return DecodedUDPPacket{}, err cur := s.current.Load()
var st *serverSessionState
var bodyAead cipher.AEAD
if cur != nil && cur.sessionID == sessionID {
st = cur
bodyAead = cur.cipher
} else {
old := s.old.Load()
if old != nil && old.sessionID == sessionID {
if now-old.lastSeen.Load() > 60 {
return DecodedUDPPacket{}, errors.New("old server session expired")
} }
st = old
bodyAead = old.cipher
}
}
if st != nil {
if !st.check(packetID) { if !st.check(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique return DecodedUDPPacket{}, ErrPacketIdNotUnique
} }
bodyAead := st.cipher
bodyNonce := rawHeader[4:16] bodyNonce := rawHeader[4:16]
bodyCipher := data[16:] bodyCipher := data[16:]
bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil) bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil)
@@ -699,7 +845,61 @@ func (s *ClientUDPSession) DecodePacket(data []byte) (DecodedUDPPacket, error) {
return DecodedUDPPacket{}, errors.New("client session ID mismatch") return DecodedUDPPacket{}, errors.New("client session ID mismatch")
} }
st.add(packetID) if !st.commit(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
st.lastSeen.Store(now)
return decoded, nil
}
// 2. Unknown session: Trial path without mutating s.current or s.old yet!
// Re-check cur in case another worker concurrently transitioned to this sessionID
cur = s.current.Load()
old := s.old.Load()
if (cur == nil || cur.sessionID != sessionID) && (old == nil || old.sessionID != sessionID) {
// Spec §3.2.4: reject newer server sessions when the last packet received from the old session is less than 1 minute old.
if old != nil && now-old.lastSeen.Load() < 60 {
return DecodedUDPPacket{}, errors.New("newer server session rejected: old session is less than 1 minute old")
}
}
var sessBytes [8]byte
binary.BigEndian.PutUint64(sessBytes[:], sessionID)
bodyKey := DeriveSessionSubKey(s.codec.psk, sessBytes[:], s.codec.method.KeySaltLength)
trialAead, err := s.codec.method.NewAEAD(bodyKey)
if err != nil {
return DecodedUDPPacket{}, err
}
bodyNonce := rawHeader[4:16]
bodyCipher := data[16:]
bodyPlain, err := trialAead.Open(nil, bodyNonce, bodyCipher, nil)
if err != nil {
return DecodedUDPPacket{}, errors.New("failed to decrypt aes udp body").Base(err)
}
decoded, err := parsePlainUDPPacket(sessionID, packetID, bodyPlain)
if err != nil {
return DecodedUDPPacket{}, err
}
if decoded.HeaderType != HeaderTypeServer {
return DecodedUDPPacket{}, ErrBadHeaderType
}
if decoded.ClientSessionID != s.clientSessionID {
return DecodedUDPPacket{}, errors.New("client session ID mismatch")
}
// Payload authenticated! Safely commit and transition session under lock
st, err = s.getOrTransitionServerSession(sessionID, trialAead, now)
if err != nil {
return DecodedUDPPacket{}, err
}
if !st.commit(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
st.lastSeen.Store(now) st.lastSeen.Store(now)
return decoded, nil return decoded, nil
+4 -3
View File
@@ -191,7 +191,7 @@ func TestRelayUDPSessionStabilityAndDispatch(t *testing.T) {
// Verify downstream destination can decode both packets // Verify downstream destination can decode both packets
method, err := GetCipherMethod(MethodAES128GCM) method, err := GetCipherMethod(MethodAES128GCM)
common.Must(err) common.Must(err)
destCodec, err := NewUDPServerCodec(method, destKey, 300*time.Second) destCodec, err := NewUDPServerCodec(method, destKey, nil, 300*time.Second)
common.Must(err) common.Must(err)
mu.Lock() mu.Lock()
@@ -202,7 +202,8 @@ func TestRelayUDPSessionStabilityAndDispatch(t *testing.T) {
t.Fatalf("expected 2 received packets at destination, got %d", len(pkts)) t.Fatalf("expected 2 received packets at destination, got %d", len(pkts))
} }
dec1, err := destCodec.DecodePacket(pkts[0]) dec1, dec2 := DecodedUDPPacket{}, DecodedUDPPacket{}
_, dec1, err = destCodec.DecodePacket(pkts[0])
if err != nil { if err != nil {
t.Fatalf("dest failed to decode packet 1: %v", err) t.Fatalf("dest failed to decode packet 1: %v", err)
} }
@@ -210,7 +211,7 @@ func TestRelayUDPSessionStabilityAndDispatch(t *testing.T) {
t.Fatalf("dec1 mismatch: sess=%x, pktID=%d, payload=%s", dec1.SessionID, dec1.PacketID, string(dec1.Payload)) t.Fatalf("dec1 mismatch: sess=%x, pktID=%d, payload=%s", dec1.SessionID, dec1.PacketID, string(dec1.Payload))
} }
dec2, err := destCodec.DecodePacket(pkts[1]) _, dec2, err = destCodec.DecodePacket(pkts[1])
if err != nil { if err != nil {
t.Fatalf("dest failed to decode packet 2: %v", err) t.Fatalf("dest failed to decode packet 2: %v", err)
} }
+23 -7
View File
@@ -100,19 +100,27 @@ type ServerUDPSession struct {
func (s *ServerUDPSession) CheckPacketID(packetID uint64) bool { func (s *ServerUDPSession) CheckPacketID(packetID uint64) bool {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
if s.Window == nil {
s.Window = new(SlidingWindow)
}
return s.Window.Check(packetID) return s.Window.Check(packetID)
} }
func (s *ServerUDPSession) AddPacketID(packetID uint64) { func (s *ServerUDPSession) Commit(packetID uint64, bodyAead cipher.AEAD, user *protocol.MemoryUser, userPSK []byte) bool {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
if s.Window == nil { if s.User != nil && user != nil && s.User != user {
s.Window = new(SlidingWindow) return false
} }
s.Window.Add(packetID) if !s.Window.CheckAndAdd(packetID) {
return false
}
if s.clientBodyCipher == nil && bodyAead != nil {
s.clientBodyCipher = bodyAead
}
if s.User == nil && user != nil {
s.User = user
s.UserPSK = userPSK
}
s.LastActive.Store(time.Now().Unix())
return true
} }
type UDPSessionManager struct { type UDPSessionManager struct {
@@ -128,6 +136,13 @@ func NewUDPSessionManager(timeout time.Duration) *UDPSessionManager {
} }
} }
func (m *UDPSessionManager) Get(sessionID uint64) *ServerUDPSession {
if s, ok := m.sessions.Load(sessionID); ok {
return s
}
return nil
}
func (m *UDPSessionManager) GetOrCreate(sessionID uint64) *ServerUDPSession { func (m *UDPSessionManager) GetOrCreate(sessionID uint64) *ServerUDPSession {
now := time.Now().Unix() now := time.Now().Unix()
if s, ok := m.sessions.Load(sessionID); ok { if s, ok := m.sessions.Load(sessionID); ok {
@@ -137,6 +152,7 @@ func (m *UDPSessionManager) GetOrCreate(sessionID uint64) *ServerUDPSession {
s := &ServerUDPSession{ s := &ServerUDPSession{
SessionID: sessionID, SessionID: sessionID,
Window: new(SlidingWindow),
manager: m, manager: m,
} }
s.LastActive.Store(now) s.LastActive.Store(now)
+9 -3
View File
@@ -63,10 +63,16 @@ func (s *ServerUDPSession) EnsureLink(
s.currentConn.Store(conn) s.currentConn.Store(conn)
sessCtx, cancel := context.WithCancel(ctx) // UDP Session might travel across multiple connections
// don't bind its lifetime to a parent context
sessCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
inbound := session.InboundFromContext(sessCtx) inbound := session.InboundFromContext(sessCtx)
if inbound != nil && s.User != nil { if inbound != nil {
inbound.User = s.User newInbound := *inbound
if s.User != nil {
newInbound.User = s.User
}
sessCtx = session.ContextWithInbound(sessCtx, &newInbound)
} }
var email string var email string
var level uint32 var level uint32

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