Compare commits

..
1 Commits
Author SHA1 Message Date
Fangliding 6de10652d7 Do not start tun in init 2026-06-05 18:03:19 +08:00
309 changed files with 7266 additions and 17068 deletions
-1
View File
@@ -1 +0,0 @@
powershell.exe -ExecutionPolicy Bypass -File ".\xray_no_window.ps1"
-1
View File
@@ -1 +0,0 @@
Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden
-1
View File
@@ -1 +0,0 @@
CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0
-1
View File
@@ -37,7 +37,6 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
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/99_version.json
# Create log files
-1
View File
@@ -37,7 +37,6 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
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/99_version.json
# Create log files
+2 -21
View File
@@ -64,16 +64,8 @@ jobs:
echo "Latest: '$LATEST'."
echo "LATEST=$LATEST" >>${GITHUB_ENV}
NEWEST=false
if [[ "${{ github.event_name }}" == "release" ]]; then
NEWEST=true
fi
echo "Newest: '$NEWEST'."
echo "NEWEST=$NEWEST" >>${GITHUB_ENV}
- name: Checkout code
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -82,7 +74,7 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v4.6.0
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -132,13 +124,6 @@ jobs:
${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
fi
if [[ "${{ env.NEWEST }}" == "true" ]]; then
echo "Adding 'pre-release' tag to manifest: '${{ env.FULL_IMAGE_NAME }}:pre-release'."
docker buildx imagetools create \
--tag ${{ env.FULL_IMAGE_NAME }}:pre-release \
${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
fi
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
@@ -146,7 +131,3 @@ jobs:
if [[ "${{ env.LATEST }}" == "true" ]]; then
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:latest
fi
if [[ "${{ env.NEWEST }}" == "true" ]]; then
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:pre-release
fi
+9 -11
View File
@@ -14,13 +14,13 @@ jobs:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-wintun-
@@ -83,7 +83,7 @@ jobs:
CGO_ENABLED: 0
steps:
- name: Checkout codebase
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Show workflow information
run: |
@@ -92,7 +92,7 @@ jobs:
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -119,13 +119,13 @@ jobs:
# go build -o build_assets/wxray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main
- name: Restore Geodat Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-wintun-
@@ -134,17 +134,15 @@ jobs:
run: |
mv -f resources/geo* build_assets/
if [[ ${GOOS} == 'windows' ]]; then
cp .github/build/windows/* build_assets/
fi
if [[ ${GOOS} == 'windows' ]]; then
echo 'Adding Wintun into packages'
echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs
echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1
if [[ ${GOARCH} == 'amd64' ]]; then
mv resources/wintun/bin/amd64/wintun.dll build_assets/
fi
if [[ ${GOARCH} == '386' ]]; then
mv resources/wintun/bin/x86/wintun.dll build_assets/
fi
mv resources/wintun/LICENSE.txt build_assets/LICENSE-Wintun
mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt
fi
- name: Copy README.md & LICENSE
+9 -11
View File
@@ -14,13 +14,13 @@ jobs:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-wintun-
@@ -170,7 +170,7 @@ jobs:
CGO_ENABLED: 0
steps:
- name: Checkout codebase
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up NDK
if: matrix.goos == 'android'
@@ -193,7 +193,7 @@ jobs:
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
@@ -225,14 +225,14 @@ jobs:
fi
- name: Restore Geodat Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
if: matrix.goos == 'windows'
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-wintun-
@@ -241,10 +241,8 @@ jobs:
run: |
mv -f resources/geo* build_assets/
if [[ ${GOOS} == 'windows' ]]; then
cp .github/build/windows/* build_assets/
fi
if [[ ${GOOS} == 'windows' ]]; then
echo 'Adding Wintun into packages'
echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs
echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1
if [[ ${GOARCH} == 'amd64' ]]; then
mv resources/wintun/bin/amd64/wintun.dll build_assets/
fi
@@ -254,7 +252,7 @@ jobs:
if [[ ${GOARCH} == 'arm64' ]]; then
mv resources/wintun/bin/arm64/wintun.dll build_assets/
fi
mv resources/wintun/LICENSE.txt build_assets/LICENSE-Wintun
mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt
fi
- name: Copy README.md & LICENSE
+9 -16
View File
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-geodat-
@@ -59,7 +59,7 @@ jobs:
done
- name: Save Geodat Cache
uses: actions/cache/save@v6
uses: actions/cache/save@v5
if: ${{ steps.update.outputs.unhit }}
with:
path: resources
@@ -68,12 +68,9 @@ jobs:
wintun:
if: github.event.schedule == '30 22 * * *' || github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
env:
ASSETVER: 0.14.1
ASSETHASH: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51
steps:
- name: Restore Wintun Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-wintun-
@@ -99,6 +96,7 @@ jobs:
echo -e "Checking if wintun.dll for ${ARCHITECTURE} exists..."
if [ -s "./resources/wintun/bin/${ARCHITECTURE}/wintun.dll" ]; then
echo -e "wintun.dll for ${ARCHITECTURE} exists"
continue
else
echo -e "wintun.dll for ${ARCHITECTURE} is missing"
missing=true
@@ -115,21 +113,16 @@ jobs:
fi
if [[ "$missing" == true ]]; then
FILENAME=wintun.zip
DOWNLOAD_FILE=wintun-${ASSETVER}.zip
DOWNLOAD_FILE=wintun-0.14.1.zip
echo -e "Downloading https://www.wintun.net/builds/${DOWNLOAD_FILE}..."
curl -L "https://www.wintun.net/builds/${DOWNLOAD_FILE}" -o "${FILENAME}"
if [[ "$(sha256sum "./${FILENAME}" | awk -F ' ' '{print $1}')" == "${ASSETHASH}" ]]; then
echo -e "Unpacking wintun..."
unzip -u ${FILENAME} -d resources/
echo "unhit=true" >> $GITHUB_OUTPUT
else
echo -e "Digest of ${FILENAME} mismatch."
exit 1
fi
echo -e "Unpacking wintun..."
unzip -u ${FILENAME} -d resources/
echo "unhit=true" >> $GITHUB_OUTPUT
fi
- name: Save Wintun Cache
uses: actions/cache/save@v6
uses: actions/cache/save@v5
if: ${{ steps.update.outputs.unhit }}
with:
path: resources
+10 -8
View File
@@ -11,7 +11,7 @@ jobs:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-geodat-
@@ -40,7 +40,7 @@ jobs:
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
uses: actions/checkout@v6
- name: Check Proto Version Header
run: |
head -n 4 core/config.pb.go > ref.txt
@@ -59,15 +59,17 @@ jobs:
contents: read
steps:
- name: Checkout codebase
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
cache: false
- name: Check Format
run: go run ./infra/vformat/main.go -mode check -pwd ./
run: |
go install -v mvdan.cc/gofumpt@latest
go run ./infra/vformat/main.go -mode check -pwd ./
test:
needs: check-assets
@@ -81,14 +83,14 @@ jobs:
os: [windows-latest, ubuntu-latest, macos-latest]
steps:
- name: Checkout codebase
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Restore Geodat Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-geodat-
-22
View File
@@ -73,7 +73,6 @@
- [Xray_bash_onekey](https://github.com/hello-yunshu/Xray_bash_onekey), [XTool](https://github.com/LordPenguin666/XTool), [VPainLess](https://github.com/vpainless/vpainless)
- [v2ray-agent](https://github.com/mack-a/v2ray-agent), [Xray_onekey](https://github.com/wulabing/Xray_onekey), [ProxySU](https://github.com/proxysu/ProxySU)
- Magisk
- [Magic_V2Ray](https://github.com/vincentng295/Magic_V2Ray)
- [Xray_For_Magisk](https://github.com/E7KMbb/Xray_For_Magisk)
- Homebrew
- `brew install xray`
@@ -187,27 +186,6 @@
- [Xray-core v1.0.0](https://github.com/XTLS/Xray-core/releases/tag/v1.0.0) was forked from [v2fly-core 9a03cc5](https://github.com/v2fly/v2ray-core/commit/9a03cc5c98d04cc28320fcee26dbc236b3291256), and we have made & accumulated a huge number of enhancements over time, check [the release notes for each version](https://github.com/XTLS/Xray-core/releases).
- For third-party projects used in [Xray-core](https://github.com/XTLS/Xray-core), check your local or [the latest go.mod](https://github.com/XTLS/Xray-core/blob/main/go.mod).
### Bundled Third-Party Components Redistribution
**Certain optional features dynamically load third-party components. These optional components are separate works distributed under their own licenses, and are bundled into the ZIP package for ease of use. Users may replace these components under the licenses from these components.**
These components include:
#### Wintun
This distribution contains unmodified official precompiled and pre-signed Wintun binaries.
- Project: Wintun
- Copyright: Copyright (C) 2018-2021 WireGuard LLC. All Rights Reserved.
- Redistribution License: Prebuilt Binaries License (PBL) bundled with official precompiled and pre-signed binaries from wintun.net
- Component(s): wintun.dll
- Source: https://www.wintun.net/
- Included in:
- Windows x86 (windows-32, win7-32)
- Windows x86-64 (windows-64, win7-64)
- Windows AArch64 (windows-arm64)
- Notes: Wintun is an optional runtime-loaded component only used for TUN inbound functionality on supported Windows platforms.
## One-line Compilation
### Windows (PowerShell)
+5 -5
View File
@@ -162,7 +162,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
p := d.policy.ForLevel(user.Level)
if p.Stats.UserUplink {
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
if c, _ := d.stats.GetOrRegisterCounter(name); c != nil {
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
inboundLink.Writer = &SizeStatWriter{
Counter: c,
Writer: inboundLink.Writer,
@@ -171,7 +171,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
}
if p.Stats.UserDownlink {
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
if c, _ := d.stats.GetOrRegisterCounter(name); c != nil {
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
outboundLink.Writer = &SizeStatWriter{
Counter: c,
Writer: outboundLink.Writer,
@@ -200,13 +200,13 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
p := policyManager.ForLevel(user.Level)
if p.Stats.UserUplink {
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
if c, _ := statsManager.GetOrRegisterCounter(name); c != nil {
if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil {
link.Reader.(*buf.TimeoutWrapperReader).Counter = c
}
}
if p.Stats.UserDownlink {
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
if c, _ := statsManager.GetOrRegisterCounter(name); c != nil {
if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil {
link.Writer = &SizeStatWriter{
Counter: c,
Writer: link.Writer,
@@ -223,7 +223,7 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
func trackOnlineIP(ctx context.Context, sm stats.Manager, email, ip string) {
name := "user>>>" + email + ">>>online"
if om, _ := sm.GetOrRegisterOnlineMap(name); om != nil {
if om, _ := stats.GetOrRegisterOnlineMap(sm, name); om != nil {
om.AddIP(ip)
context.AfterFunc(ctx, func() { om.RemoveIP(ip) })
}
+1 -1
View File
@@ -23,7 +23,7 @@ func newFakeDNSSniffer(ctx context.Context) (protocolSnifferWithMetadata, error)
}
if fakeDNSEngine == nil {
errNotInit := errors.New("FakeDNSEngine is not initialized, but such a sniffer is used")
errNotInit := errors.New("FakeDNSEngine is not initialized, but such a sniffer is used").AtError()
return protocolSnifferWithMetadata{}, errNotInit
}
return protocolSnifferWithMetadata{protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) {
+1 -1
View File
@@ -28,7 +28,7 @@ func toNetIP(addrs []net.Address) ([]net.IP, error) {
if addr.Family().IsIP() {
ips = append(ips, addr.IP())
} else {
return nil, errors.New("Failed to convert address", addr, "to Net IP.")
return nil, errors.New("Failed to convert address", addr, "to Net IP.").AtWarning()
}
}
return ips, nil
+4 -9
View File
@@ -188,24 +188,19 @@ func parseResponse(payload []byte) (*IPRecord, error) {
var parser dnsmessage.Parser
h, err := parser.Start(payload)
if err != nil {
return nil, errors.New("failed to parse DNS response").Base(err)
return nil, errors.New("failed to parse DNS response").Base(err).AtWarning()
}
if err := parser.SkipAllQuestions(); err != nil {
return nil, errors.New("failed to skip questions in DNS response").Base(err)
return nil, errors.New("failed to skip questions in DNS response").Base(err).AtWarning()
}
now := time.Now()
ipRecord := &IPRecord{
ReqID: h.ID,
RCode: h.RCode,
Expire: now.Add(time.Second * dns_feature.DefaultTTL),
RawHeader: &h,
}
defer func() {
// set to default TTL if no valid TTL is found
if ipRecord.Expire.IsZero() {
ipRecord.Expire = now.Add(time.Second * dns_feature.DefaultTTL)
}
}()
L:
for {
@@ -222,7 +217,7 @@ L:
ttl = 1
}
expire := now.Add(time.Duration(ttl) * time.Second)
if ipRecord.Expire.IsZero() || ipRecord.Expire.After(expire) {
if ipRecord.Expire.After(expire) {
ipRecord.Expire = expire
}
+3 -3
View File
@@ -58,7 +58,7 @@ func NewFakeDNSHolder() (*Holder, error) {
var err error
if fkdns, err = NewFakeDNSHolderConfigOnly(nil); err != nil {
return nil, errors.New("Unable to create Fake Dns Engine").Base(err)
return nil, errors.New("Unable to create Fake Dns Engine").Base(err).AtError()
}
err = fkdns.initialize(dns.FakeIPv4Pool, 65535)
if err != nil {
@@ -80,13 +80,13 @@ func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error {
var err error
if _, ipRange, err = net.ParseCIDR(ipPoolCidr); err != nil {
return errors.New("Unable to parse CIDR for Fake DNS IP assignment").Base(err)
return errors.New("Unable to parse CIDR for Fake DNS IP assignment").Base(err).AtError()
}
ones, bits := ipRange.Mask.Size()
rooms := bits - ones
if math.Log2(float64(lruSize)) >= float64(rooms) {
return errors.New("LRU size is bigger than subnet size")
return errors.New("LRU size is bigger than subnet size").AtError()
}
fkdns.domainToIP = cache.NewLru(lruSize)
fkdns.ipRange = ipRange
+4 -4
View File
@@ -84,7 +84,7 @@ func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dis
if dest.Network == net.Network_UDP { // UDP classic DNS mode
return NewClassicNameServer(dest, dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP), nil
}
return nil, errors.New("No available name server could be created from ", dest)
return nil, errors.New("No available name server could be created from ", dest).AtWarning()
}
// NewClient creates a DNS client managing a name server with client IP, domain rules and expected IPs.
@@ -102,7 +102,7 @@ func NewClient(
// Create a new server for each client for now
server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP)
if err != nil {
return errors.New("failed to create nameserver").Base(err)
return errors.New("failed to create nameserver").Base(err).AtWarning()
}
_, isLocalDNS := server.(*LocalNameServer)
@@ -113,7 +113,7 @@ func NewClient(
if len(ns.ExpectedIp) > 0 {
expectedMatcher, err = geodata.IPReg.BuildIPMatcher(ns.ExpectedIp)
if err != nil {
return errors.New("failed to create expected ip matcher").Base(err)
return errors.New("failed to create expected ip matcher").Base(err).AtWarning()
}
}
@@ -122,7 +122,7 @@ func NewClient(
if len(ns.UnexpectedIp) > 0 {
unexpectedMatcher, err = geodata.IPReg.BuildIPMatcher(ns.UnexpectedIp)
if err != nil {
return errors.New("failed to create unexpected ip matcher").Base(err)
return errors.New("failed to create unexpected ip matcher").Base(err).AtWarning()
}
}
+2 -2
View File
@@ -27,7 +27,7 @@ func (s *FakeDNSServer) IsDisableCache() bool {
func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, opt dns.IPOption) ([]net.IP, uint32, error) {
if f.fakeDNSEngine == nil {
return nil, 0, errors.New("Unable to locate a fake DNS Engine")
return nil, 0, errors.New("Unable to locate a fake DNS Engine").AtError()
}
var ips []net.Address
@@ -39,7 +39,7 @@ func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, opt dns.IPOp
netIP, err := toNetIP(ips)
if err != nil {
return nil, 0, errors.New("Unable to convert IP to net ip").Base(err)
return nil, 0, errors.New("Unable to convert IP to net ip").Base(err).AtError()
}
errors.LogInfo(ctx, f.Name(), " got answer: ", domain, " -> ", ips)
+41 -83
View File
@@ -2,7 +2,6 @@ package geodata
import (
"context"
"crypto/tls"
go_errors "errors"
"io"
"net/http"
@@ -10,7 +9,6 @@ import (
"path/filepath"
"time"
utls "github.com/refraction-networking/utls"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform/filesystem"
@@ -18,7 +16,6 @@ import (
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport/internet/tagged"
"golang.org/x/net/http2"
)
const idleTimeout = 30 * time.Second
@@ -29,9 +26,8 @@ type stage struct {
}
type downloader struct {
ctx context.Context
httpClient *http.Client
httpsClient *http.Client
ctx context.Context
client *http.Client
}
type idleConn struct {
@@ -57,84 +53,52 @@ func (c *idleConn) Write(b []byte) (int, error) {
func newDownloader(ctx context.Context, dispatcher routing.Dispatcher, outbound string) *downloader {
return &downloader{
ctx: ctx,
httpClient: newClient(ctx, dispatcher, outbound, false),
httpsClient: newClient(ctx, dispatcher, outbound, true),
ctx: ctx,
client: newClient(ctx, dispatcher, outbound),
}
}
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) {
var conn net.Conn
err := task.Run(ctx, func() error {
if tagged.Dialer == nil {
return errors.New("tagged dialer is not initialized")
}
dest, err := net.ParseDestination(network + ":" + address)
if err != nil {
return errors.New("cannot understand address").Base(err)
}
c, err := tagged.Dialer(baseCtx, dispatcher, dest, outbound)
if err != nil {
return errors.New("cannot dial remote address ", dest).Base(err)
}
conn = c
return nil
})
if err != nil {
return nil, errors.New("cannot finish connection").Base(err)
}
return &idleConn{
Conn: conn,
}, nil
}
if isHTTPS {
return &http.Client{
Transport: &http2.Transport{
DialTLSContext: func(ctx context.Context, network string, address string, cfg *tls.Config) (net.Conn, error) {
conn, err := dial(ctx, network, address)
func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string) *http.Client {
return &http.Client{
Transport: &http.Transport{
Proxy: nil,
DisableKeepAlives: true,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
var conn net.Conn
err := task.Run(ctx, func() error {
if tagged.Dialer == nil {
return errors.New("tagged dialer is not initialized")
}
dest, err := net.ParseDestination(network + ":" + address)
if err != nil {
return nil, err
return errors.New("cannot understand address").Base(err)
}
host, _, _ := net.SplitHostPort(address)
tlsConn := utls.UClient(conn, &utls.Config{ServerName: host}, utls.HelloChrome_Auto)
handshakeCtx, cancel := context.WithTimeout(ctx, idleTimeout)
defer cancel()
if err := tlsConn.HandshakeContext(handshakeCtx); err != nil {
conn.Close()
return nil, err
c, err := tagged.Dialer(baseCtx, dispatcher, dest, outbound)
if err != nil {
return errors.New("cannot dial remote address ", dest).Base(err)
}
return tlsConn, nil
},
},
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())
conn = c
return nil
})
if err != nil {
return nil, errors.New("cannot finish connection").Base(err)
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
return &idleConn{
Conn: conn,
}, 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
},
}
TLSHandshakeTimeout: idleTimeout,
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
},
}
}
@@ -196,13 +160,7 @@ func (d *downloader) fetch(rawURL string, writer io.Writer) error {
}
utils.TryDefaultHeadersWith(req.Header, "nav")
var client *http.Client
if req.URL.Scheme == "https" {
client = d.httpsClient
} else {
client = d.httpClient
}
resp, err := client.Do(req)
resp, err := d.client.Do(req)
if err != nil {
return err
}
+2 -6
View File
@@ -89,10 +89,10 @@ func (g *Instance) startInternal() error {
g.active = true
if err := g.initAccessLogger(); err != nil {
return errors.New("failed to initialize access logger").Base(err)
return errors.New("failed to initialize access logger").Base(err).AtWarning()
}
if err := g.initErrorLogger(); err != nil {
return errors.New("failed to initialize error logger").Base(err)
return errors.New("failed to initialize error logger").Base(err).AtWarning()
}
return nil
@@ -141,10 +141,6 @@ func (g *Instance) Handle(msg log.Message) {
}
}
func (g *Instance) Severity() log.Severity {
return g.config.ErrorLogLevel
}
// Close implements common.Closable.Close().
func (g *Instance) Close() error {
errors.LogDebug(context.Background(), "Logger closing")
+59 -151
View File
@@ -2,18 +2,15 @@ package metrics
import (
"context"
"encoding/json"
stderrors "errors"
"expvar"
stdnet "net"
"net/http"
"net/http/pprof"
_ "net/http/pprof"
"strings"
"github.com/xtls/xray-core/app/observatory"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/signal/done"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/extension"
@@ -24,17 +21,15 @@ import (
type MetricsHandler struct {
ohm outbound.Manager
statsManager feature_stats.Manager
ctx context.Context
observatory extension.Observatory
tag string
listen string
tcpListener xnet.Listener
listener *OutboundListener
tcpListener net.Listener
}
// NewMetricsHandler creates a new MetricsHandler based on the given config.
func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, error) {
c := &MetricsHandler{
ctx: ctx,
tag: config.Tag,
listen: config.Listen,
}
@@ -42,6 +37,46 @@ func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, er
c.statsManager = sm
c.ohm = om
}))
expvar.Publish("stats", expvar.Func(func() interface{} {
resp := map[string]map[string]map[string]int64{
"inbound": {},
"outbound": {},
"user": {},
}
c.statsManager.VisitCounters(func(name string, counter feature_stats.Counter) bool {
nameSplit := strings.Split(name, ">>>")
typeName, tagOrUser, direction := nameSplit[0], nameSplit[1], nameSplit[3]
if item, found := resp[typeName][tagOrUser]; found {
item[direction] = counter.Value()
} else {
resp[typeName][tagOrUser] = map[string]int64{
direction: counter.Value(),
}
}
return true
})
return resp
}))
expvar.Publish("observatory", expvar.Func(func() interface{} {
if c.observatory == nil {
common.Must(core.RequireFeatures(ctx, func(observatory extension.Observatory) error {
c.observatory = observatory
return nil
}))
if c.observatory == nil {
return nil
}
}
resp := map[string]*observatory.OutboundStatus{}
if o, err := c.observatory.GetObservation(context.Background()); err != nil {
return err
} else {
for _, x := range o.(*observatory.ObservationResult).GetStatus() {
resp[x.OutboundTag] = x
}
}
return resp
}))
return c, nil
}
@@ -50,172 +85,45 @@ func (p *MetricsHandler) Type() interface{} {
}
func (p *MetricsHandler) Start() error {
handler := p.httpHandler()
// direct listen a port if listen is set
if p.listen != "" {
TCPlistener, err := xnet.Listen("tcp", p.listen)
TCPlistener, err := net.Listen("tcp", p.listen)
if err != nil {
return err
}
p.tcpListener = TCPlistener
errors.LogInfo(context.Background(), "Metrics server listening on ", p.listen)
go p.serve(TCPlistener, handler)
}
if p.tag == "" {
if p.tcpListener == nil {
return errors.New("metrics must have a tag or listen address")
}
return nil
go func() {
if err := http.Serve(TCPlistener, http.DefaultServeMux); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to start metrics server")
}
}()
}
listener := &OutboundListener{
buffer: make(chan xnet.Conn, 4),
buffer: make(chan net.Conn, 4),
done: done.New(),
}
p.listener = listener
go p.serve(listener, handler)
go func() {
if err := http.Serve(listener, http.DefaultServeMux); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to start metrics server")
}
}()
if err := p.ohm.RemoveHandler(context.Background(), p.tag); err != nil {
errors.LogInfo(context.Background(), "failed to remove existing handler")
}
if err := p.ohm.AddHandler(context.Background(), &Outbound{
return p.ohm.AddHandler(context.Background(), &Outbound{
tag: p.tag,
listener: listener,
}); err != nil {
if closeErr := p.Close(); closeErr != nil {
errors.LogErrorInner(context.Background(), closeErr, "failed to close metrics server after start failure")
}
return err
}
return nil
})
}
func (p *MetricsHandler) Close() error {
var errs []error
if p.tcpListener != nil {
errs = append(errs, p.tcpListener.Close())
p.tcpListener = nil
}
if p.listener != nil {
errs = append(errs, p.listener.Close())
p.listener = nil
}
if p.ohm != nil && p.tag != "" {
if err := p.ohm.RemoveHandler(context.Background(), p.tag); err != nil {
errors.LogInfo(context.Background(), "failed to remove metrics handler")
}
}
return errors.Combine(errs...)
}
func (p *MetricsHandler) serve(listener xnet.Listener, handler http.Handler) {
if err := http.Serve(listener, handler); err != nil && !isClosedListenerError(err) {
errors.LogErrorInner(context.Background(), err, "failed to start metrics server")
}
}
func isClosedListenerError(err error) bool {
if err == nil {
return true
}
if stderrors.Is(err, stdnet.ErrClosed) || stderrors.Is(err, http.ErrServerClosed) {
return true
}
errText := err.Error()
return strings.Contains(errText, "listen closed") ||
strings.Contains(errText, "use of closed network connection")
}
func (p *MetricsHandler) httpHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/debug/vars", p.handleDebugVars)
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
return mux
}
func (p *MetricsHandler) handleDebugVars(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
vars := map[string]json.RawMessage{}
expvar.Do(func(kv expvar.KeyValue) {
value := json.RawMessage(kv.Value.String())
if !json.Valid(value) {
value = json.RawMessage("null")
}
vars[kv.Key] = value
})
vars["stats"] = marshalJSON(p.stats())
vars["observatory"] = marshalJSON(p.observatoryStatus())
payload, err := json.Marshal(vars)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(payload)
}
func marshalJSON(value interface{}) json.RawMessage {
data, err := json.Marshal(value)
if err != nil {
return json.RawMessage("null")
}
return data
}
func (p *MetricsHandler) stats() map[string]map[string]map[string]int64 {
resp := map[string]map[string]map[string]int64{
"inbound": {},
"outbound": {},
"user": {},
}
p.statsManager.VisitCounters(func(name string, counter feature_stats.Counter) bool {
nameSplit := strings.Split(name, ">>>")
if len(nameSplit) < 4 {
return true
}
typeName, tagOrUser, direction := nameSplit[0], nameSplit[1], nameSplit[3]
items, found := resp[typeName]
if !found {
items = map[string]map[string]int64{}
resp[typeName] = items
}
if item, found := items[tagOrUser]; found {
item[direction] = counter.Value()
} else {
items[tagOrUser] = map[string]int64{
direction: counter.Value(),
}
}
return true
})
return resp
}
func (p *MetricsHandler) observatoryStatus() interface{} {
feature := core.MustFromContext(p.ctx).GetFeature(extension.ObservatoryType())
if feature == nil {
return nil
}
observatoryFeature := feature.(extension.Observatory)
resp := map[string]*observatory.OutboundStatus{}
if o, err := observatoryFeature.GetObservation(context.Background()); err != nil {
return err
} else {
for _, x := range o.(*observatory.ObservationResult).GetStatus() {
resp[x.OutboundTag] = x
}
}
return resp
return nil
}
func init() {
-161
View File
@@ -1,161 +0,0 @@
package metrics
import (
"context"
"encoding/json"
stdnet "net"
"net/http"
"net/http/httptest"
"testing"
"github.com/xtls/xray-core/app/dispatcher"
"github.com/xtls/xray-core/app/proxyman"
_ "github.com/xtls/xray-core/app/proxyman/inbound"
_ "github.com/xtls/xray-core/app/proxyman/outbound"
appstats "github.com/xtls/xray-core/app/stats"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/core"
feature_outbound "github.com/xtls/xray-core/features/outbound"
)
func TestMetricsCanRestartInSameProcess(t *testing.T) {
for i := 0; i < 2; i++ {
server := startMetricsTestServer(t)
readMetricsVars(t, server)
readMetricsPprof(t, server)
if err := server.Close(); err != nil {
t.Fatalf("failed to close metrics server: %v", err)
}
}
}
func TestMetricsCanRunMultipleInstancesInSameProcess(t *testing.T) {
server1 := startMetricsTestServer(t)
t.Cleanup(func() {
_ = server1.Close()
})
server2 := startMetricsTestServer(t)
t.Cleanup(func() {
_ = server2.Close()
})
readMetricsVars(t, server1)
readMetricsVars(t, server2)
}
func TestMetricsListenOnlyWithoutTagDoesNotRegisterOutbound(t *testing.T) {
listen := pickMetricsListenAddress(t)
server := startMetricsTestServerWithMetricsConfig(t, &Config{
Listen: listen,
})
t.Cleanup(func() {
_ = server.Close()
})
response, err := http.Get("http://" + listen + "/debug/vars")
if err != nil {
t.Fatalf("failed to read listen-only metrics: %v", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
t.Fatalf("unexpected listen-only metrics status: %d", response.StatusCode)
}
outboundManager := server.GetFeature(feature_outbound.ManagerType()).(feature_outbound.Manager)
if handlers := outboundManager.ListHandlers(context.Background()); len(handlers) != 0 {
t.Fatalf("listen-only metrics registered outbound handlers: got %d, want 0", len(handlers))
}
}
func startMetricsTestServer(t *testing.T) *core.Instance {
return startMetricsTestServerWithMetricsConfig(t, &Config{
Tag: "metrics_out",
})
}
func startMetricsTestServerWithMetricsConfig(t *testing.T, metricsConfig *Config) *core.Instance {
t.Helper()
server, err := core.New(metricsTestConfig(metricsConfig))
if err != nil {
t.Fatalf("failed to create metrics server: %v", err)
}
if err := server.Start(); err != nil {
_ = server.Close()
t.Fatalf("failed to start metrics server: %v", err)
}
return server
}
func metricsTestConfig(metricsConfig *Config) *core.Config {
return &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&appstats.Config{}),
serial.ToTypedMessage(metricsConfig),
},
}
}
func pickMetricsListenAddress(t *testing.T) string {
t.Helper()
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to pick metrics listen address: %v", err)
}
defer listener.Close()
return listener.Addr().String()
}
func readMetricsVars(t *testing.T, server *core.Instance) {
t.Helper()
recorder := httptest.NewRecorder()
metricsHandler(t, server).httpHandler().ServeHTTP(
recorder,
httptest.NewRequest(http.MethodGet, "/debug/vars", nil),
)
if recorder.Code != http.StatusOK {
t.Fatalf("unexpected metrics vars status: %d", recorder.Code)
}
var payload map[string]interface{}
if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil {
t.Fatalf("failed to decode metrics vars: %v", err)
}
if _, found := payload["stats"]; !found {
t.Fatal("metrics vars missing stats")
}
if _, found := payload["observatory"]; !found {
t.Fatal("metrics vars missing observatory")
}
}
func readMetricsPprof(t *testing.T, server *core.Instance) {
t.Helper()
recorder := httptest.NewRecorder()
metricsHandler(t, server).httpHandler().ServeHTTP(
recorder,
httptest.NewRequest(http.MethodGet, "/debug/pprof/goroutine?debug=1", nil),
)
if recorder.Code != http.StatusOK {
t.Fatalf("unexpected metrics pprof status: %d", recorder.Code)
}
}
func metricsHandler(t *testing.T, server *core.Instance) *MetricsHandler {
t.Helper()
feature := server.GetFeature((*MetricsHandler)(nil))
handler, ok := feature.(*MetricsHandler)
if !ok || handler == nil {
t.Fatal("metrics handler not registered")
}
return handler
}
-6
View File
@@ -78,12 +78,6 @@ func (o *Observer) background() {
sleepTime = time.Duration(o.config.ProbeInterval)
}
if len(outbounds) == 0 {
errors.LogWarning(o.ctx, "no outbound matches subjectSelector ", o.config.SubjectSelector)
time.Sleep(sleepTime)
continue
}
if !o.config.EnableConcurrency {
sort.Strings(outbounds)
for _, v := range outbounds {
+22 -11
View File
@@ -330,6 +330,7 @@ type SenderConfig struct {
// Send traffic through the given IP. Only IP is allowed.
Via *net.IPOrDomain `protobuf:"bytes,1,opt,name=via,proto3" json:"via,omitempty"`
StreamSettings *internet.StreamConfig `protobuf:"bytes,2,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"`
ProxySettings *internet.ProxyConfig `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"`
MultiplexSettings *MultiplexingConfig `protobuf:"bytes,4,opt,name=multiplex_settings,json=multiplexSettings,proto3" json:"multiplex_settings,omitempty"`
ViaCidr string `protobuf:"bytes,5,opt,name=via_cidr,json=viaCidr,proto3" json:"via_cidr,omitempty"`
TargetStrategy internet.DomainStrategy `protobuf:"varint,6,opt,name=target_strategy,json=targetStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"target_strategy,omitempty"`
@@ -381,6 +382,13 @@ func (x *SenderConfig) GetStreamSettings() *internet.StreamConfig {
return nil
}
func (x *SenderConfig) GetProxySettings() *internet.ProxyConfig {
if x != nil {
return x.ProxySettings
}
return nil
}
func (x *SenderConfig) GetMultiplexSettings() *MultiplexingConfig {
if x != nil {
return x.MultiplexSettings
@@ -498,13 +506,14 @@ const file_app_proxyman_config_proto_rawDesc = "" +
"\x03tag\x18\x01 \x01(\tR\x03tag\x12M\n" +
"\x11receiver_settings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\x10receiverSettings\x12G\n" +
"\x0eproxy_settings\x18\x03 \x01(\v2 .xray.common.serial.TypedMessageR\rproxySettings\"\x10\n" +
"\x0eOutboundConfig\"\xd6\x02\n" +
"\x0eOutboundConfig\"\x9d\x03\n" +
"\fSenderConfig\x12-\n" +
"\x03via\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x03via\x12N\n" +
"\x0fstream_settings\x18\x02 \x01(\v2%.xray.transport.internet.StreamConfigR\x0estreamSettings\x12T\n" +
"\x0fstream_settings\x18\x02 \x01(\v2%.xray.transport.internet.StreamConfigR\x0estreamSettings\x12K\n" +
"\x0eproxy_settings\x18\x03 \x01(\v2$.xray.transport.internet.ProxyConfigR\rproxySettings\x12T\n" +
"\x12multiplex_settings\x18\x04 \x01(\v2%.xray.app.proxyman.MultiplexingConfigR\x11multiplexSettings\x12\x19\n" +
"\bvia_cidr\x18\x05 \x01(\tR\aviaCidr\x12P\n" +
"\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategyJ\x04\b\x03\x10\x04\"\xa4\x01\n" +
"\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategy\"\xa4\x01\n" +
"\x12MultiplexingConfig\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\x12 \n" +
"\vconcurrency\x18\x02 \x01(\x05R\vconcurrency\x12(\n" +
@@ -539,7 +548,8 @@ var file_app_proxyman_config_proto_goTypes = []any{
(*net.IPOrDomain)(nil), // 10: xray.common.net.IPOrDomain
(*internet.StreamConfig)(nil), // 11: xray.transport.internet.StreamConfig
(*serial.TypedMessage)(nil), // 12: xray.common.serial.TypedMessage
(internet.DomainStrategy)(0), // 13: xray.transport.internet.DomainStrategy
(*internet.ProxyConfig)(nil), // 13: xray.transport.internet.ProxyConfig
(internet.DomainStrategy)(0), // 14: xray.transport.internet.DomainStrategy
}
var file_app_proxyman_config_proto_depIdxs = []int32{
7, // 0: xray.app.proxyman.SniffingConfig.domains_excluded:type_name -> xray.common.geodata.DomainRule
@@ -552,13 +562,14 @@ var file_app_proxyman_config_proto_depIdxs = []int32{
12, // 7: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage
10, // 8: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain
11, // 9: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig
6, // 10: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig
13, // 11: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy
12, // [12:12] is the sub-list for method output_type
12, // [12:12] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
13, // 10: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig
6, // 11: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig
14, // 12: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy
13, // [13:13] is the sub-list for method output_type
13, // [13:13] is the sub-list for method input_type
13, // [13:13] is the sub-list for extension type_name
13, // [13:13] is the sub-list for extension extendee
0, // [0:13] is the sub-list for field type_name
}
func init() { file_app_proxyman_config_proto_init() }
+1 -1
View File
@@ -57,7 +57,7 @@ message SenderConfig {
// Send traffic through the given IP. Only IP is allowed.
xray.common.net.IPOrDomain via = 1;
xray.transport.internet.StreamConfig stream_settings = 2;
reserved 3;
xray.transport.internet.ProxyConfig proxy_settings = 3;
MultiplexingConfig multiplex_settings = 4;
string via_cidr = 5;
xray.transport.internet.DomainStrategy target_strategy = 6;
+16 -18
View File
@@ -26,7 +26,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.InboundUplink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + tag + ">>>traffic>>>uplink"
c, _ := statsManager.GetOrRegisterCounter(name)
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
uplinkCounter = c
}
@@ -34,7 +34,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.InboundDownlink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + tag + ">>>traffic>>>downlink"
c, _ := statsManager.GetOrRegisterCounter(name)
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
downlinkCounter = c
}
@@ -57,23 +57,16 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
if err != nil {
return nil, err
}
src := net.TCPDestination(net.AnyIP, 0)
if receiverConfig.Listen != nil {
src.Address = receiverConfig.Listen.AsAddress()
}
if receiverConfig.PortList != nil && len(receiverConfig.PortList.Range) > 0 {
src.Port = net.Port(receiverConfig.PortList.Range[0].From)
}
mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings)
if err != nil {
return nil, errors.New("failed to parse stream config").Base(err)
}
newCtx := session.ContextWithInbound(ctx, &session.Inbound{Tag: tag, Source: src})
newCtx = session.ContextWithContent(newCtx, &session.Content{SniffingRequest: sniffingRequest})
newCtx = session.ContextWithStreamSettings(newCtx, mss)
rawProxy, err := common.CreateObject(newCtx, proxyConfig)
// Set tag and sniffing config in context before creating proxy
// This allows proxies like TUN to access these settings
ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: tag})
if receiverConfig.SniffingSettings != nil {
ctx = session.ContextWithContent(ctx, &session.Content{
SniffingRequest: sniffingRequest,
})
}
rawProxy, err := common.CreateObject(ctx, proxyConfig)
if err != nil {
return nil, err
}
@@ -99,6 +92,11 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
address = net.AnyIP
}
mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings)
if err != nil {
return nil, errors.New("failed to parse stream config").Base(err).AtWarning()
}
if receiverConfig.ReceiveOriginalDestination {
if mss.SocketSettings == nil {
mss.SocketSettings = &internet.SocketConfig{}
+1 -1
View File
@@ -165,7 +165,7 @@ func NewHandler(ctx context.Context, config *core.InboundHandlerConfig) (inbound
receiverSettings, ok := rawReceiverSettings.(*proxyman.ReceiverConfig)
if !ok {
return nil, errors.New("not a ReceiverConfig")
return nil, errors.New("not a ReceiverConfig").AtError()
}
streamSettings := receiverSettings.StreamSettings
+2 -2
View File
@@ -142,7 +142,7 @@ func (w *tcpWorker) Start() error {
go w.callback(conn)
})
if err != nil {
return errors.New("failed to listen TCP on ", w.port).Base(err)
return errors.New("failed to listen TCP on ", w.port).AtWarning().Base(err)
}
w.hub = hub
return nil
@@ -528,7 +528,7 @@ func (w *dsWorker) Start() error {
go w.callback(conn)
})
if err != nil {
return errors.New("failed to listen Unix Domain Socket on ", w.address).Base(err)
return errors.New("failed to listen Unix Domain Socket on ", w.address).AtWarning().Base(err)
}
w.hub = hub
return nil
+66 -15
View File
@@ -6,6 +6,7 @@ import (
goerrors "errors"
"io"
"math/big"
"os"
"github.com/xtls/xray-core/common/dice"
@@ -15,6 +16,7 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/mux"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/net/cnc"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core"
@@ -25,6 +27,8 @@ import (
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tls"
"github.com/xtls/xray-core/transport/pipe"
"google.golang.org/protobuf/proto"
)
@@ -36,7 +40,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.OutboundUplink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "outbound>>>" + tag + ">>>traffic>>>uplink"
c, _ := statsManager.GetOrRegisterCounter(name)
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
uplinkCounter = c
}
@@ -44,7 +48,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.OutboundDownlink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "outbound>>>" + tag + ">>>traffic>>>downlink"
c, _ := statsManager.GetOrRegisterCounter(name)
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
downlinkCounter = c
}
@@ -60,6 +64,7 @@ type Handler struct {
streamSettings *internet.MemoryStreamConfig
proxyConfig proto.Message
proxy proxy.Outbound
outboundManager outbound.Manager
mux *mux.ClientManager
xudp *mux.ClientManager
udp443 string
@@ -73,6 +78,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
uplinkCounter, downlinkCounter := getStatCounter(v, config.Tag)
h := &Handler{
tag: config.Tag,
outboundManager: v.GetFeature(outbound.ManagerType()).(outbound.Manager),
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
}
@@ -87,7 +93,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
h.senderSettings = s
mss, err := internet.ToMemoryStreamConfig(s.StreamSettings)
if err != nil {
return nil, errors.New("failed to parse stream settings").Base(err)
return nil, errors.New("failed to parse stream settings").Base(err).AtWarning()
}
h.streamSettings = mss
default:
@@ -103,10 +109,6 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
ctx = session.ContextWithFullHandler(ctx, h)
if h.streamSettings != nil {
ctx = session.ContextWithStreamSettings(ctx, h.streamSettings)
}
rawProxyHandler, err := common.CreateObject(ctx, proxyConfig)
if err != nil {
return nil, err
@@ -194,6 +196,7 @@ func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) {
common.Interrupt(link.Reader)
return
}
} else {
unchangedDomain := ob.Target.Address.Domain()
ob.Target.Address = net.IPAddress(ips[dice.Roll(len(ips))])
@@ -217,7 +220,7 @@ func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) {
if ob.Target.Network == net.Network_UDP && ob.Target.Port == 443 {
switch h.udp443 {
case "reject":
test(errors.New("XUDP rejected UDP/443 traffic"))
test(errors.New("XUDP rejected UDP/443 traffic").AtInfo())
return
case "skip":
goto out
@@ -266,26 +269,71 @@ func (h *Handler) DestIpAddress() net.IP {
// Dial implements internet.Dialer.
func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) {
if h.senderSettings != nil && h.senderSettings.Via != nil {
outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1]
h.SetOutboundGateway(ctx, ob)
if h.senderSettings != nil {
if h.senderSettings.ProxySettings.HasTag() {
tag := h.senderSettings.ProxySettings.Tag
handler := h.outboundManager.GetHandler(tag)
if handler != nil {
errors.LogDebug(ctx, "proxying to ", tag, " for dest ", dest)
outbounds := session.OutboundsFromContext(ctx)
ctx = session.ContextWithOutbounds(ctx, append(outbounds, &session.Outbound{
Target: dest,
Tag: tag,
})) // add another outbound in session ctx
opts := pipe.OptionsFromContext(ctx)
uplinkReader, uplinkWriter := pipe.New(opts...)
downlinkReader, downlinkWriter := pipe.New(opts...)
go handler.Dispatch(ctx, &transport.Link{Reader: uplinkReader, Writer: downlinkWriter})
conn := cnc.NewConnection(cnc.ConnectionInputMulti(uplinkWriter), cnc.ConnectionOutputMulti(downlinkReader))
if config := tls.ConfigFromStreamSettings(h.streamSettings); config != nil {
tlsConfig := config.GetTLSConfig(tls.WithDestination(dest))
conn = tls.Client(conn, tlsConfig)
}
return h.getStatCouterConnection(conn), nil
}
errors.LogError(ctx, "failed to get outbound handler with tag: ", tag)
return nil, errors.New("failed to get outbound handler with tag: " + tag)
}
if h.senderSettings.Via != nil {
outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1]
h.SetOutboundGateway(ctx, ob)
}
}
if conn, err := h.getUoTConnection(ctx, dest); err != os.ErrInvalid {
return conn, err
}
conn, err := internet.Dial(ctx, dest, h.streamSettings)
conn = h.getStatCouterConnection(conn)
outbounds := session.OutboundsFromContext(ctx)
if outbounds != nil {
ob := outbounds[len(outbounds)-1]
ob.Conn = conn
} else {
// for Vision's pre-connect
}
return conn, err
}
func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound) {
if ob.Gateway == nil && h.senderSettings != nil && h.senderSettings.Via != nil &&
(h.streamSettings.SocketSettings == nil || len(h.streamSettings.SocketSettings.DialerProxy) == 0) {
if ob.Gateway == nil && h.senderSettings != nil && h.senderSettings.Via != nil && !h.senderSettings.ProxySettings.HasTag() && (h.streamSettings.SocketSettings == nil || len(h.streamSettings.SocketSettings.DialerProxy) == 0) {
var domain string
addr := h.senderSettings.Via.AsAddress()
domain = h.senderSettings.Via.GetDomain()
switch {
case h.senderSettings.ViaCidr != "":
ob.Gateway = ParseRandomIP(addr, h.senderSettings.ViaCidr)
case domain == "origin":
if inbound := session.InboundFromContext(ctx); inbound != nil {
if inbound.Local.IsValid() && inbound.Local.Address.Family().IsIP() {
@@ -300,9 +348,12 @@ func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound)
errors.LogDebug(ctx, "use inbound source ip as sendthrough: ", inbound.Source.Address.String())
}
}
default: // case addr.Family().IsDomain():
// case addr.Family().IsDomain():
default:
ob.Gateway = addr
}
}
}
+35
View File
@@ -0,0 +1,35 @@
package outbound
import (
"context"
"os"
"github.com/sagernet/sing/common/uot"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat"
)
func (h *Handler) getUoTConnection(ctx context.Context, dest net.Destination) (stat.Connection, error) {
if dest.Address == nil {
return nil, errors.New("nil destination address")
}
if !dest.Address.Family().IsDomain() {
return nil, os.ErrInvalid
}
var uotVersion int
if dest.Address.Domain() == uot.MagicAddress {
uotVersion = uot.Version
} else if dest.Address.Domain() == uot.LegacyMagicAddress {
uotVersion = uot.LegacyVersion
} else {
return nil, os.ErrInvalid
}
packetConn, err := internet.ListenSystemPacket(ctx, &net.UDPAddr{IP: net.AnyIP.IP(), Port: 0}, h.streamSettings.SocketSettings)
if err != nil {
return nil, errors.New("unable to listen socket").Base(err)
}
conn := uot.NewServerConn(packetConn, uotVersion)
return h.getStatCouterConnection(conn), nil
}
+2 -2
View File
@@ -68,13 +68,13 @@ func (p *Portal) HandleConnection(ctx context.Context, link *transport.Link) err
outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1]
if ob == nil {
return errors.New("outbound metadata not found")
return errors.New("outbound metadata not found").AtError()
}
if isDomain(ob.Target, p.domain) {
muxClient, err := mux.NewClientWorker(*link, mux.ClientStrategy{})
if err != nil {
return errors.New("failed to create mux client worker").Base(err)
return errors.New("failed to create mux client worker").Base(err).AtWarning()
}
worker, err := NewPortalWorker(muxClient)
+3 -3
View File
@@ -136,7 +136,7 @@ func (b *Balancer) SelectOutbounds() ([]string, error) {
// GetPrincipleTarget implements routing.BalancerPrincipleTarget
func (r *Router) GetPrincipleTarget(tag string) ([]string, error) {
if b, ok := (*r.balancers.Load())[tag]; ok {
if b, ok := r.balancers[tag]; ok {
if s, ok := b.strategy.(BalancingPrincipleTarget); ok {
candidates, err := b.SelectOutbounds()
if err != nil {
@@ -151,7 +151,7 @@ func (r *Router) GetPrincipleTarget(tag string) ([]string, error) {
// SetOverrideTarget implements routing.BalancerOverrider
func (r *Router) SetOverrideTarget(tag, target string) error {
if b, ok := (*r.balancers.Load())[tag]; ok {
if b, ok := r.balancers[tag]; ok {
b.override.Put(target)
return nil
}
@@ -160,7 +160,7 @@ func (r *Router) SetOverrideTarget(tag, target string) error {
// GetOverrideTarget implements routing.BalancerOverrider
func (r *Router) GetOverrideTarget(tag string) (string, error) {
if b, ok := (*r.balancers.Load())[tag]; ok {
if b, ok := r.balancers[tag]; ok {
return b.override.Get(), nil
}
return "", errors.New("cannot find tag")
+17
View File
@@ -2,8 +2,25 @@ package router
import (
sync "sync"
"github.com/xtls/xray-core/common/errors"
)
func (r *Router) OverrideBalancer(balancer string, target string) error {
var b *Balancer
for tag, bl := range r.balancers {
if tag == balancer {
b = bl
break
}
}
if b == nil {
return errors.New("balancer '", balancer, "' not found")
}
b.override.Put(target)
return nil
}
type overrideSettings struct {
target string
}
-20
View File
@@ -5,7 +5,6 @@ import (
"os"
"path/filepath"
"regexp"
"runtime"
"slices"
"strings"
@@ -394,22 +393,3 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
}
return false
}
// LocalOSMatcher matches the operating system Xray itself is running on. That never
// changes while Xray is running, so the result is resolved when the rule is built.
type LocalOSMatcher struct {
matched bool
}
func NewLocalOSMatcher(names []string) *LocalOSMatcher {
return &LocalOSMatcher{
matched: slices.ContainsFunc(names, func(name string) bool {
return strings.EqualFold(name, runtime.GOOS)
}),
}
}
// Apply implements Condition.
func (m *LocalOSMatcher) Apply(_ routing.Context) bool {
return m.matched
}
-27
View File
@@ -2,9 +2,7 @@ package router_test
import (
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
. "github.com/xtls/xray-core/app/router"
@@ -345,31 +343,6 @@ func TestChinaSites(t *testing.T) {
}
}
func TestLocalOSRule(t *testing.T) {
otherOS := "plan9"
if runtime.GOOS == otherOS {
otherOS = "linux"
}
cases := []struct {
localOS []string
output bool
}{
{localOS: []string{runtime.GOOS}, output: true},
{localOS: []string{otherOS}, output: false},
{localOS: []string{otherOS, runtime.GOOS}, output: true},
{localOS: []string{strings.ToUpper(runtime.GOOS)}, output: true},
}
for _, test := range cases {
cond, err := (&RoutingRule{LocalOs: test.localOS}).BuildCondition()
common.Must(err)
if got := cond.Apply(withBackground()); got != test.output {
t.Errorf("for localOS %v on %s: expected %v, got %v", test.localOS, runtime.GOOS, test.output, got)
}
}
}
func BenchmarkMphDomainMatcher(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules, err := geodata.ParseDomainRules([]string{"geosite:cn"}, geodata.Domain_Substr)
+2 -6
View File
@@ -33,10 +33,6 @@ func (r *Rule) Apply(ctx routing.Context) bool {
func (rr *RoutingRule) BuildCondition() (Condition, error) {
conds := NewConditionChan()
if len(rr.LocalOs) > 0 {
conds.Add(NewLocalOSMatcher(rr.LocalOs))
}
if len(rr.InboundTag) > 0 {
conds.Add(NewInboundTagMatcher(rr.InboundTag))
}
@@ -115,7 +111,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) {
}
if conds.Len() == 0 {
return nil, errors.New("this rule has no effective fields")
return nil, errors.New("this rule has no effective fields").AtWarning()
}
return conds, nil
@@ -145,7 +141,7 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch
}
s, ok := i.(*StrategyLeastLoadConfig)
if !ok {
return nil, errors.New("not a StrategyLeastLoadConfig")
return nil, errors.New("not a StrategyLeastLoadConfig").AtError()
}
leastLoadStrategy := NewLeastLoadStrategy(s)
return &Balancer{
+4 -14
View File
@@ -107,10 +107,8 @@ type RoutingRule struct {
VlessRouteList *net.PortList `protobuf:"bytes,20,opt,name=vless_route_list,json=vlessRouteList,proto3" json:"vless_route_list,omitempty"`
Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"`
Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"`
// List of operating systems for matching the one Xray itself is running on.
LocalOs []string `protobuf:"bytes,23,rep,name=local_os,json=localOs,proto3" json:"local_os,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RoutingRule) Reset() {
@@ -280,13 +278,6 @@ func (x *RoutingRule) GetWebhook() *WebhookConfig {
return nil
}
func (x *RoutingRule) GetLocalOs() []string {
if x != nil {
return x.LocalOs
}
return nil
}
type isRoutingRule_TargetTag interface {
isRoutingRule_TargetTag()
}
@@ -646,7 +637,7 @@ var File_app_router_config_proto protoreflect.FileDescriptor
const file_app_router_config_proto_rawDesc = "" +
"\n" +
"\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"\xdc\a\n" +
"\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"\xc1\a\n" +
"\vRoutingRule\x12\x12\n" +
"\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\n" +
"\rbalancing_tag\x18\f \x01(\tH\x00R\fbalancingTag\x12\x19\n" +
@@ -670,8 +661,7 @@ const file_app_router_config_proto_rawDesc = "" +
"\x0flocal_port_list\x18\x12 \x01(\v2\x19.xray.common.net.PortListR\rlocalPortList\x12C\n" +
"\x10vless_route_list\x18\x14 \x01(\v2\x19.xray.common.net.PortListR\x0evlessRouteList\x12\x18\n" +
"\aprocess\x18\x15 \x03(\tR\aprocess\x128\n" +
"\awebhook\x18\x16 \x01(\v2\x1e.xray.app.router.WebhookConfigR\awebhook\x12\x19\n" +
"\blocal_os\x18\x17 \x03(\tR\alocalOs\x1a=\n" +
"\awebhook\x18\x16 \x01(\v2\x1e.xray.app.router.WebhookConfigR\awebhook\x1a=\n" +
"\x0fAttributesEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\f\n" +
-3
View File
@@ -56,9 +56,6 @@ message RoutingRule {
repeated string process = 21;
WebhookConfig webhook = 22;
// List of operating systems for matching the one Xray itself is running on.
repeated string local_os = 23;
}
message WebhookConfig {
+114 -59
View File
@@ -2,9 +2,7 @@ package router
import (
"context"
"maps"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
@@ -19,8 +17,8 @@ import (
// Router is an implementation of routing.Router.
type Router struct {
domainStrategy Config_DomainStrategy
rules atomic.Pointer[[]*Rule]
balancers atomic.Pointer[map[string]*Balancer]
rules []*Rule
balancers map[string]*Balancer
dns dns.Client
ctx context.Context
@@ -45,9 +43,52 @@ func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm out
r.ohm = ohm
r.dispatcher = dispatcher
r.rules.Store(new([]*Rule))
r.balancers.Store(&map[string]*Balancer{})
return r.ReloadRules(config, false)
r.balancers = make(map[string]*Balancer, len(config.BalancingRule))
for _, rule := range config.BalancingRule {
balancer, err := rule.Build(ohm, dispatcher)
if err != nil {
return err
}
balancer.InjectContext(ctx)
r.balancers[rule.Tag] = balancer
}
r.rules = make([]*Rule, 0, len(config.Rule))
for _, rule := range config.Rule {
cond, err := rule.BuildCondition()
if err != nil {
r.closeWebhooks()
return err
}
rr := &Rule{
Condition: cond,
Tag: rule.GetTag(),
RuleTag: rule.GetRuleTag(),
}
if wh := rule.GetWebhook(); wh != nil {
notifier, err := NewWebhookNotifier(wh)
if err != nil {
r.closeWebhooks()
return err
}
rr.Webhook = notifier
}
btag := rule.GetBalancingTag()
if len(btag) > 0 {
brule, found := r.balancers[btag]
if !found {
if rr.Webhook != nil {
rr.Webhook.Close()
}
r.closeWebhooks()
return errors.New("balancer ", btag, " not found")
}
rr.Balancer = brule
}
r.rules = append(r.rules, rr)
}
return nil
}
// PickRoute implements routing.Router.
@@ -83,22 +124,18 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
r.mu.Lock()
defer r.mu.Unlock()
oldRules := *r.rules.Load()
oldBalancers := *r.balancers.Load()
var newRules []*Rule
newBalancers := make(map[string]*Balancer)
existTags := make(map[string]bool, len(oldRules)+len(config.Rule))
if shouldAppend {
newRules = append(newRules, oldRules...)
maps.Copy(newBalancers, oldBalancers)
for _, rule := range oldRules {
existTags[rule.RuleTag] = true
if !shouldAppend {
for _, rule := range r.rules {
if rule.Webhook != nil {
rule.Webhook.Close()
}
}
r.balancers = make(map[string]*Balancer, len(config.BalancingRule))
r.rules = make([]*Rule, 0, len(config.Rule))
}
for _, rule := range config.BalancingRule {
if _, found := newBalancers[rule.Tag]; found {
_, found := r.balancers[rule.Tag]
if found {
return errors.New("duplicate balancer tag")
}
balancer, err := rule.Build(r.ohm, r.dispatcher)
@@ -106,12 +143,27 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
return err
}
balancer.InjectContext(r.ctx)
newBalancers[rule.Tag] = balancer
r.balancers[rule.Tag] = balancer
}
startIdx := len(r.rules)
closeNewWebhooks := func() {
for i := startIdx; i < len(r.rules); i++ {
if r.rules[i].Webhook != nil {
r.rules[i].Webhook.Close()
}
}
r.rules = r.rules[:startIdx]
}
for _, rule := range config.Rule {
if r.RuleExists(rule.GetRuleTag()) {
closeNewWebhooks()
return errors.New("duplicate ruleTag ", rule.GetRuleTag())
}
cond, err := rule.BuildCondition()
if err != nil {
closeNewWebhooks()
return err
}
rr := &Rule{
@@ -119,64 +171,69 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
Tag: rule.GetTag(),
RuleTag: rule.GetRuleTag(),
}
if rr.RuleTag != "" && existTags[rr.RuleTag] {
return errors.New("duplicate ruleTag ", rr.RuleTag)
}
existTags[rr.RuleTag] = true
if wh := rule.GetWebhook(); wh != nil {
notifier, err := NewWebhookNotifier(wh)
if err != nil {
closeNewWebhooks()
return err
}
rr.Webhook = notifier
}
if btag := rule.GetBalancingTag(); len(btag) > 0 {
brule, found := newBalancers[btag]
btag := rule.GetBalancingTag()
if len(btag) > 0 {
brule, found := r.balancers[btag]
if !found {
if rr.Webhook != nil {
rr.Webhook.Close()
}
closeNewWebhooks()
return errors.New("balancer ", btag, " not found")
}
rr.Balancer = brule
}
newRules = append(newRules, rr)
r.rules = append(r.rules, rr)
}
r.balancers.Store(&newBalancers)
r.rules.Store(&newRules)
if !shouldAppend {
closeWebhooks(oldRules)
}
return nil
}
func (r *Router) RuleExists(tag string) bool {
if tag != "" {
for _, rule := range r.rules {
if rule.RuleTag == tag {
return true
}
}
}
return false
}
// RemoveRule implements routing.Router.
func (r *Router) RemoveRule(tag string) error {
if tag == "" {
return errors.New("empty tag name!")
}
r.mu.Lock()
defer r.mu.Unlock()
oldRules := *r.rules.Load()
newRules := make([]*Rule, 0, len(oldRules))
var removed []*Rule
for _, rule := range oldRules {
if rule.RuleTag != tag {
newRules = append(newRules, rule)
} else {
removed = append(removed, rule)
newRules := []*Rule{}
if tag != "" {
for _, rule := range r.rules {
if rule.RuleTag != tag {
newRules = append(newRules, rule)
} else if rule.Webhook != nil {
rule.Webhook.Close()
}
}
r.rules = newRules
return nil
}
r.rules.Store(&newRules)
closeWebhooks(removed)
return nil
return errors.New("empty tag name!")
}
// ListRule implements routing.Router
func (r *Router) ListRule() []routing.Route {
rules := *r.rules.Load()
ruleList := make([]routing.Route, 0, len(rules))
for _, rule := range rules {
r.mu.Lock()
defer r.mu.Unlock()
ruleList := make([]routing.Route, 0)
for _, rule := range r.rules {
ruleList = append(ruleList, &Route{
outboundTag: rule.Tag,
ruleTag: rule.RuleTag,
@@ -195,9 +252,7 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
}
rules := *r.rules.Load()
for _, rule := range rules {
for _, rule := range r.rules {
if rule.Apply(ctx) {
return rule, ctx, nil
}
@@ -210,7 +265,7 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
// Try applying rules again if we have IPs.
for _, rule := range rules {
for _, rule := range r.rules {
if rule.Apply(ctx) {
return rule, ctx, nil
}
@@ -224,9 +279,9 @@ func (r *Router) Start() error {
return nil
}
// closeWebhooks closes all webhook notifiers in the given rule set.
func closeWebhooks(rules []*Rule) {
for _, rule := range rules {
// closeWebhooks closes all webhook notifiers in the current rule set.
func (r *Router) closeWebhooks() {
for _, rule := range r.rules {
if rule.Webhook != nil {
rule.Webhook.Close()
}
@@ -237,7 +292,7 @@ func closeWebhooks(rules []*Rule) {
func (r *Router) Close() error {
r.mu.Lock()
defer r.mu.Unlock()
closeWebhooks(*r.rules.Load())
r.closeWebhooks()
return nil
}
+23 -17
View File
@@ -8,7 +8,6 @@ import (
"net"
"net/http"
"sync"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common/errors"
@@ -41,7 +40,6 @@ type WebhookNotifier struct {
deduplication uint32
client *http.Client
seen sync.Map
lastSweep atomic.Int64
done chan struct{}
wg sync.WaitGroup
closeOnce sync.Once
@@ -79,6 +77,11 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
}
}
if h.deduplication > 0 {
h.wg.Add(1)
go h.cleanupLoop()
}
return h, nil
}
@@ -198,7 +201,6 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
}
ttl := time.Duration(h.deduplication) * time.Second
now := time.Now()
h.maybeSweep(now, ttl)
if v, loaded := h.seen.LoadOrStore(email, now); loaded {
if now.Sub(v.(time.Time)) < ttl {
return true
@@ -208,23 +210,27 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
return false
}
func (h *WebhookNotifier) maybeSweep(now time.Time, ttl time.Duration) {
last := h.lastSweep.Load()
if now.UnixNano()-last < int64(ttl) {
return
}
if !h.lastSweep.CompareAndSwap(last, now.UnixNano()) {
return // another goroutine did the sweep
}
h.seen.Range(func(key, value any) bool {
if now.Sub(value.(time.Time)) >= ttl {
h.seen.Delete(key)
func (h *WebhookNotifier) cleanupLoop() {
defer h.wg.Done()
ttl := time.Duration(h.deduplication) * time.Second
ticker := time.NewTicker(ttl)
defer ticker.Stop()
for {
select {
case <-h.done:
return
case <-ticker.C:
now := time.Now()
h.seen.Range(func(key, value any) bool {
if now.Sub(value.(time.Time)) >= ttl {
h.seen.Delete(key)
}
return true
})
}
return true
})
}
}
// Only need to call if the Notifier is really used, otherwise GC can clean it
func (h *WebhookNotifier) Close() error {
h.closeOnce.Do(func() {
close(h.done)
-48
View File
@@ -48,20 +48,6 @@ func (m *Manager) RegisterCounter(name string) (stats.Counter, error) {
return c, nil
}
// GetOrRegisterCounter implements stats.Manager.
func (m *Manager) GetOrRegisterCounter(name string) (stats.Counter, error) {
m.access.Lock()
defer m.access.Unlock()
if c, found := m.counters[name]; found {
return c, nil
}
errors.LogDebug(context.Background(), "create new counter ", name)
c := new(Counter)
m.counters[name] = c
return c, nil
}
// UnregisterCounter implements stats.Manager.
func (m *Manager) UnregisterCounter(name string) error {
m.access.Lock()
@@ -111,20 +97,6 @@ func (m *Manager) RegisterOnlineMap(name string) (stats.OnlineMap, error) {
return om, nil
}
// GetOrRegisterOnlineMap implements stats.Manager.
func (m *Manager) GetOrRegisterOnlineMap(name string) (stats.OnlineMap, error) {
m.access.Lock()
defer m.access.Unlock()
if om, found := m.onlineMaps[name]; found {
return om, nil
}
errors.LogDebug(context.Background(), "create new OnlineMap ", name)
om := NewOnlineMap()
m.onlineMaps[name] = om
return om, nil
}
// UnregisterOnlineMap implements stats.Manager.
func (m *Manager) UnregisterOnlineMap(name string) error {
m.access.Lock()
@@ -177,26 +149,6 @@ func (m *Manager) RegisterChannel(name string) (stats.Channel, error) {
return c, nil
}
// GetOrRegisterChannel implements stats.Manager.
func (m *Manager) GetOrRegisterChannel(name string) (stats.Channel, error) {
m.access.Lock()
defer m.access.Unlock()
if c, found := m.channels[name]; found {
return c, nil
}
errors.LogDebug(context.Background(), "create new channel ", name)
c := NewChannel(&ChannelConfig{BufferSize: 64, Blocking: false})
if m.running {
// Start before publishing so no goroutine can observe an unstarted channel.
if err := c.Start(); err != nil {
return nil, err
}
}
m.channels[name] = c
return c, nil
}
// UnregisterChannel implements stats.Manager.
func (m *Manager) UnregisterChannel(name string) error {
m.access.Lock()
+1 -1
View File
@@ -122,7 +122,7 @@ func NewReader(reader io.Reader) Reader {
}
_, isFile := reader.(*os.File)
if !isFile && useReadV() {
if !isFile && useReadv {
if sc, ok := reader.(syscall.Conn); ok {
rawConn, err := sc.SyscallConn()
if err != nil {
+7 -19
View File
@@ -5,7 +5,6 @@ package buf
import (
"io"
"sync/atomic"
"syscall"
"github.com/xtls/xray-core/common/platform"
@@ -144,24 +143,13 @@ func (r *ReadVReader) ReadMultiBuffer() (MultiBuffer, error) {
return mb, nil
}
var useReadv atomic.Bool
func useReadV() bool {
return useReadv.Load()
}
func reloadEnvSettings() error {
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
value := platform.NewEnvFlag(platform.UseReadV).GetValue(func() string { return defaultFlagValue })
enabled := false
switch value {
case defaultFlagValue, "auto", "enable":
enabled = true
}
useReadv.Store(enabled)
return nil
}
var useReadv bool
func init() {
platform.RegisterEnvReload(reloadEnvSettings)
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
value := platform.NewEnvFlag(platform.UseReadV).GetValue(func() string { return defaultFlagValue })
switch value {
case defaultFlagValue, "auto", "enable":
useReadv = true
}
}
+1 -3
View File
@@ -10,9 +10,7 @@ import (
"github.com/xtls/xray-core/features/stats"
)
func useReadV() bool {
return false
}
const useReadv = false
func NewReadVReader(reader io.Reader, rawConn syscall.RawConn, counter stats.Counter) Reader {
panic("not implemented")
+1 -3
View File
@@ -118,9 +118,7 @@ func (w *BufferedWriter) Write(b []byte) (int, error) {
nBytes, err := w.buffer.Write(b)
totalBytes += nBytes
// ErrBufferFull means a partial write, so flush below and continue
if err != nil && err != ErrBufferFull {
if err != nil {
return totalBytes, err
}
if !w.buffered || w.buffer.IsFull() {
+3 -3
View File
@@ -10,12 +10,12 @@ import (
// [,)
func RandBetween(from int64, to int64) int64 {
if from == to {
return from
}
if from > to {
from, to = to, from
}
if d := to - from; d == 0 || d == 1 {
return from
}
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
return from + bigInt.Int64()
}
+65 -13
View File
@@ -18,12 +18,17 @@ type hasInnerError interface {
Unwrap() error
}
type hasSeverity interface {
Severity() log.Severity
}
// Error is an error object with underlying error.
type Error struct {
prefix []interface{}
message []interface{}
caller string
inner error
prefix []interface{}
message []interface{}
caller string
inner error
severity log.Severity
}
// Error implements error.Error().
@@ -64,6 +69,46 @@ func (err *Error) Base(e error) *Error {
return err
}
func (err *Error) atSeverity(s log.Severity) *Error {
err.severity = s
return err
}
func (err *Error) Severity() log.Severity {
if err.inner == nil {
return err.severity
}
if s, ok := err.inner.(hasSeverity); ok {
as := s.Severity()
if as < err.severity {
return as
}
}
return err.severity
}
// AtDebug sets the severity to debug.
func (err *Error) AtDebug() *Error {
return err.atSeverity(log.Severity_Debug)
}
// AtInfo sets the severity to info.
func (err *Error) AtInfo() *Error {
return err.atSeverity(log.Severity_Info)
}
// AtWarning sets the severity to warning.
func (err *Error) AtWarning() *Error {
return err.atSeverity(log.Severity_Warning)
}
// AtError sets the severity to error.
func (err *Error) AtError() *Error {
return err.atSeverity(log.Severity_Error)
}
// String returns the string representation of this error.
func (err *Error) String() string {
return err.Error()
@@ -87,8 +132,9 @@ func New(msg ...interface{}) *Error {
details = details[:i]
}
return &Error{
message: msg,
caller: details,
message: msg,
severity: log.Severity_Info,
caller: details,
}
}
@@ -125,9 +171,6 @@ func LogErrorInner(ctx context.Context, inner error, msg ...interface{}) {
}
func doLog(ctx context.Context, inner error, severity log.Severity, msg ...interface{}) {
if log.GetSeverity() < severity {
return
}
pc, _, _, _ := runtime.Caller(2)
details := runtime.FuncForPC(pc).Name()
if len(details) >= trim {
@@ -138,9 +181,10 @@ func doLog(ctx context.Context, inner error, severity log.Severity, msg ...inter
details = details[:i]
}
err := &Error{
message: msg,
caller: details,
inner: inner,
message: msg,
severity: severity,
caller: details,
inner: inner,
}
if ctx != nil && ctx != context.Background() {
id := uint32(c.IDFromContext(ctx))
@@ -149,7 +193,7 @@ func doLog(ctx context.Context, inner error, severity log.Severity, msg ...inter
}
}
log.Record(&log.GeneralMessage{
Severity: severity,
Severity: GetSeverity(err),
Content: err,
})
}
@@ -173,3 +217,11 @@ L:
}
return err
}
// GetSeverity returns the actual severity of the error, including inner errors.
func GetSeverity(err error) log.Severity {
if s, ok := err.(hasSeverity); ok {
return s.Severity()
}
return log.Severity_Info
}
+15 -6
View File
@@ -7,21 +7,30 @@ import (
"github.com/google/go-cmp/cmp"
. "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log"
)
func TestError(t *testing.T) {
err := New("TestError")
if v := err.Error(); !strings.Contains(v, "TestError") {
t.Error("error: ", v)
if v := GetSeverity(err); v != log.Severity_Info {
t.Error("severity: ", v)
}
err = New("TestError2").Base(io.EOF)
if v := err.Error(); !strings.Contains(v, "EOF") {
t.Error("error: ", v)
if v := GetSeverity(err); v != log.Severity_Info {
t.Error("severity: ", v)
}
err = New("TestError3").Base(io.EOF)
err = New("TestError4").Base(err)
err = New("TestError3").Base(io.EOF).AtWarning()
if v := GetSeverity(err); v != log.Severity_Warning {
t.Error("severity: ", v)
}
err = New("TestError4").Base(io.EOF).AtWarning()
err = New("TestError5").Base(err)
if v := GetSeverity(err); v != log.Severity_Warning {
t.Error("severity: ", v)
}
if v := err.Error(); !strings.Contains(v, "EOF") {
t.Error("error: ", v)
}
-49
View File
@@ -1,49 +0,0 @@
package geodata
import (
"sync"
"github.com/xtls/xray-core/common"
)
var privateIPMatcher = sync.OnceValue(func() IPMatcher {
return common.Must2(IPReg.BuildIPMatcher(common.Must2(ParseIPRules([]string{
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"224.0.0.0/3",
"::/127",
"fc00::/7",
"fe80::/10",
"ff00::/8",
}))))
})
func GetPrivateIPMatcher() IPMatcher { return privateIPMatcher() }
var privateDomainMatcher = sync.OnceValue(func() DomainMatcher {
return common.Must2(DomainReg.BuildDomainMatcher(common.Must2(ParseDomainRules([]string{
"lan",
"localdomain",
"example",
"invalid",
"localhost",
"test",
"local",
"home.arpa",
"internal",
"regexp:^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$", // Dotless domains
}, Domain_Domain))))
})
func GetPrivateDomainMatcher() DomainMatcher { return privateDomainMatcher() }
+1 -1
View File
@@ -220,7 +220,7 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
case Domain_Regex:
return strmatcher.Regex.New(d.Value)
case Domain_Domain:
return strmatcher.Domain.New(strings.ToLower(d.Value))
return strmatcher.Domain.New(d.Value)
case Domain_Full:
return strmatcher.Full.New(strings.ToLower(d.Value))
default:
+7 -15
View File
@@ -6,14 +6,12 @@ import (
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/common/uuid"
)
type DomainRegistry struct {
mu sync.Mutex
factory DomainMatcherFactory
matchers *utils.WeakCacheMap[uuid.UUID, DynamicDomainMatcher]
matchers []*DynamicDomainMatcher
}
func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) {
@@ -26,7 +24,7 @@ func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher,
}
d := NewDynamicDomainMatcher(rules, m)
r.matchers.Store(uuid.New(), d)
r.matchers = append(r.matchers, d)
return d, nil
}
@@ -34,20 +32,15 @@ func (r *DomainRegistry) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
var matchers []*DynamicDomainMatcher
r.matchers.Range(func(_ uuid.UUID, matcher *DynamicDomainMatcher) bool {
matchers = append(matchers, matcher)
return true
})
errors.LogInfo(context.Background(), "reloading GeoSite data for ", len(matchers), " domain matcher(s)")
errors.LogInfo(context.Background(), "reloading GeoSite data for ", len(r.matchers), " domain matcher(s)")
factory := newDomainMatcherFactory()
type reloadEntry struct {
dynamic *DynamicDomainMatcher
matcher DomainMatcher
}
reloaded := make([]reloadEntry, len(matchers))
for i, d := range matchers {
reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range r.matchers {
m, err := factory.BuildMatcher(d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoSite data for domain matcher ", i)
@@ -59,14 +52,13 @@ func (r *DomainRegistry) Reload() error {
entry.dynamic.Reload(entry.matcher)
}
r.factory = factory
errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(matchers), " domain matcher(s)")
errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(r.matchers), " domain matcher(s)")
return nil
}
func newDomainRegistry() *DomainRegistry {
return &DomainRegistry{
factory: newDomainMatcherFactory(),
matchers: utils.NewWeakCacheMap[uuid.UUID, DynamicDomainMatcher](),
factory: newDomainMatcherFactory(),
}
}
+11 -19
View File
@@ -7,27 +7,25 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/common/uuid"
)
type IPRegistry struct {
mu sync.Mutex
factory *IPSetFactory
matchers *utils.WeakCacheMap[uuid.UUID, DynamicIPMatcher]
mu sync.Mutex
ipsetFactory *IPSetFactory
matchers []*DynamicIPMatcher
}
func (r *IPRegistry) BuildIPMatcher(rules []*IPRule) (IPMatcher, error) {
r.mu.Lock()
defer r.mu.Unlock()
m, err := buildOptimizedIPMatcher(r.factory, rules)
m, err := buildOptimizedIPMatcher(r.ipsetFactory, rules)
if err != nil {
return nil, err
}
d := NewDynamicIPMatcher(rules, m)
r.matchers.Store(uuid.New(), d)
r.matchers = append(r.matchers, d)
return d, nil
}
@@ -35,20 +33,15 @@ func (r *IPRegistry) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
var matchers []*DynamicIPMatcher
r.matchers.Range(func(_ uuid.UUID, matcher *DynamicIPMatcher) bool {
matchers = append(matchers, matcher)
return true
})
errors.LogInfo(context.Background(), "reloading GeoIP data for ", len(matchers), " IP matcher(s)")
errors.LogInfo(context.Background(), "reloading GeoIP data for ", len(r.matchers), " IP matcher(s)")
factory := newIPSetFactory()
type reloadEntry struct {
dynamic *DynamicIPMatcher
matcher IPMatcher
}
reloaded := make([]reloadEntry, len(matchers))
for i, d := range matchers {
reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range r.matchers {
m, err := buildOptimizedIPMatcher(factory, d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoIP data for IP matcher ", i)
@@ -59,15 +52,14 @@ func (r *IPRegistry) Reload() error {
for _, entry := range reloaded {
entry.dynamic.Reload(entry.matcher)
}
r.factory = factory
errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(matchers), " IP matcher(s)")
r.ipsetFactory = factory
errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(r.matchers), " IP matcher(s)")
return nil
}
func newIPRegistry() *IPRegistry {
return &IPRegistry{
factory: newIPSetFactory(),
matchers: utils.NewWeakCacheMap[uuid.UUID, DynamicIPMatcher](),
ipsetFactory: newIPSetFactory(),
}
}
+2 -2
View File
@@ -138,7 +138,7 @@ func ParseDomainRule(r string, defaultType Domain_Type) (*DomainRule, error) {
}
prefix := 0
for _, ext := range [...]string{"ext:", "ext-domain:", "ext-site:"} {
for _, ext := range [...]string{"ext:", "ext-domain:"} {
if strings.HasPrefix(r, ext) {
prefix = len(ext)
break
@@ -167,7 +167,7 @@ func ParseDomainRules(rules []string, defaultType Domain_Type) ([]*DomainRule, e
}
prefix := 0
for _, ext := range [...]string{"ext:", "ext-domain:", "ext-site:"} {
for _, ext := range [...]string{"ext:", "ext-domain:"} {
if strings.HasPrefix(r, ext) {
prefix = len(ext)
break
+25 -21
View File
@@ -1,7 +1,7 @@
package log // import "github.com/xtls/xray-core/common/log"
import (
"sync/atomic"
"sync"
"github.com/xtls/xray-core/common/serial"
)
@@ -29,32 +29,36 @@ func (m *GeneralMessage) String() string {
// Record writes a message into log stream.
func Record(msg Message) {
if h := logHandler.Load(); h != nil {
(*h).Handle(msg)
}
logHandler.Handle(msg)
}
type SeverityLogger interface {
Handler
Severity() Severity
}
func GetSeverity() Severity {
if h := logHandler.Load(); h != nil {
if sh, ok := (*h).(SeverityLogger); ok {
return sh.Severity()
}
}
// log everything by default
return Severity_Debug
}
var logHandler atomic.Pointer[Handler]
var logHandler syncHandler
// RegisterHandler registers a new handler as current log handler. Previous registered handler will be discarded.
func RegisterHandler(handler Handler) {
if handler == nil {
panic("Log handler is nil")
}
logHandler.Store(&handler)
logHandler.Set(handler)
}
type syncHandler struct {
sync.RWMutex
Handler
}
func (h *syncHandler) Handle(msg Message) {
h.RLock()
defer h.RUnlock()
if h.Handler != nil {
h.Handler.Handle(msg)
}
}
func (h *syncHandler) Set(handler Handler) {
h.Lock()
defer h.Unlock()
h.Handler = handler
}
-4
View File
@@ -68,10 +68,6 @@ func (l *serverityLogger) Handle(msg Message) {
}
}
func (l *serverityLogger) Severity() Severity {
return l.logLevel
}
func (l *generalLogger) run() {
defer l.access.Signal()
+1 -1
View File
@@ -38,7 +38,7 @@ func (m *ClientManager) Dispatch(ctx context.Context, link *transport.Link) erro
}
}
return errors.New("unable to find an available mux client")
return errors.New("unable to find an available mux client").AtWarning()
}
type WorkerPicker interface {
+1 -1
View File
@@ -117,7 +117,7 @@ func (f *FrameMetadata) Unmarshal(reader io.Reader, readSourceAndLocal bool) err
return err
}
if metaLen > 512 {
return errors.New("invalid metalen ", metaLen)
return errors.New("invalid metalen ", metaLen).AtError()
}
b := buf.New()
+1 -1
View File
@@ -351,7 +351,7 @@ func (w *ServerWorker) handleFrame(ctx context.Context, reader *buf.BufferedRead
err = w.handleStatusKeep(&meta, reader)
default:
status := meta.SessionStatus
return errors.New("unknown status: ", status)
return errors.New("unknown status: ", status).AtError()
}
if err != nil {
-350
View File
@@ -1,350 +0,0 @@
//go:build darwin && !ios
package net
import (
"bytes"
"net"
"net/netip"
"path/filepath"
"strings"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
"github.com/xtls/xray-core/common/errors"
)
const (
darwinProcPIDListFDs = 1
darwinProcPIDFDSocketInfo = 3
darwinProcFDTypeSocket = 2
darwinProcFDInfoSize = 8
darwinSocketFDInfoSize = 792
darwinSocketFDInfoPSIOff = 24
darwinSocketInfoProtoOff = darwinSocketFDInfoPSIOff + 156
darwinSocketInfoFamilyOff = darwinSocketFDInfoPSIOff + 160
darwinSocketInfoKindOff = darwinSocketFDInfoPSIOff + 232
darwinSocketInfoInSockOff = darwinSocketFDInfoPSIOff + 240
darwinInSockInfoFPortOff = darwinSocketInfoInSockOff
darwinInSockInfoLPortOff = darwinSocketInfoInSockOff + 4
darwinInSockInfoVFlagOff = darwinSocketInfoInSockOff + 24
darwinInSockInfoFAddrOff = darwinSocketInfoInSockOff + 32
darwinInSockInfoLAddrOff = darwinSocketInfoInSockOff + 48
darwinInSockInfoSize = 80
darwinInSockInfoIPv4 = 0x1
darwinInSockInfoIPv6 = 0x2
darwinSockInfoIN = 1
darwinSockInfoTCP = 2
)
type darwinSocketMatchLevel int
const (
darwinSocketNoMatch darwinSocketMatchLevel = iota
darwinSocketPortMatch
darwinSocketRemoteMatch
darwinSocketLocalMatch
darwinSocketExactMatch
)
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (PID int, Name string, AbsolutePath string, err error) {
isLocal, err := IsLocal(net.ParseIP(srcIP))
if err != nil {
return 0, "", "", errors.New("failed to determine if address is local: ", err)
}
if !isLocal {
return 0, "", "", ErrNotLocal
}
if network != "tcp" && network != "udp" {
panic("Unsupported network type for process lookup.")
}
srcAddr, err := netip.ParseAddr(srcIP)
if err != nil {
return 0, "", "", errors.New("invalid source IP address: ", srcIP)
}
srcAddr = srcAddr.Unmap()
var dstAddr netip.Addr
hasDstAddr := false
if destIP != "" && destPort != 0 {
dstAddr, err = netip.ParseAddr(destIP)
if err != nil {
return 0, "", "", errors.New("invalid destination IP address: ", destIP)
}
dstAddr = dstAddr.Unmap()
hasDstAddr = true
}
processes, err := unix.SysctlKinfoProcSlice("kern.proc.all")
if err != nil {
return 0, "", "", errors.New("failed to list processes").Base(err)
}
var bestPID int32
bestLevel := darwinSocketNoMatch
ambiguousBest := false
for _, process := range processes {
pid := process.Proc.P_pid
if pid <= 0 {
continue
}
matchLevel, err := darwinProcessSocketMatchLevel(pid, network, srcAddr, srcPort, dstAddr, destPort, hasDstAddr)
if err != nil || matchLevel == darwinSocketNoMatch {
continue
}
if matchLevel == darwinSocketExactMatch {
bestPID = pid
bestLevel = matchLevel
ambiguousBest = false
break
}
if matchLevel > bestLevel {
bestPID = pid
bestLevel = matchLevel
ambiguousBest = false
continue
}
if matchLevel == bestLevel {
ambiguousBest = true
}
}
if bestLevel == darwinSocketNoMatch {
return 0, "", "", errors.New("process not found for ", network, " connection from ", srcIP, ":", srcPort, " to ", destIP, ":", destPort)
}
if ambiguousBest {
return 0, "", "", errors.New("ambiguous process match for ", network, " connection from ", srcIP, ":", srcPort, " to ", destIP, ":", destPort)
}
absPath, err := darwinProcessPath(bestPID)
if err != nil {
return 0, "", "", errors.New("could not get process path for PID ", bestPID, ": ", err)
}
absPath = filepath.ToSlash(absPath)
return int(bestPID), filepath.Base(absPath), absPath, nil
}
func darwinProcessSocketMatchLevel(pid int32, network string, srcAddr netip.Addr, srcPort uint16, dstAddr netip.Addr, dstPort uint16, hasDstAddr bool) (darwinSocketMatchLevel, error) {
fds, err := darwinProcessFDs(pid)
if err != nil {
return darwinSocketNoMatch, err
}
bestLevel := darwinSocketNoMatch
info := make([]byte, darwinSocketFDInfoSize)
for fd := 0; fd+darwinProcFDInfoSize <= len(fds); fd += darwinProcFDInfoSize {
fdNumber := int32(darwinReadNativeUint32(fds[fd : fd+4]))
fdType := darwinReadNativeUint32(fds[fd+4 : fd+8])
if fdType != darwinProcFDTypeSocket {
continue
}
n, err := darwinProcPIDFDInfo(pid, fdNumber, darwinProcPIDFDSocketInfo, info)
if err != nil || n < darwinSocketInfoInSockOff+darwinInSockInfoSize {
continue
}
level := darwinSocketInfoMatchLevel(info[:n], network, srcAddr, srcPort, dstAddr, dstPort, hasDstAddr)
if level == darwinSocketExactMatch {
return level, nil
}
if level > bestLevel {
bestLevel = level
}
}
return bestLevel, nil
}
func darwinProcessFDs(pid int32) ([]byte, error) {
n, err := darwinProcPIDInfo(pid, darwinProcPIDListFDs, 0, nil)
if err != nil {
return nil, err
}
if n <= 0 {
return nil, nil
}
buf := make([]byte, n)
n, err = darwinProcPIDInfo(pid, darwinProcPIDListFDs, 0, buf)
if err != nil {
return nil, err
}
return buf[:n], nil
}
func darwinSocketInfoMatchLevel(info []byte, network string, srcAddr netip.Addr, srcPort uint16, dstAddr netip.Addr, dstPort uint16, hasDstAddr bool) darwinSocketMatchLevel {
protocol := int(darwinReadNativeUint32(info[darwinSocketInfoProtoOff : darwinSocketInfoProtoOff+4]))
family := int(darwinReadNativeUint32(info[darwinSocketInfoFamilyOff : darwinSocketInfoFamilyOff+4]))
kind := int(darwinReadNativeUint32(info[darwinSocketInfoKindOff : darwinSocketInfoKindOff+4]))
switch network {
case "tcp":
if protocol != unix.IPPROTO_TCP || kind != darwinSockInfoTCP {
return darwinSocketNoMatch
}
case "udp":
if protocol != unix.IPPROTO_UDP || kind != darwinSockInfoIN {
return darwinSocketNoMatch
}
default:
return darwinSocketNoMatch
}
vflag := info[darwinInSockInfoVFlagOff]
if srcAddr.Is4() {
// Dual-stack sockets expose IPv4-mapped connections as AF_INET6
// while marking the endpoint as IPv4 in ini_vflag.
if (family != unix.AF_INET && family != unix.AF_INET6) || vflag&darwinInSockInfoIPv4 == 0 {
return darwinSocketNoMatch
}
} else {
if family != unix.AF_INET6 || vflag&darwinInSockInfoIPv6 == 0 {
return darwinSocketNoMatch
}
}
localPort := int32(darwinReadNativeUint32(info[darwinInSockInfoLPortOff : darwinInSockInfoLPortOff+4]))
if !darwinPortMatches(localPort, srcPort) {
return darwinSocketNoMatch
}
localAddrMatches := darwinAddrMatchesOrUnspecified(info[darwinInSockInfoLAddrOff:darwinInSockInfoLAddrOff+16], srcAddr)
foreignAddrRaw := info[darwinInSockInfoFAddrOff : darwinInSockInfoFAddrOff+16]
foreignPort := int32(darwinReadNativeUint32(info[darwinInSockInfoFPortOff : darwinInSockInfoFPortOff+4]))
if !hasDstAddr {
if localAddrMatches {
return darwinSocketExactMatch
}
return darwinSocketNoMatch
}
remoteMatches := darwinPortMatches(foreignPort, dstPort) && darwinAddrMatches(foreignAddrRaw, dstAddr)
if network == "udp" && darwinEndpointIsZero(foreignAddrRaw, foreignPort) && localAddrMatches {
return darwinSocketExactMatch
}
switch {
case localAddrMatches && remoteMatches:
return darwinSocketExactMatch
case localAddrMatches:
return darwinSocketLocalMatch
case remoteMatches:
return darwinSocketRemoteMatch
default:
return darwinSocketPortMatch
}
}
func darwinPortMatches(value int32, port uint16) bool {
raw := uint16(value)
return raw == port || darwinNtohs(raw) == port
}
func darwinNtohs(value uint16) uint16 {
return value<<8 | value>>8
}
func darwinAddrMatches(raw []byte, addr netip.Addr) bool {
if addr.Is4() {
ip := addr.As4()
return bytes.Equal(raw[12:16], ip[:])
}
ip := addr.As16()
return bytes.Equal(raw, ip[:])
}
func darwinAddrMatchesOrUnspecified(raw []byte, addr netip.Addr) bool {
if darwinAddrMatches(raw, addr) {
return true
}
if addr.Is4() {
return darwinBytesAreZero(raw[12:16])
}
return darwinBytesAreZero(raw)
}
func darwinEndpointIsZero(rawAddr []byte, port int32) bool {
return uint32(port) == 0 && darwinBytesAreZero(rawAddr)
}
func darwinBytesAreZero(raw []byte) bool {
for _, value := range raw {
if value != 0 {
return false
}
}
return true
}
func darwinReadNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}
func darwinProcessPath(pid int32) (string, error) {
buf := make([]byte, unix.PathMax)
n, err := darwinProcPIDPath(pid, buf)
if err != nil {
return "", err
}
if n <= 0 {
return "", errors.New("empty process path")
}
return strings.TrimRight(string(buf[:n]), "\x00"), nil
}
func darwinProcPIDInfo(pid int32, flavor int, arg uint64, buf []byte) (int, error) {
var ptr unsafe.Pointer
if len(buf) > 0 {
ptr = unsafe.Pointer(&buf[0])
}
r0, _, errno := syscall_syscall6(libc_proc_pidinfo_trampoline_addr, uintptr(pid), uintptr(flavor), uintptr(arg), uintptr(ptr), uintptr(len(buf)), 0)
if errno != 0 {
return 0, errno
}
return int(r0), nil
}
func darwinProcPIDFDInfo(pid int32, fd int32, flavor int, buf []byte) (int, error) {
var ptr unsafe.Pointer
if len(buf) > 0 {
ptr = unsafe.Pointer(&buf[0])
}
r0, _, errno := syscall_syscall6(libc_proc_pidfdinfo_trampoline_addr, uintptr(pid), uintptr(fd), uintptr(flavor), uintptr(ptr), uintptr(len(buf)), 0)
if errno != 0 {
return 0, errno
}
return int(r0), nil
}
func darwinProcPIDPath(pid int32, buf []byte) (int, error) {
r0, _, errno := syscall_syscall6(libc_proc_pidpath_trampoline_addr, uintptr(pid), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0, 0, 0)
if errno != 0 {
return 0, errno
}
return int(r0), nil
}
var libc_proc_pidinfo_trampoline_addr uintptr
//go:cgo_import_dynamic libc_proc_pidinfo proc_pidinfo "/usr/lib/libproc.dylib"
var libc_proc_pidfdinfo_trampoline_addr uintptr
//go:cgo_import_dynamic libc_proc_pidfdinfo proc_pidfdinfo "/usr/lib/libproc.dylib"
var libc_proc_pidpath_trampoline_addr uintptr
//go:cgo_import_dynamic libc_proc_pidpath proc_pidpath "/usr/lib/libproc.dylib"
// Implemented in the runtime package (runtime/sys_darwin.go).
func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
//go:linkname syscall_syscall6 syscall.syscall6
-18
View File
@@ -1,18 +0,0 @@
//go:build darwin && !ios
#include "textflag.h"
TEXT libc_proc_pidinfo_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_proc_pidinfo(SB)
GLOBL ·libc_proc_pidinfo_trampoline_addr(SB), RODATA, $8
DATA ·libc_proc_pidinfo_trampoline_addr(SB)/8, $libc_proc_pidinfo_trampoline<>(SB)
TEXT libc_proc_pidfdinfo_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_proc_pidfdinfo(SB)
GLOBL ·libc_proc_pidfdinfo_trampoline_addr(SB), RODATA, $8
DATA ·libc_proc_pidfdinfo_trampoline_addr(SB)/8, $libc_proc_pidfdinfo_trampoline<>(SB)
TEXT libc_proc_pidpath_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_proc_pidpath(SB)
GLOBL ·libc_proc_pidpath_trampoline_addr(SB), RODATA, $8
DATA ·libc_proc_pidpath_trampoline_addr(SB)/8, $libc_proc_pidpath_trampoline<>(SB)
-356
View File
@@ -1,356 +0,0 @@
//go:build darwin && !ios
package net
import (
stdnet "net"
"net/netip"
"os"
"testing"
"time"
"unsafe"
"golang.org/x/sys/unix"
)
func TestFindProcessDarwinTCP(t *testing.T) {
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
conn, err := stdnet.Dial("tcp", listener.Addr().String())
if err != nil {
t.Fatal(err)
}
defer conn.Close()
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local := conn.LocalAddr().(*stdnet.TCPAddr)
remote := conn.RemoteAddr().(*stdnet.TCPAddr)
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), remote.IP.String(), uint16(remote.Port))
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinTCPIPv4Mapped(t *testing.T) {
listener, err := stdnet.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
listenerAddr := listener.Addr().(*stdnet.TCPAddr)
fd, err := unix.Socket(unix.AF_INET6, unix.SOCK_STREAM, unix.IPPROTO_TCP)
if err != nil {
t.Fatal(err)
}
defer unix.Close(fd)
mappedAddr := [16]byte{10: 0xff, 11: 0xff, 12: 127, 15: 1}
if err := unix.Connect(fd, &unix.SockaddrInet6{
Port: listenerAddr.Port,
Addr: mappedAddr,
}); err != nil {
t.Fatal(err)
}
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local, err := unix.Getsockname(fd)
if err != nil {
t.Fatal(err)
}
localPort := local.(*unix.SockaddrInet6).Port
pid, name, path, err := FindProcess("tcp", "127.0.0.1", uint16(localPort), "127.0.0.1", uint16(listenerAddr.Port))
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinUDP(t *testing.T) {
conn, err := stdnet.ListenUDP("udp", &stdnet.UDPAddr{IP: stdnet.ParseIP("127.0.0.1")})
if err != nil {
t.Fatal(err)
}
defer conn.Close()
local := conn.LocalAddr().(*stdnet.UDPAddr)
pid, name, path, err := FindProcess("udp", local.IP.String(), uint16(local.Port), "", 0)
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinNonLocal(t *testing.T) {
_, _, _, err := FindProcess("tcp", "203.0.113.1", 80, "", 0)
if err != ErrNotLocal {
t.Fatalf("expected ErrNotLocal, got %v", err)
}
}
func TestFindProcessDarwinUnsupportedNetwork(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("expected panic")
}
}()
_, _, _, _ = FindProcess("icmp", "127.0.0.1", 0, "", 0)
}
func assertCurrentProcess(t *testing.T, pid int, name string, path string) {
t.Helper()
if pid != os.Getpid() {
t.Fatalf("expected pid %d, got %d (%s, %s)", os.Getpid(), pid, name, path)
}
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
if path == "" || name == "" {
t.Fatalf("expected process path and name, got name=%q path=%q", name, path)
}
if sameFile(executable, path) {
return
}
t.Fatalf("expected executable %q, got %q", executable, path)
}
func sameFile(left string, right string) bool {
leftInfo, leftErr := os.Stat(left)
rightInfo, rightErr := os.Stat(right)
if leftErr != nil || rightErr != nil {
return false
}
return os.SameFile(leftInfo, rightInfo)
}
func TestFindProcessDarwinTCPWithoutDestination(t *testing.T) {
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
conn, err := stdnet.DialTimeout("tcp", listener.Addr().String(), time.Second)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local := conn.LocalAddr().(*stdnet.TCPAddr)
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), "", 0)
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinTCPWithDifferentDestination(t *testing.T) {
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
conn, err := stdnet.DialTimeout("tcp", listener.Addr().String(), time.Second)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local := conn.LocalAddr().(*stdnet.TCPAddr)
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), "203.0.113.10", 443)
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestDarwinSocketInfoMatchLevelFallbacks(t *testing.T) {
src := netip.MustParseAddr("198.18.0.2")
dst := netip.MustParseAddr("203.0.113.10")
otherLocal := netip.MustParseAddr("192.168.1.10")
otherRemote := netip.MustParseAddr("198.51.100.10")
unspecifiedLocal := netip.MustParseAddr("0.0.0.0")
tests := []struct {
name string
local netip.Addr
remote netip.Addr
hasDst bool
wantLevel darwinSocketMatchLevel
}{
{
name: "exact",
local: src,
remote: dst,
hasDst: true,
wantLevel: darwinSocketExactMatch,
},
{
name: "unspecified local with matching remote",
local: unspecifiedLocal,
remote: dst,
hasDst: true,
wantLevel: darwinSocketExactMatch,
},
{
name: "unspecified local without destination",
local: unspecifiedLocal,
remote: otherRemote,
hasDst: false,
wantLevel: darwinSocketExactMatch,
},
{
name: "local match with different remote",
local: src,
remote: otherRemote,
hasDst: true,
wantLevel: darwinSocketLocalMatch,
},
{
name: "remote match with different local",
local: otherLocal,
remote: dst,
hasDst: true,
wantLevel: darwinSocketRemoteMatch,
},
{
name: "port only with destination",
local: otherLocal,
remote: otherRemote,
hasDst: true,
wantLevel: darwinSocketPortMatch,
},
{
name: "different local without destination",
local: otherLocal,
remote: otherRemote,
hasDst: false,
wantLevel: darwinSocketNoMatch,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
info := newDarwinSocketInfo("tcp", test.local, 12345, test.remote, 443)
level := darwinSocketInfoMatchLevel(info, "tcp", src, 12345, dst, 443, test.hasDst)
if level != test.wantLevel {
t.Fatalf("unexpected match level: got %d, want %d", level, test.wantLevel)
}
})
}
}
func TestDarwinSocketInfoMatchLevelIPv4Mapped(t *testing.T) {
src := netip.MustParseAddr("127.0.0.1")
dst := netip.MustParseAddr("203.0.113.10")
info := newDarwinSocketInfo("tcp", src, 12345, dst, 443)
writeDarwinNativeUint32(info, darwinSocketInfoFamilyOff, uint32(unix.AF_INET6))
level := darwinSocketInfoMatchLevel(info, "tcp", src, 12345, dst, 443, true)
if level != darwinSocketExactMatch {
t.Fatalf("unexpected match level: got %d, want %d", level, darwinSocketExactMatch)
}
}
func newDarwinSocketInfo(network string, local netip.Addr, localPort uint16, remote netip.Addr, remotePort uint16) []byte {
info := make([]byte, darwinSocketFDInfoSize)
switch network {
case "tcp":
writeDarwinNativeUint32(info, darwinSocketInfoProtoOff, uint32(unix.IPPROTO_TCP))
writeDarwinNativeUint32(info, darwinSocketInfoKindOff, uint32(darwinSockInfoTCP))
case "udp":
writeDarwinNativeUint32(info, darwinSocketInfoProtoOff, uint32(unix.IPPROTO_UDP))
writeDarwinNativeUint32(info, darwinSocketInfoKindOff, uint32(darwinSockInfoIN))
}
writeDarwinNativeUint32(info, darwinSocketInfoFamilyOff, uint32(unix.AF_INET))
info[darwinInSockInfoVFlagOff] = darwinInSockInfoIPv4
writeDarwinNativeUint32(info, darwinInSockInfoLPortOff, uint32(localPort))
writeDarwinNativeUint32(info, darwinInSockInfoFPortOff, uint32(remotePort))
copyDarwinIPv4(info[darwinInSockInfoLAddrOff:darwinInSockInfoLAddrOff+16], local)
copyDarwinIPv4(info[darwinInSockInfoFAddrOff:darwinInSockInfoFAddrOff+16], remote)
return info
}
func writeDarwinNativeUint32(b []byte, offset int, value uint32) {
*(*uint32)(unsafe.Pointer(&b[offset])) = value
}
func copyDarwinIPv4(dst []byte, addr netip.Addr) {
ip := addr.As4()
copy(dst[12:16], ip[:])
}
-11
View File
@@ -1,11 +0,0 @@
//go:build ios
package net
import (
"github.com/xtls/xray-core/common/errors"
)
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (int, string, string, error) {
return 0, "", "", errors.New("process lookup is not supported on this platform")
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !windows && !linux && !android && !darwin
//go:build !windows && !linux && !android
package net
-40
View File
@@ -1,40 +0,0 @@
package platform
import (
"errors"
"sync"
)
var envReloadRegistry = struct {
sync.RWMutex
handlers []func() error
}{}
// RegisterEnvReload registers an environment reload handler and runs it once
// immediately so package defaults keep the same behavior as init-time reads.
func RegisterEnvReload(handler func() error) {
if handler == nil {
return
}
envReloadRegistry.Lock()
envReloadRegistry.handlers = append(envReloadRegistry.handlers, handler)
envReloadRegistry.Unlock()
if err := handler(); err != nil {
panic(err)
}
}
// ReloadEnvSettings refreshes all registered environment-backed package state.
func ReloadEnvSettings() error {
envReloadRegistry.RLock()
handlers := append([]func() error{}, envReloadRegistry.handlers...)
envReloadRegistry.RUnlock()
var errs []error
for _, handler := range handlers {
if err := handler(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
+34 -25
View File
@@ -3,8 +3,11 @@ package bittorrent
import (
"encoding/binary"
"errors"
"math"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
)
type SniffHeader struct{}
@@ -36,44 +39,50 @@ func SniffUTP(b []byte) (*SniffHeader, error) {
return nil, common.ErrNoClue
}
// type 4 (ST_SYN), version 1
if b[0] != 0x41 {
buffer := buf.FromBytes(b)
var typeAndVersion uint8
if binary.Read(buffer, binary.BigEndian, &typeAndVersion) != nil {
return nil, common.ErrNoClue
} else if b[0]>>4&0xF > 4 || b[0]&0xF != 1 {
return nil, errNotBittorrent
}
// timestamp_difference is always 0 in new connections
if binary.BigEndian.Uint32(b[8:12]) != 0 {
var extension uint8
if binary.Read(buffer, binary.BigEndian, &extension) != nil {
return nil, common.ErrNoClue
} else if extension != 0 && extension != 1 {
return nil, errNotBittorrent
}
// Walk the extension chain. Selective ack (1) and extension bits (2)
extension, offset := b[1], 20
for extension != 0 {
if len(b) < offset+2 {
if extension != 1 {
return nil, errNotBittorrent
}
length := int(b[offset+1])
switch extension {
case 1: // selective ack
if length < 4 || length%4 != 0 {
return nil, errNotBittorrent
}
case 2: // extension bits: fixed 8 bytes, sent in ST_SYN by µTorrent
if length != 8 {
return nil, errNotBittorrent
}
default:
return nil, errNotBittorrent
if binary.Read(buffer, binary.BigEndian, &extension) != nil {
return nil, common.ErrNoClue
}
if len(b) < offset+2+length {
return nil, errNotBittorrent
var length uint8
if err := binary.Read(buffer, binary.BigEndian, &length); err != nil {
return nil, common.ErrNoClue
}
if common.Error2(buffer.ReadBytes(int32(length))) != nil {
return nil, common.ErrNoClue
}
extension = b[offset]
offset += 2 + length
}
// extensions should consume all ST_SYN payload
if len(b) != offset {
if common.Error2(buffer.ReadBytes(2)) != nil {
return nil, common.ErrNoClue
}
var timestamp uint32
if err := binary.Read(buffer, binary.BigEndian, &timestamp); err != nil {
return nil, common.ErrNoClue
}
if math.Abs(float64(time.Now().UnixMicro()-int64(timestamp))) > float64(24*time.Hour) {
return nil, errNotBittorrent
}
@@ -1,67 +0,0 @@
package bittorrent
import (
"encoding/binary"
"testing"
"github.com/xtls/xray-core/common"
)
// utpPacket builds the fixed 20-byte header defined by BEP 29.
func utpPacket(packetType, extension byte, tsDiff uint32, payload ...byte) []byte {
b := make([]byte, 20)
b[0] = packetType<<4 | 1
b[1] = extension
binary.BigEndian.PutUint16(b[2:4], 0x4a3f) // connection_id, random
binary.BigEndian.PutUint32(b[4:8], 0x8c3a91d2) // timestamp_microseconds, sender's clock
binary.BigEndian.PutUint32(b[8:12], tsDiff)
binary.BigEndian.PutUint32(b[12:16], 0x00100000) // wnd_size
binary.BigEndian.PutUint16(b[16:18], 0x71ee) // seq_nr, random in libutp/libtorrent
binary.BigEndian.PutUint16(b[18:20], 0x0000) // ack_nr
return append(b, payload...)
}
func TestSniffUTP(t *testing.T) {
selectiveAck := []byte{0, 4, 0xff, 0x00, 0xff, 0x00}
wrongVersion := utpPacket(4, 0, 0)
wrongVersion[0] = 4<<4 | 2
cases := []struct {
name string
payload []byte
err error
}{
{"syn", utpPacket(4, 0, 0), nil},
{"syn with selective ack", append(utpPacket(4, 1, 0), selectiveAck...), nil},
{"syn with extension bits", append(utpPacket(4, 2, 0), 0, 8, 1, 2, 3, 4, 5, 6, 7, 8), nil},
{"extension bits with wrong length", append(utpPacket(4, 2, 0), 0, 4, 1, 2, 3, 4), errNotBittorrent},
{"syn with nonzero timestamp_difference", utpPacket(4, 0, 0x1234), errNotBittorrent},
{"syn with trailing payload", utpPacket(4, 0, 0, 'x'), errNotBittorrent},
// txid 0x4100, no EDNS0: the worst case colliding with the uTP header
{"dns query", []byte{
0x41, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 'a', 0x02, 'c', 'o', 0x00, 0x00, 0x01, 0x00, 0x01,
}, errNotBittorrent},
{"established connection packets", utpPacket(0, 0, 0x5678, 'x', 'y', 'z'), errNotBittorrent},
{"state", utpPacket(2, 0, 0x5678), errNotBittorrent},
{"fin", utpPacket(1, 0, 0x5678), errNotBittorrent},
{"wrong version", wrongVersion, errNotBittorrent},
{"unknown packet type", utpPacket(5, 0, 0), errNotBittorrent},
{"unknown extension", utpPacket(4, 2, 0), errNotBittorrent},
{"extension chain past the datagram", utpPacket(4, 1, 0, 0, 8, 0xff), errNotBittorrent},
{"selective ack not in multiples of 4", append(utpPacket(4, 1, 0), 0, 3, 0xff, 0x00, 0xff), errNotBittorrent},
{"shorter than the header", utpPacket(4, 0, 0)[:19], common.ErrNoClue},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
h, err := SniffUTP(c.payload)
if err != c.err {
t.Fatalf("expected error %v, got %v", c.err, err)
}
if err == nil && h == nil {
t.Fatal("expected a sniff header, got nil")
}
})
}
}
+10 -2
View File
@@ -28,6 +28,8 @@ const (
SecurityType_AUTO SecurityType = 2
SecurityType_AES128_GCM SecurityType = 3
SecurityType_CHACHA20_POLY1305 SecurityType = 4
SecurityType_NONE SecurityType = 5 // [DEPRECATED 2023-06]
SecurityType_ZERO SecurityType = 6
)
// Enum value maps for SecurityType.
@@ -37,12 +39,16 @@ var (
2: "AUTO",
3: "AES128_GCM",
4: "CHACHA20_POLY1305",
5: "NONE",
6: "ZERO",
}
SecurityType_value = map[string]int32{
"UNKNOWN": 0,
"AUTO": 2,
"AES128_GCM": 3,
"CHACHA20_POLY1305": 4,
"NONE": 5,
"ZERO": 6,
}
)
@@ -123,13 +129,15 @@ const file_common_protocol_headers_proto_rawDesc = "" +
"\n" +
"\x1dcommon/protocol/headers.proto\x12\x14xray.common.protocol\"H\n" +
"\x0eSecurityConfig\x126\n" +
"\x04type\x18\x01 \x01(\x0e2\".xray.common.protocol.SecurityTypeR\x04type*L\n" +
"\x04type\x18\x01 \x01(\x0e2\".xray.common.protocol.SecurityTypeR\x04type*`\n" +
"\fSecurityType\x12\v\n" +
"\aUNKNOWN\x10\x00\x12\b\n" +
"\x04AUTO\x10\x02\x12\x0e\n" +
"\n" +
"AES128_GCM\x10\x03\x12\x15\n" +
"\x11CHACHA20_POLY1305\x10\x04B^\n" +
"\x11CHACHA20_POLY1305\x10\x04\x12\b\n" +
"\x04NONE\x10\x05\x12\b\n" +
"\x04ZERO\x10\x06B^\n" +
"\x18com.xray.common.protocolP\x01Z)github.com/xtls/xray-core/common/protocol\xaa\x02\x14Xray.Common.Protocolb\x06proto3"
var (
+2
View File
@@ -11,6 +11,8 @@ enum SecurityType {
AUTO = 2;
AES128_GCM = 3;
CHACHA20_POLY1305 = 4;
NONE = 5; // [DEPRECATED 2023-06]
ZERO = 6;
}
message SecurityConfig {
+10 -26
View File
@@ -1,41 +1,25 @@
package http
import (
"context"
"net/http"
"strconv"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
)
// ApplyTrustedXForwardedFor returns remoteAddr overridden by X-Forwarded-For only when a configured trusted header is present.
func ApplyTrustedXForwardedFor(header http.Header, trusted []string, remoteAddr net.Addr) net.Addr {
value := header.Get("X-Forwarded-For")
if value == "" {
return remoteAddr
// ParseXForwardedFor parses X-Forwarded-For header in http headers, and return the IP list in it.
func ParseXForwardedFor(header http.Header) []net.Address {
xff := header.Get("X-Forwarded-For")
if xff == "" {
return nil
}
for _, t := range trusted {
if len(header.Values(t)) > 0 {
if idx := strings.IndexByte(value, ','); idx >= 0 {
value = value[:idx]
}
if addr := net.ParseAddress(value); addr.Family().IsIP() {
return &net.TCPAddr{
IP: addr.IP(),
Port: 0,
}
}
return remoteAddr
}
list := strings.Split(xff, ",")
addrs := make([]net.Address, 0, len(list))
for _, proxy := range list {
addrs = append(addrs, net.ParseAddress(proxy))
}
if len(trusted) == 0 {
errors.LogWarning(context.Background(), `received "X-Forwarded-For" from `, remoteAddr, ` but "sockopt.trustedXForwardedFor" is not configured; ignoring it and using the real remote address`)
} else {
errors.LogError(context.Background(), `ignored potentially forged "X-Forwarded-For" from `, remoteAddr, `: `, value)
}
return remoteAddr
return addrs
}
// RemoveHopByHopHeaders removes hop by hop headers in http header list.
+8 -33
View File
@@ -2,48 +2,23 @@ package http_test
import (
"bufio"
gonet "net"
"net/http"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/net"
. "github.com/xtls/xray-core/common/protocol/http"
)
func TestApplyTrustedXForwardedFor(t *testing.T) {
remoteAddr := &gonet.TCPAddr{IP: gonet.ParseIP("127.0.0.1"), Port: 12345}
t.Run("ignore X-Forwarded-For without trusted header", func(t *testing.T) {
header := http.Header{}
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103")
if addr := ApplyTrustedXForwardedFor(header, nil, remoteAddr); addr != remoteAddr {
t.Fatalf("unexpected remote address: %v", addr)
}
})
t.Run("trust X-Forwarded-For", func(t *testing.T) {
header := http.Header{}
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103")
header.Add("X-Trusted-CDN", "")
addr := ApplyTrustedXForwardedFor(header, []string{"X-Trusted-CDN"}, remoteAddr)
if addr.String() != "129.78.138.66:0" {
t.Fatalf("unexpected remote address: %v", addr)
}
})
t.Run("ignore non-IP X-Forwarded-For", func(t *testing.T) {
header := http.Header{}
header.Add("X-Forwarded-For", "example.com")
header.Add("X-Trusted-CDN", "")
if addr := ApplyTrustedXForwardedFor(header, []string{"X-Trusted-CDN"}, remoteAddr); addr != remoteAddr {
t.Fatalf("unexpected remote address: %v", addr)
}
})
func TestParseXForwardedFor(t *testing.T) {
header := http.Header{}
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103")
addrs := ParseXForwardedFor(header)
if r := cmp.Diff(addrs, []net.Address{net.ParseAddress("129.78.138.66"), net.ParseAddress("129.78.64.103")}); r != "" {
t.Error(r)
}
}
func TestHopByHopHeadersRemoving(t *testing.T) {
+8
View File
@@ -1,10 +1,18 @@
package quic
import (
"crypto"
"crypto/cipher"
_ "crypto/tls"
_ "unsafe"
)
type CipherSuiteTLS13 struct {
ID uint16
KeyLen int
AEAD func(key, fixedNonce []byte) cipher.AEAD
Hash crypto.Hash
}
//go:linkname AEADAESGCMTLS13 crypto/tls.aeadAESGCMTLS13
func AEADAESGCMTLS13(key, nonceMask []byte) cipher.AEAD
+81 -98
View File
@@ -3,6 +3,7 @@ package quic
import (
"crypto"
"crypto/aes"
"crypto/tls"
"encoding/binary"
"io"
@@ -27,43 +28,22 @@ func (s SniffHeader) Domain() string {
return s.domain
}
var (
errNotQUIC = errors.New("not quic")
errNotQUICInitial = errors.New("not initial packet")
const (
versionDraft29 uint32 = 0xff00001d
version1 uint32 = 0x1
)
type quicVersionSpec struct {
ver uint32
typeInitial byte
initialSalt []byte
labelPrefix string
}
var (
quicDraft29 = quicVersionSpec{
ver: 0xff00001d,
typeInitial: 0b00,
initialSalt: []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99},
labelPrefix: "quic",
}
quicV1 = quicVersionSpec{
ver: 0x1,
typeInitial: 0b00,
initialSalt: []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a},
labelPrefix: "quic",
}
quicV2 = quicVersionSpec{
ver: 0x6b3343cf,
typeInitial: 0b01,
initialSalt: []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9},
labelPrefix: "quicv2",
}
quicVersionSpecMap = map[uint32]*quicVersionSpec{
quicDraft29.ver: &quicDraft29,
quicV1.ver: &quicV1,
quicV2.ver: &quicV2,
quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}
quicSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
initialSuite = &CipherSuiteTLS13{
ID: tls.TLS_AES_128_GCM_SHA256,
KeyLen: 16,
AEAD: AEADAESGCMTLS13,
Hash: crypto.SHA256,
}
errNotQuic = errors.New("not quic")
errNotQuicInitial = errors.New("not initial packet")
)
func SniffQUIC(b []byte) (*SniffHeader, error) {
@@ -83,61 +63,60 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
buffer := buf.FromBytes(b)
typeByte, err := buffer.ReadByte()
if err != nil {
return nil, errNotQUIC
return nil, errNotQuic
}
isLongHeader := typeByte&0x80 > 0
if !isLongHeader || typeByte&0x40 == 0 {
return nil, errNotQUICInitial
return nil, errNotQuicInitial
}
vb, err := buffer.ReadBytes(4)
if err != nil {
return nil, errNotQUIC
return nil, errNotQuic
}
versionNumber := binary.BigEndian.Uint32(vb)
var s *quicVersionSpec
if v, ok := quicVersionSpecMap[versionNumber]; ok {
s = v
} else {
return nil, errNotQUIC
}
var destConnID []byte
if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQUIC
} else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil {
return nil, errNotQUIC
}
if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQUIC
} else if common.Error2(buffer.ReadBytes(int32(l))) != nil {
return nil, errNotQUIC
if versionNumber != 0 && typeByte&0x40 == 0 {
return nil, errNotQuic
} else if versionNumber != versionDraft29 && versionNumber != version1 {
return nil, errNotQuic
}
packetType := (typeByte & 0x30) >> 4
isQUICInitial := packetType == s.typeInitial
isQuicInitial := packetType == 0x0
if isQUICInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2
tokenLen, err := readShortQUICVarint(buffer)
var destConnID []byte
if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQuic
} else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil {
return nil, errNotQuic
}
if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQuic
} else if common.Error2(buffer.ReadBytes(int32(l))) != nil {
return nil, errNotQuic
}
if isQuicInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2
tokenLen, err := readShortQuicVarint(buffer)
if err != nil || tokenLen > int32(len(b)) {
return nil, errNotQUIC
return nil, errNotQuic
}
if _, err = buffer.ReadBytes(tokenLen); err != nil {
return nil, errNotQUIC
return nil, errNotQuic
}
}
packetLen, err := readShortQUICVarint(buffer)
packetLen, err := readShortQuicVarint(buffer)
if err != nil {
return nil, errNotQUIC
return nil, errNotQuic
}
// packetLen is impossible to be shorter than this
if packetLen < 4 {
return nil, errNotQUIC
return nil, errNotQuic
}
hdrLen := len(b) - int(buffer.Len())
@@ -146,23 +125,25 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
}
restPayload := b[hdrLen+int(packetLen):]
if !isQUICInitial { // Skip this packet if it's not initial packet
if !isQuicInitial { // Skip this packet if it's not initial packet
b = restPayload
continue
}
salt := s.initialSalt
label := s.labelPrefix
var salt []byte
if versionNumber == version1 {
salt = quicSalt
} else {
salt = quicSaltOld
}
initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt)
secret := hkdfExpandLabel(initialSecret, "client in", crypto.SHA256.Size())
hpKey := hkdfExpandLabel(secret, label+" hp", 16)
secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size())
hpKey := hkdfExpandLabel(initialSuite.Hash, secret, []byte{}, "quic hp", initialSuite.KeyLen)
block, err := aes.NewCipher(hpKey)
if err != nil {
return nil, err
}
if len(b) < hdrLen+4+block.BlockSize() {
return nil, errNotQUIC
}
cache.Clear()
mask := cache.Extend(int32(block.BlockSize()))
block.Encrypt(mask, b[hdrLen+4:hdrLen+4+len(mask)])
@@ -172,8 +153,8 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
b[hdrLen+i] ^= mask[i+1]
}
key := hkdfExpandLabel(secret, label+" key", 16)
iv := hkdfExpandLabel(secret, label+" iv", 12)
key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16)
iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12)
cipher := AEADAESGCMTLS13(key, iv)
nonce := cache.Extend(int32(cipher.NonceSize()))
@@ -198,44 +179,44 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
case 0x00: // PADDING frame
case 0x01: // PING frame
case 0x02, 0x03: // ACK frame
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Largest Acknowledged
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Largest Acknowledged
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Delay
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Delay
return nil, io.ErrUnexpectedEOF
}
ackRangeCount, err := readShortQUICVarint(buffer) // Field: ACK Range Count
ackRangeCount, err := readShortQuicVarint(buffer) // Field: ACK Range Count
if err != nil {
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQUICVarint(buffer); err != nil { // Field: First ACK Range
if _, err = readShortQuicVarint(buffer); err != nil { // Field: First ACK Range
return nil, io.ErrUnexpectedEOF
}
for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Range -> Gap
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> Gap
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length
return nil, io.ErrUnexpectedEOF
}
}
if frameType == 0x03 {
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQUICVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
if _, err = readShortQuicVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
return nil, io.ErrUnexpectedEOF
}
}
case 0x06: // CRYPTO frame, we will use this frame
offset, err := readShortQUICVarint(buffer) // Field: Offset
offset, err := readShortQuicVarint(buffer) // Field: Offset
if err != nil {
return nil, io.ErrUnexpectedEOF
}
length, err := readShortQUICVarint(buffer) // Field: Length
length, err := readShortQuicVarint(buffer) // Field: Length
if err != nil || length > buffer.Len() {
return nil, io.ErrUnexpectedEOF
}
@@ -251,13 +232,13 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, io.ErrUnexpectedEOF
}
case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Error Code
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Error Code
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Frame Type
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Frame Type
return nil, io.ErrUnexpectedEOF
}
length, err := readShortQUICVarint(buffer) // Field: Reason Phrase Length
length, err := readShortQuicVarint(buffer) // Field: Reason Phrase Length
if err != nil {
return nil, io.ErrUnexpectedEOF
}
@@ -267,7 +248,7 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
default:
// Only above frame types are permitted in initial packet.
// See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8
return nil, errNotQUICInitial
return nil, errNotQuicInitial
}
}
@@ -285,33 +266,35 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, protocol.ErrProtoNeedMoreData
}
func hkdfExpandLabel(secret []byte, label string, length int) []byte {
b := make([]byte, 0, 2+1+6+len(label)+1)
b = binary.BigEndian.AppendUint16(b, uint16(length))
b = append(b, byte(6+len(label)))
b = append(b, "tls13 "...)
b = append(b, label...)
b = append(b, 0) // context
func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte {
b := make([]byte, 3, 3+6+len(label)+1+len(context))
binary.BigEndian.PutUint16(b, uint16(length))
b[2] = uint8(6 + len(label))
b = append(b, []byte("tls13 ")...)
b = append(b, []byte(label)...)
b = b[:3+6+len(label)+1]
b[3+6+len(label)] = uint8(len(context))
b = append(b, context...)
out := make([]byte, length)
n, err := hkdf.Expand(crypto.SHA256.New, secret, b).Read(out)
n, err := hkdf.Expand(hash.New, secret, b).Read(out)
if err != nil || n != length {
panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
}
return out
}
// readShortQUICVarint wraps quicvarint.Read with a max limit for length related fields.
// readShortQuicVarint wraps quicvarint.Read with a max limit for length related fields.
// we only handle QUIC Initial so these numbers should not exceed 65535
// returns int32 to reduce type conversion
func readShortQUICVarint(reader io.ByteReader) (int32, error) {
func readShortQuicVarint(reader io.ByteReader) (int32, error) {
v, err := quicvarint.Read(reader)
if err != nil {
return 0, err
}
if v > 65535 {
// not used(
return 0, errNotQUICInitial
return 0, errNotQuicInitial
}
return int32(v), nil
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@ import (
func (u *User) GetTypedAccount() (Account, error) {
if u.GetAccount() == nil {
return nil, errors.New("Account is missing")
return nil, errors.New("Account is missing").AtWarning()
}
rawAccount, err := u.Account.GetInstance()
-1
View File
@@ -207,7 +207,6 @@ func getConfig() string {
"tag": "XHTTP_IN",
"streamSettings": {
"network": "xhttp",
"security": "tls",
"xhttpSettings": {
"host": "bing.com",
"path": "/xhttp_client_upload",
-10
View File
@@ -26,8 +26,6 @@ const (
fullHandlerKey ctx.SessionKey = 10 // outbound gets full handler
mitmAlpn11Key ctx.SessionKey = 11 // used by TLS dialer
mitmServerNameKey ctx.SessionKey = 12 // used by TLS dialer
streamSettingsKey ctx.SessionKey = 13
)
func ContextWithInbound(ctx context.Context, inbound *Inbound) context.Context {
@@ -194,11 +192,3 @@ func MitmServerNameFromContext(ctx context.Context) string {
}
return ""
}
func ContextWithStreamSettings(ctx context.Context, streamSettings any) context.Context {
return context.WithValue(ctx, streamSettingsKey, streamSettings)
}
func StreamSettingsFromContext(ctx context.Context) any {
return ctx.Value(streamSettingsKey)
}
+2
View File
@@ -70,6 +70,8 @@ type Outbound struct {
Tag string
// Name of the outbound proxy that handles the connection.
Name string
// Unused. Conn is actually internet.Connection. May be nil. It is currently nil for outbound with proxySettings
Conn net.Conn
// CanSpliceCopy is a property for this connection
// 1 = can, 2 = after processing protocol info should be able to, 3 = cannot
CanSpliceCopy int
+2 -2
View File
@@ -16,7 +16,7 @@ var typeCreatorRegistry = make(map[reflect.Type]ConfigCreator)
func RegisterConfig(config interface{}, configCreator ConfigCreator) error {
configType := reflect.TypeOf(config)
if _, found := typeCreatorRegistry[configType]; found {
return errors.New(configType.Name() + " is already registered")
return errors.New(configType.Name() + " is already registered").AtError()
}
typeCreatorRegistry[configType] = configCreator
return nil
@@ -27,7 +27,7 @@ func CreateObject(ctx context.Context, config interface{}) (interface{}, error)
configType := reflect.TypeOf(config)
creator, found := typeCreatorRegistry[configType]
if !found {
return nil, errors.New(configType.String() + " is not registered")
return nil, errors.New(configType.String() + " is not registered").AtError()
}
return creator(ctx, config)
}
-14
View File
@@ -1,7 +1,6 @@
package utils
import (
"maps"
"runtime"
"sync"
"weak"
@@ -44,16 +43,3 @@ func (c *WeakCacheMap[K, V]) Store(key K, value *V) {
}
}, struct{}{})
}
func (c *WeakCacheMap[K, V]) Range(f func(K, *V) bool) {
c.mu.Lock()
snapshot := maps.Clone(c.m)
c.mu.Unlock()
for k, v := range snapshot {
if value := v.Value(); value != nil {
if !f(k, value) {
break
}
}
}
}
+19 -33
View File
@@ -8,7 +8,7 @@ import (
"io"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
@@ -27,39 +27,25 @@ var AddrParser = protocol.NewAddressParser(
)
var (
Show atomic.Bool
baseKey atomic.Value
Show bool
BaseKey []byte
)
func reloadEnvSettings() error {
Show.Store(strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true")
raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" })
if raw == "" {
ensureBaseKey()
return nil
}
key, _ := base64.RawURLEncoding.DecodeString(raw)
if len(key) != 32 {
return errors.New(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(key)))
}
baseKey.Store(append([]byte(nil), key...))
return nil
}
func ensureBaseKey() []byte {
if key := baseKey.Load(); key != nil {
return key.([]byte)
}
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
panic(err)
}
baseKey.Store(key)
return key
}
func init() {
platform.RegisterEnvReload(reloadEnvSettings)
if strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true" {
Show = true
}
BaseKey = make([]byte, 32)
rand.Read(BaseKey)
go func() {
time.Sleep(100 * time.Millisecond) // this is not nice, but need to give some time for Android to setup ENV
if raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" }); raw != "" {
if BaseKey, _ = base64.RawURLEncoding.DecodeString(raw); len(BaseKey) == 32 {
return
}
panic(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(BaseKey)))
}
}()
}
func GetGlobalID(ctx context.Context) (globalID [8]byte) {
@@ -68,10 +54,10 @@ func GetGlobalID(ctx context.Context) (globalID [8]byte) {
}
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP &&
(inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun") {
h := blake3.New(8, ensureBaseKey())
h := blake3.New(8, BaseKey)
h.Write([]byte(inbound.Source.String()))
copy(globalID[:], h.Sum(nil))
if Show.Load() {
if Show {
errors.LogInfo(ctx, fmt.Sprintf("XUDP inbound.Source.String(): %v\tglobalID: %v\n", inbound.Source.String(), globalID))
}
}
+4 -4
View File
@@ -125,7 +125,7 @@ func LoadConfig(formatName string, input interface{}) (*Config, error) {
}
if f == "" {
return nil, errors.New("Failed to get format of ", file)
return nil, errors.New("Failed to get format of ", file).AtWarning()
}
if f == "protobuf" {
@@ -142,7 +142,7 @@ func LoadConfig(formatName string, input interface{}) (*Config, error) {
if len(v) == 1 {
return configLoaderByName["protobuf"].Loader(v)
} else {
return nil, errors.New("Only one protobuf config file is allowed")
return nil, errors.New("Only one protobuf config file is allowed").AtWarning()
}
}
@@ -152,11 +152,11 @@ func LoadConfig(formatName string, input interface{}) (*Config, error) {
if f, found := configLoaderByName[formatName]; found {
return f.Loader(v)
} else {
return nil, errors.New("Unable to load config in", formatName)
return nil, errors.New("Unable to load config in", formatName).AtWarning()
}
}
return nil, errors.New("Unable to load config")
return nil, errors.New("Unable to load config").AtWarning()
}
func loadProtobufConfig(data []byte) (*Config, error) {
+2 -2
View File
@@ -19,8 +19,8 @@ import (
var (
Version_x byte = 26
Version_y byte = 9
Version_z byte = 9
Version_y byte = 6
Version_z byte = 1
)
var (
-3
View File
@@ -187,9 +187,6 @@ func NewWithContext(ctx context.Context, config *Config) (*Instance, error) {
}
func initInstanceWithConfig(config *Config, server *Instance) (bool, error) {
if err := platform.ReloadEnvSettings(); err != nil {
return true, errors.New("failed to reload environment settings").Base(err)
}
server.ctx = context.WithValue(server.ctx, "cone",
platform.NewEnvFlag(platform.UseCone).GetValue(func() string { return "" }) != "true")
+8 -18
View File
@@ -3,7 +3,6 @@ package policy
import (
"context"
"runtime"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common/platform"
@@ -83,41 +82,32 @@ func ManagerType() interface{} {
return (*Manager)(nil)
}
var defaultBufferSize atomic.Int32
var defaultBufferSize int32
func reloadEnvSettings() error {
defaultBufferSize.Store(readDefaultBufferSize())
return nil
}
func readDefaultBufferSize() int32 {
func init() {
const defaultValue = -17
size := platform.NewEnvFlag(platform.BufferSize).GetValueAsInt(defaultValue)
switch size {
case 0:
return -1 // For pipe to use unlimited size
defaultBufferSize = -1 // For pipe to use unlimited size
case defaultValue: // Env flag not defined. Use default values per CPU-arch.
switch runtime.GOARCH {
case "arm", "mips", "mipsle":
return 0
defaultBufferSize = 0
case "arm64", "mips64", "mips64le":
return 4 * 1024 // 4k cache for low-end devices
defaultBufferSize = 4 * 1024 // 4k cache for low-end devices
default:
return 512 * 1024
defaultBufferSize = 512 * 1024
}
default:
return int32(size) * 1024 * 1024
defaultBufferSize = int32(size) * 1024 * 1024
}
}
func init() {
platform.RegisterEnvReload(reloadEnvSettings)
}
func defaultBufferPolicy() Buffer {
return Buffer{
PerConnection: defaultBufferSize.Load(),
PerConnection: defaultBufferSize,
}
}
+30 -21
View File
@@ -81,8 +81,6 @@ type Manager interface {
// RegisterCounter registers a new counter to the manager. The identifier string must not be empty, and unique among other counters.
RegisterCounter(string) (Counter, error)
// GetOrRegisterCounter returns the counter by its identifier, atomically creating and registering it if absent.
GetOrRegisterCounter(string) (Counter, error)
// UnregisterCounter unregisters a counter from the manager by its identifier.
UnregisterCounter(string) error
// GetCounter returns a counter by its identifier.
@@ -93,8 +91,6 @@ type Manager interface {
// RegisterOnlineMap registers a new OnlineMap to the manager. The identifier string must not be empty, and unique among other OnlineMaps.
RegisterOnlineMap(string) (OnlineMap, error)
// GetOrRegisterOnlineMap returns the OnlineMap by its identifier, atomically creating and registering it if absent.
GetOrRegisterOnlineMap(string) (OnlineMap, error)
// UnregisterOnlineMap unregisters an OnlineMap from the manager by its identifier.
UnregisterOnlineMap(string) error
// GetOnlineMap returns an OnlineMap by its identifier.
@@ -105,8 +101,6 @@ type Manager interface {
// RegisterChannel registers a new channel to the manager. The identifier string must not be empty, and unique among other channels.
RegisterChannel(string) (Channel, error)
// GetOrRegisterChannel returns the channel by its identifier, atomically creating and registering it if absent.
GetOrRegisterChannel(string) (Channel, error)
// UnregisterChannel unregisters a channel from the manager by its identifier.
UnregisterChannel(string) error
// GetChannel returns a channel by its identifier.
@@ -116,6 +110,36 @@ type Manager interface {
GetAllOnlineUsers() []string
}
// GetOrRegisterCounter tries to get the StatCounter first. If not exist, it then tries to create a new counter.
func GetOrRegisterCounter(m Manager, name string) (Counter, error) {
counter := m.GetCounter(name)
if counter != nil {
return counter, nil
}
return m.RegisterCounter(name)
}
// GetOrRegisterOnlineMap tries to get the OnlineMap first. If not exist, it then tries to create a new OnlineMap.
func GetOrRegisterOnlineMap(m Manager, name string) (OnlineMap, error) {
onlineMap := m.GetOnlineMap(name)
if onlineMap != nil {
return onlineMap, nil
}
return m.RegisterOnlineMap(name)
}
// GetOrRegisterChannel tries to get the StatChannel first. If not exist, it then tries to create a new channel.
func GetOrRegisterChannel(m Manager, name string) (Channel, error) {
channel := m.GetChannel(name)
if channel != nil {
return channel, nil
}
return m.RegisterChannel(name)
}
// ManagerType returns the type of Manager interface. Can be used to implement common.HasType.
//
// xray:api:stable
@@ -136,11 +160,6 @@ func (NoopManager) RegisterCounter(string) (Counter, error) {
return nil, errors.New("not implemented")
}
// GetOrRegisterCounter implements Manager.
func (NoopManager) GetOrRegisterCounter(string) (Counter, error) {
return nil, errors.New("not implemented")
}
// UnregisterCounter implements Manager.
func (NoopManager) UnregisterCounter(string) error {
return nil
@@ -159,11 +178,6 @@ func (NoopManager) RegisterOnlineMap(string) (OnlineMap, error) {
return nil, errors.New("not implemented")
}
// GetOrRegisterOnlineMap implements Manager.
func (NoopManager) GetOrRegisterOnlineMap(string) (OnlineMap, error) {
return nil, errors.New("not implemented")
}
// UnregisterOnlineMap implements Manager.
func (NoopManager) UnregisterOnlineMap(string) error {
return nil
@@ -182,11 +196,6 @@ func (NoopManager) RegisterChannel(string) (Channel, error) {
return nil, errors.New("not implemented")
}
// GetOrRegisterChannel implements Manager.
func (NoopManager) GetOrRegisterChannel(string) (Channel, error) {
return nil, errors.New("not implemented")
}
// UnregisterChannel implements Manager.
func (NoopManager) UnregisterChannel(string) error {
return nil
+26 -30
View File
@@ -1,65 +1,61 @@
module github.com/xtls/xray-core
go 1.27
go 1.26
require (
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
github.com/cloudflare/circl v1.6.5
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716
github.com/cloudflare/circl v1.6.3
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/klauspost/cpuid/v2 v2.4.0
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138
github.com/miekg/dns v1.1.73
github.com/klauspost/cpuid/v2 v2.3.0
github.com/miekg/dns v1.1.72
github.com/pelletier/go-toml v1.9.5
github.com/pion/stun/v3 v3.1.7
github.com/pires/go-proxyproto v0.15.0
github.com/pion/stun/v3 v3.1.2
github.com/pires/go-proxyproto v0.12.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.1
github.com/sagernet/sing v0.5.1
github.com/sagernet/sing-shadowsocks v0.2.7
github.com/stretchr/testify v1.12.1
github.com/stretchr/testify v1.11.1
github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.55.0
golang.org/x/crypto v0.51.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.58.0
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.45.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.0.1
google.golang.org/grpc v1.83.2
google.golang.org/protobuf v1.36.12
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.11
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
h12.io/socks v1.0.3
lukechampine.com/blake3 v1.4.1
mvdan.cc/gofumpt v0.12.0
)
require (
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/huin/goupnp v1.2.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/pion/dtls/v3 v3.1.5 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pion/dtls/v3 v3.1.2 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.1.0 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.49.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+61 -75
View File
@@ -1,19 +1,20 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
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.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I=
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -22,111 +23,97 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI=
github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c=
github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY=
github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
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/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
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/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138 h1:YohuNPT/1k3VcThCQlBZ43PCPWPfMRS1zcxWBF2SLK8=
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138/go.mod h1:TXQg5tfSy+bUjnhT5728j5j/MBj7keIYqqZ1+8k/ui8=
github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU=
github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ=
github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pion/dtls/v3 v3.1.5 h1:9xJtVsHwMYeSjPp5Hh1FTis4DchnQWtnOa5o+6ygqfc=
github.com/pion/dtls/v3 v3.1.5/go.mod h1:gz1K4jg6c+fq86oQMH4pilpCEOEPwmEr2jY+VcF/mkU=
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
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/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/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
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/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/pion/stun/v3 v3.1.2 h1:86IhD8wFn6IDW4b1/0QzoQS+f5PeA8OHHRn8UZW5ErY=
github.com/pion/stun/v3 v3.1.2/go.mod h1:H7gDic7nNwlUL05pbs6T1dtaBehh/KjupxfWw3ZI7cA=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0 h1:rb+fKQFhz+5I2PPuQsNYxI5mUU840XWYtRF0ZBjvkws=
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
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/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -134,22 +121,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -161,23 +147,23 @@ golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
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/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk=
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo=
h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
mvdan.cc/gofumpt v0.12.0 h1:1Lbudkz2kpM9Cjz2pL4M19u7q+GaEhCTNf7N9mfpcho=
mvdan.cc/gofumpt v0.12.0/go.mod h1:SmBHHrljiZu/uoypeKup3rFzP6eoC9UwCp2iH5E3jZA=
+30 -20
View File
@@ -1,42 +1,52 @@
package conf
import (
"encoding/base64"
"strings"
"encoding/json"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/proxy/blackhole"
"google.golang.org/protobuf/proto"
)
type ResponseConfig struct {
Type string `json:"type"`
CustomResponseData string `json:"customResponseData"`
type NoneResponse struct{}
func (*NoneResponse) Build() (proto.Message, error) {
return new(blackhole.NoneResponse), nil
}
type HTTPResponse struct{}
func (*HTTPResponse) Build() (proto.Message, error) {
return new(blackhole.HTTPResponse), nil
}
type BlackholeConfig struct {
Response *ResponseConfig `json:"response"`
Response json.RawMessage `json:"response"`
}
func (v *BlackholeConfig) Build() (proto.Message, error) {
config := new(blackhole.Config)
if v.Response != nil {
responseName := strings.ToLower(v.Response.Type)
switch responseName {
case "none", "":
config.Response = &blackhole.Response{Type: "none"}
case "http":
config.Response = &blackhole.Response{Type: "http"}
case "custom":
data, err := base64.StdEncoding.DecodeString(v.Response.CustomResponseData)
if err != nil {
return nil, errors.New("failed to decode custom response data: " + err.Error())
}
config.Response = &blackhole.Response{Type: "custom", CustomResponseData: data}
default:
return nil, errors.New("unknown blackhole response: " + responseName)
response, _, err := configLoader.Load(v.Response)
if err != nil {
return nil, errors.New("Config: Failed to parse Blackhole response config.").Base(err)
}
responseSettings, err := response.(Buildable).Build()
if err != nil {
return nil, err
}
config.Response = serial.ToTypedMessage(responseSettings)
}
return config, nil
}
var configLoader = NewJSONConfigLoader(
ConfigCreatorCache{
"none": func() interface{} { return new(NoneResponse) },
"http": func() interface{} { return new(HTTPResponse) },
},
"type",
"",
)
+2 -25
View File
@@ -3,6 +3,7 @@ package conf_test
import (
"testing"
"github.com/xtls/xray-core/common/serial"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy/blackhole"
)
@@ -21,7 +22,7 @@ func TestHTTPResponseJSON(t *testing.T) {
}`,
Parser: loadJSON(creator),
Output: &blackhole.Config{
Response: &blackhole.Response{Type: "http"},
Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}),
},
},
{
@@ -31,27 +32,3 @@ func TestHTTPResponseJSON(t *testing.T) {
},
})
}
func TestCustomResponseJSON(t *testing.T) {
creator := func() Buildable {
return new(BlackholeConfig)
}
runMultiTestCase(t, []TestCase{
{
Input: `{
"response": {
"type": "custom",
"customResponseData": "Y3VzdG9tIHJlc3BvbnNl"
}
}`,
Parser: loadJSON(creator),
Output: &blackhole.Config{
Response: &blackhole.Response{
Type: "custom",
CustomResponseData: []byte("custom response"),
},
},
},
})
}
-3
View File
@@ -65,9 +65,6 @@ func (v *Address) UnmarshalJSON(data []byte) error {
}
func (v *Address) Build() *net.IPOrDomain {
if v == nil {
return nil
}
return net.NewIPOrDomain(v.Address)
}
-59
View File
@@ -1,59 +0,0 @@
package conf
import (
"encoding/json"
"os"
"testing"
)
func TestRootEnvAppliesArbitraryValues(t *testing.T) {
const (
valueKey = "XRAY_TEST_CONFIG_ENV"
emptyKey = "XRAY_TEST_CONFIG_EMPTY"
)
t.Setenv(valueKey, "before")
t.Setenv(emptyKey, "before")
config := new(Config)
if err := json.Unmarshal([]byte(`{
"env": {
"XRAY_TEST_CONFIG_ENV": "configured",
"XRAY_TEST_CONFIG_EMPTY": ""
}
}`), config); err != nil {
t.Fatal(err)
}
if _, err := config.Build(); err != nil {
t.Fatal(err)
}
if got := os.Getenv(valueKey); got != "configured" {
t.Fatalf("env %q = %q, want %q", valueKey, got, "configured")
}
if got := os.Getenv(emptyKey); got != "" {
t.Fatalf("env %q = %q, want empty", emptyKey, got)
}
}
func TestEnvConfigOverride(t *testing.T) {
base := EnvConfig{
"ONE": "one",
"TWO": "old",
}
override := EnvConfig{
"TWO": "new",
"THREE": "three",
}
base.Override(override)
want := map[string]string{
"ONE": "one",
"TWO": "new",
"THREE": "three",
}
for key, value := range want {
if got := base[key]; got != value {
t.Fatalf("env %q = %q, want %q", key, got, value)
}
}
}
+41
View File
@@ -0,0 +1,41 @@
package conf
import (
"github.com/xtls/xray-core/transport/internet/grpc"
"google.golang.org/protobuf/proto"
)
type GRPCConfig struct {
Authority string `json:"authority"`
ServiceName string `json:"serviceName"`
MultiMode bool `json:"multiMode"`
IdleTimeout int32 `json:"idle_timeout"`
HealthCheckTimeout int32 `json:"health_check_timeout"`
PermitWithoutStream bool `json:"permit_without_stream"`
InitialWindowsSize int32 `json:"initial_windows_size"`
UserAgent string `json:"user_agent"`
}
func (g *GRPCConfig) Build() (proto.Message, error) {
if g.IdleTimeout <= 0 {
g.IdleTimeout = 0
}
if g.HealthCheckTimeout <= 0 {
g.HealthCheckTimeout = 0
}
if g.InitialWindowsSize < 0 {
// default window size of gRPC-go
g.InitialWindowsSize = 0
}
return &grpc.Config{
Authority: g.Authority,
ServiceName: g.ServiceName,
MultiMode: g.MultiMode,
IdleTimeout: g.IdleTimeout,
HealthCheckTimeout: g.HealthCheckTimeout,
PermitWithoutStream: g.PermitWithoutStream,
InitialWindowsSize: g.InitialWindowsSize,
UserAgent: g.UserAgent,
}, nil
}
+2 -2
View File
@@ -97,7 +97,7 @@ func (v *HTTPClientConfig) Build() (proto.Message, error) {
user.Email = v.Email
} else {
if err := json.Unmarshal(rawUser, user); err != nil {
return nil, errors.New("failed to parse HTTP user").Base(err)
return nil, errors.New("failed to parse HTTP user").Base(err).AtError()
}
}
account := new(HTTPAccount)
@@ -106,7 +106,7 @@ func (v *HTTPClientConfig) Build() (proto.Message, error) {
account.Password = v.Password
} else {
if err := json.Unmarshal(rawUser, account); err != nil {
return nil, errors.New("failed to parse HTTP account").Base(err)
return nil, errors.New("failed to parse HTTP account").Base(err).AtError()
}
}
user.Account = serial.ToTypedMessage(account.Build())
+1 -4
View File
@@ -22,6 +22,7 @@ func (c *HysteriaClientConfig) Build() (proto.Message, error) {
}
config := &hysteria.ClientConfig{}
config.Version = c.Version
config.Server = &protocol.ServerEndpoint{
Address: c.Address.Build(),
Port: uint32(c.Port),
@@ -43,10 +44,6 @@ type HysteriaServerConfig struct {
}
func (c *HysteriaServerConfig) Build() (proto.Message, error) {
if c.Version != 2 {
return nil, errors.New("version != 2")
}
config := new(hysteria.ServerConfig)
if c.Clients != nil {
+1 -1
View File
@@ -18,7 +18,7 @@ func RegisterConfigureFilePostProcessingStage(name string, stage ConfigureFilePo
func PostProcessConfigureFile(conf *Config) error {
for k, v := range configureFilePostProcessingStages {
if err := v.Process(conf); err != nil {
return errors.New("Rejected by Postprocessing Stage ", k).Base(err)
return errors.New("Rejected by Postprocessing Stage ", k).AtError().Base(err)
}
}
return nil
+2 -2
View File
@@ -13,7 +13,7 @@ type ConfigCreatorCache map[string]ConfigCreator
func (v ConfigCreatorCache) RegisterCreator(id string, creator ConfigCreator) error {
if _, found := v[id]; found {
return errors.New(id, " already registered.")
return errors.New(id, " already registered.").AtError()
}
v[id] = creator
@@ -61,7 +61,7 @@ func (v *JSONConfigLoader) Load(raw []byte) (interface{}, string, error) {
}
rawID, found := obj[v.idKey]
if !found {
return nil, "", errors.New(v.idKey, " not found in JSON context")
return nil, "", errors.New(v.idKey, " not found in JSON context").AtError()
}
var id string
if err := json.Unmarshal(rawID, &id); err != nil {
+2 -12
View File
@@ -1,24 +1,14 @@
package conf
import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/proxy/loopback"
"google.golang.org/protobuf/proto"
)
type LoopbackConfig struct {
InboundTag string `json:"inboundTag"`
Sniffing *SniffingConfig `json:"sniffing"`
InboundTag string `json:"inboundTag"`
}
func (l LoopbackConfig) Build() (proto.Message, error) {
c := &loopback.Config{InboundTag: l.InboundTag}
if l.Sniffing != nil {
sc, err := l.Sniffing.Build()
if err != nil {
return nil, errors.New("failed to build sniffing config").Base(err)
}
c.Sniffing = sc
}
return c, nil
return &loopback.Config{InboundTag: l.InboundTag}, nil
}
+1 -1
View File
@@ -23,7 +23,7 @@ func (o *ObservatoryConfig) Build() (proto.Message, error) {
type BurstObservatoryConfig struct {
SubjectSelector []string `json:"subjectSelector"`
// health check settings
HealthCheck *HealthCheckSettings `json:"pingConfig,omitempty"`
HealthCheck *healthCheckSettings `json:"pingConfig,omitempty"`
}
func (b BurstObservatoryConfig) Build() (proto.Message, error) {
-5
View File
@@ -148,7 +148,6 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) {
LocalIP *StringList `json:"localIP"`
LocalPort *PortList `json:"localPort"`
Process *StringList `json:"process"`
LocalOS *StringList `json:"localOS"`
Webhook *WebhookRuleConfig `json:"webhook"`
}
rawFieldRule := new(RawFieldRule)
@@ -262,10 +261,6 @@ func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) {
rule.Process = *rawFieldRule.Process
}
if rawFieldRule.LocalOS != nil && len(*rawFieldRule.LocalOS) > 0 {
rule.LocalOs = *rawFieldRule.LocalOS
}
if rawFieldRule.Webhook != nil && rawFieldRule.Webhook.URL != "" {
rule.Webhook = &router.WebhookConfig{
Url: rawFieldRule.Webhook.URL,
+3 -3
View File
@@ -43,8 +43,8 @@ type strategyLeastLoadConfig struct {
Tolerance float64 `json:"tolerance,omitempty"`
}
// HealthCheckSettings holds settings for health Checker
type HealthCheckSettings struct {
// healthCheckSettings holds settings for health Checker
type healthCheckSettings struct {
Destination string `json:"destination"`
Connectivity string `json:"connectivity"`
Interval duration.Duration `json:"interval"`
@@ -53,7 +53,7 @@ type HealthCheckSettings struct {
HttpMethod string `json:"httpMethod"`
}
func (h HealthCheckSettings) Build() (proto.Message, error) {
func (h healthCheckSettings) Build() (proto.Message, error) {
var httpMethod string
if h.HttpMethod == "" {
httpMethod = "HEAD"

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