Compare commits

...
13 Commits
Author SHA1 Message Date
RPRXandGitHub 45cf2898ab Xray-core v26.6.27
Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668
Project X Channel: https://t.me/projectXtls

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

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

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-06-27 13:18:03 +00:00
RPRXandGitHub 18b85adb4e XHTTP client: Change default maxConnections to 6 for anti-RKN
"xmux": {
    "maxConcurrency": 0,
    "maxConnections": "6",
    "cMaxReuseTimes": 0,
    "hMaxRequestTimes": "600-900",
    "hMaxReusableSecs": "1800-3000",
    "hKeepAlivePeriod": 0
}

Replaces https://github.com/XTLS/Xray-core/commit/9cc7907234a8297a87a0ff77fc40db373b74a0f2 and https://github.com/XTLS/Xray-core/commit/4ce65fc74c4c50919b10b3faff9725f75bba5d73

Closes https://github.com/XTLS/Xray-core/issues/6376#issuecomment-4817033592
2026-06-27 12:41:39 +00:00
452b719504 Hysteria inbound: Support routing's vlessRoute as well (#6375)
https://github.com/XTLS/Xray-core/pull/6375#issuecomment-4795522284

---------

Co-authored-by: LjhAUMEM <llnu14702@gmail.com>
2026-06-27 12:04:58 +00:00
345c76f9a8 WireGuard inbound: Support dynamic peer management (#6360)
https://github.com/XTLS/Xray-core/pull/6360#issuecomment-4780311547

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: LjhAUMEM <llnu14702@gmail.com>
2026-06-27 11:41:22 +00:00
f496437b84 XHTTP server: Refactor upload_queue.go (#6372)
https://github.com/XTLS/Xray-core/pull/6372#issuecomment-4801395378

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-06-27 10:40:58 +00:00
b12bc504c8 README.md: Add Magic_V2Ray to Magisk in Installation (#6355)
https://github.com/XTLS/Xray-core/pull/6355#issuecomment-4793603321

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-06-24 21:06:24 +00:00
yiguodevandGitHub dda2b10c9d TUN inbound: Add traffic counters; Metrics: Rely on instance (#6349)
https://github.com/XTLS/Xray-core/pull/6349#issuecomment-4775121300
2026-06-24 12:00:22 +00:00
Jasper344612andGitHub 241aa38ac0 TUN inbound: Support autoSystemRoutingTable and autoOutboundsInterface on macOS and Linux as well (#6366)
https://github.com/XTLS/Xray-core/pull/6366#issuecomment-4788510365
2026-06-24 11:06:00 +00:00
风扇滑翔翼andRPRX 7e7e820763 Geodata: Apply uTLS Chrome fingerprint when downloading (#6371)
Closes https://github.com/XTLS/Xray-core/issues/6369
2026-06-24 10:52:28 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f9eb1597ad Bump actions/cache from 5 to 6 (#6368)
Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-24 10:48:21 +00:00
风扇滑翔翼andGitHub ac04c445bd DNS: Fix unexpected TTL clamp (#6363)
Fixes https://github.com/XTLS/Xray-core/issues/6359
2026-06-23 11:48:17 +00:00
e7e9254630 TUN inbound: Avoid panic on nil RemoteAddr due to quickly closed connection (#6365)
Fixes https://github.com/XTLS/Xray-core/issues/6364#issuecomment-4778262075

---------

Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-06-23 11:37:21 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fab4bcc1ed Bump github.com/cloudflare/circl from 1.6.3 to 1.6.4 (#6362)
Bumps [github.com/cloudflare/circl](https://github.com/cloudflare/circl) from 1.6.3 to 1.6.4.
- [Release notes](https://github.com/cloudflare/circl/releases)
- [Commits](https://github.com/cloudflare/circl/compare/v1.6.3...v1.6.4)

---
updated-dependencies:
- dependency-name: github.com/cloudflare/circl
  dependency-version: 1.6.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 10:34:07 +00:00
39 changed files with 1522 additions and 467 deletions
+4 -4
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@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-wintun-
@@ -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@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-wintun-
+4 -4
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@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-wintun-
@@ -225,14 +225,14 @@ jobs:
fi
- name: Restore Geodat Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
if: matrix.goos == 'windows'
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-wintun-
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-geodat-
@@ -59,7 +59,7 @@ jobs:
done
- name: Save Geodat Cache
uses: actions/cache/save@v5
uses: actions/cache/save@v6
if: ${{ steps.update.outputs.unhit }}
with:
path: resources
@@ -73,7 +73,7 @@ jobs:
ASSETHASH: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51
steps:
- name: Restore Wintun Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-wintun-
@@ -129,7 +129,7 @@ jobs:
fi
- name: Save Wintun Cache
uses: actions/cache/save@v5
uses: actions/cache/save@v6
if: ${{ steps.update.outputs.unhit }}
with:
path: resources
+2 -2
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@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-geodat-
@@ -90,7 +90,7 @@ jobs:
go-version-file: go.mod
check-latest: true
- name: Restore Geodat Cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: resources
key: xray-geodat-
+1
View File
@@ -73,6 +73,7 @@
- [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`
+7 -2
View File
@@ -198,9 +198,14 @@ func parseResponse(payload []byte) (*IPRecord, error) {
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 {
@@ -217,7 +222,7 @@ L:
ttl = 1
}
expire := now.Add(time.Duration(ttl) * time.Second)
if ipRecord.Expire.After(expire) {
if ipRecord.Expire.IsZero() || ipRecord.Expire.After(expire) {
ipRecord.Expire = expire
}
+85 -43
View File
@@ -2,6 +2,7 @@ package geodata
import (
"context"
"crypto/tls"
go_errors "errors"
"io"
"net/http"
@@ -9,6 +10,7 @@ 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"
@@ -16,6 +18,7 @@ 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
@@ -26,8 +29,9 @@ type stage struct {
}
type downloader struct {
ctx context.Context
client *http.Client
ctx context.Context
httpClient *http.Client
httpsClient *http.Client
}
type idleConn struct {
@@ -53,52 +57,84 @@ func (c *idleConn) Write(b []byte) (int, error) {
func newDownloader(ctx context.Context, dispatcher routing.Dispatcher, outbound string) *downloader {
return &downloader{
ctx: ctx,
client: newClient(ctx, dispatcher, outbound),
ctx: ctx,
httpClient: newClient(ctx, dispatcher, outbound, false),
httpsClient: newClient(ctx, dispatcher, outbound, true),
}
}
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 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
},
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())
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")
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
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)
if err != nil {
return nil, 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
}
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())
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
},
}
} else {
return &http.Client{
Transport: &http.Transport{
Proxy: nil,
DisableKeepAlives: true,
DialContext: dial,
ResponseHeaderTimeout: idleTimeout,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if req.URL.Scheme != "https" {
return errors.New("redirected to non-https URL: ", req.URL.String())
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
},
}
}
}
@@ -160,7 +196,13 @@ func (d *downloader) fetch(rawURL string, writer io.Writer) error {
}
utils.TryDefaultHeadersWith(req.Header, "nav")
resp, err := d.client.Do(req)
var client *http.Client
if req.URL.Scheme == "https" {
client = d.httpsClient
} else {
client = d.httpClient
}
resp, err := client.Do(req)
if err != nil {
return err
}
+151 -59
View File
@@ -2,15 +2,18 @@ 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"
"github.com/xtls/xray-core/common/net"
xnet "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"
@@ -21,15 +24,17 @@ import (
type MetricsHandler struct {
ohm outbound.Manager
statsManager feature_stats.Manager
observatory extension.Observatory
ctx context.Context
tag string
listen string
tcpListener net.Listener
tcpListener xnet.Listener
listener *OutboundListener
}
// 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,
}
@@ -37,46 +42,6 @@ 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
}
@@ -85,45 +50,172 @@ 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 := net.Listen("tcp", p.listen)
TCPlistener, err := xnet.Listen("tcp", p.listen)
if err != nil {
return err
}
p.tcpListener = TCPlistener
errors.LogInfo(context.Background(), "Metrics server listening on ", p.listen)
go func() {
if err := http.Serve(TCPlistener, http.DefaultServeMux); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to start metrics server")
}
}()
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
}
listener := &OutboundListener{
buffer: make(chan net.Conn, 4),
buffer: make(chan xnet.Conn, 4),
done: done.New(),
}
p.listener = listener
go func() {
if err := http.Serve(listener, http.DefaultServeMux); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to start metrics server")
}
}()
go p.serve(listener, handler)
if err := p.ohm.RemoveHandler(context.Background(), p.tag); err != nil {
errors.LogInfo(context.Background(), "failed to remove existing handler")
}
return p.ohm.AddHandler(context.Background(), &Outbound{
if err := 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 {
return nil
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
}
func init() {
+161
View File
@@ -0,0 +1,161 @@
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
}
+1 -1
View File
@@ -20,7 +20,7 @@ import (
var (
Version_x byte = 26
Version_y byte = 6
Version_z byte = 22
Version_z byte = 27
)
var (
+1 -1
View File
@@ -4,7 +4,7 @@ go 1.26
require (
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716
github.com/cloudflare/circl v1.6.3
github.com/cloudflare/circl v1.6.4
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
+2 -2
View File
@@ -4,8 +4,8 @@ github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkd
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.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U=
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY=
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=
+4 -1
View File
@@ -22,7 +22,6 @@ 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),
@@ -44,6 +43,10 @@ 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 {
+2 -3
View File
@@ -404,8 +404,8 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
}
if c.Xmux == (XmuxConfig{}) {
c.Xmux.MaxConcurrency.From = 1
c.Xmux.MaxConcurrency.To = 1
c.Xmux.MaxConnections.From = 6
c.Xmux.MaxConnections.To = 6
c.Xmux.HMaxRequestTimes.From = 600
c.Xmux.HMaxRequestTimes.To = 900
c.Xmux.HMaxReusableSecs.From = 1800
@@ -571,7 +571,6 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
}
config := &hysteria.Config{}
config.Version = c.Version
config.Auth = c.Auth
config.UdpIdleTimeout = c.UdpIdleTimeout
config.MasqType = c.Masquerade.Type
+27 -3
View File
@@ -7,6 +7,9 @@ import (
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/proxy/wireguard"
"google.golang.org/protobuf/proto"
)
@@ -17,9 +20,12 @@ type WireGuardPeerConfig struct {
Endpoint string `json:"endpoint"`
KeepAlive uint32 `json:"keepAlive"`
AllowedIPs []string `json:"allowedIPs,omitempty"`
Level uint32 `json:"level"`
Email string `json:"email"`
}
func (c *WireGuardPeerConfig) Build() (proto.Message, error) {
func (c *WireGuardPeerConfig) Build() (*wireguard.PeerConfig, error) {
var err error
config := new(wireguard.PeerConfig)
@@ -78,14 +84,32 @@ func (c *WireGuardConfig) Build() (proto.Message, error) {
config.Endpoint = c.Address
}
if c.Peers != nil {
if c.IsClient {
config.Peers = make([]*wireguard.PeerConfig, len(c.Peers))
for i, p := range c.Peers {
msg, err := p.Build()
if err != nil {
return nil, err
}
config.Peers[i] = msg.(*wireguard.PeerConfig)
config.Peers[i] = msg
}
} else {
config.Users = make([]*protocol.User, len(c.Peers))
processUser := func(idx int) error {
p := c.Peers[idx]
m, err := p.Build()
if err != nil {
return err
}
config.Users[idx] = &protocol.User{
Email: p.Email,
Level: p.Level,
Account: serial.ToTypedMessage(m),
}
return nil
}
if err := task.ParallelForN(len(c.Peers), processUser); err != nil {
return nil, err
}
}
-50
View File
@@ -1,50 +0,0 @@
package conf_test
import (
"testing"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy/wireguard"
)
func TestWireGuardConfig(t *testing.T) {
creator := func() Buildable {
return new(WireGuardConfig)
}
runMultiTestCase(t, []TestCase{
{
Input: `{
"secretKey": "uJv5tZMDltsiYEn+kUwb0Ll/CXWhMkaSCWWhfPEZM3A=",
"address": ["10.1.1.1", "fd59:7153:2388:b5fd:0000:0000:1234:0001"],
"peers": [
{
"publicKey": "6e65ce0be17517110c17d77288ad87e7fd5252dcc7d09b95a39d61db03df832a",
"endpoint": "127.0.0.1:1234"
}
],
"mtu": 1300,
"workers": 2,
"domainStrategy": "ForceIPv6v4",
"noKernelTun": false
}`,
Parser: loadJSON(creator),
Output: &wireguard.DeviceConfig{
// key converted into hex form
SecretKey: "b89bf9b5930396db226049fe914c1bd0b97f0975a13246920965a17cf1193370",
Endpoint: []string{"10.1.1.1", "fd59:7153:2388:b5fd:0000:0000:1234:0001"},
Peers: []*wireguard.PeerConfig{
{
// also can read from hex form directly
PublicKey: "6e65ce0be17517110c17d77288ad87e7fd5252dcc7d09b95a39d61db03df832a",
Endpoint: "127.0.0.1:1234",
AllowedIps: []string{"0.0.0.0/0", "::0/0"},
},
},
Mtu: 1300,
DomainStrategy: wireguard.DeviceConfig_FORCE_IP64,
NoKernelTun: false,
},
},
})
}
+51 -78
View File
@@ -3,25 +3,32 @@ package account
import (
"sync"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/uuid"
"google.golang.org/protobuf/proto"
)
func (a *Account) AsAccount() (protocol.Account, error) {
var VR net.Port
if id, err := uuid.ParseString(a.Auth); err == nil {
VR = net.PortFromBytes(id[6:8])
}
return &MemoryAccount{
Auth: a.Auth,
VR: VR,
}, nil
}
type MemoryAccount struct {
Auth string
VR net.Port
}
func (a *MemoryAccount) Equals(another protocol.Account) bool {
if account, ok := another.(*MemoryAccount); ok {
return a.Auth == account.Auth
func (a *MemoryAccount) Equals(other protocol.Account) bool {
if b, ok := other.(*MemoryAccount); ok {
return a.Auth == b.Auth
}
return false
}
@@ -33,97 +40,63 @@ func (a *MemoryAccount) ToProto() proto.Message {
}
type Validator struct {
emails map[string]struct{}
users map[string]*protocol.MemoryUser
mutex sync.Mutex
users sync.Map
}
func NewValidator() *Validator {
return &Validator{
emails: make(map[string]struct{}),
users: make(map[string]*protocol.MemoryUser),
}
return &Validator{}
}
func (v *Validator) Add(u *protocol.MemoryUser) error {
v.mutex.Lock()
defer v.mutex.Unlock()
if u.Email != "" {
if _, ok := v.emails[u.Email]; ok {
return errors.New("User ", u.Email, " already exists.")
}
v.emails[u.Email] = struct{}{}
}
v.users[u.Account.(*MemoryAccount).Auth] = u
func (v *Validator) Add(user *protocol.MemoryUser) error {
v.users.Store(user.Account.(*MemoryAccount).Auth, user)
return nil
}
func (v *Validator) Del(email string) error {
if email == "" {
return errors.New("Email must not be empty.")
func (v *Validator) DelByEmail(email string) error {
if user := v.GetByEmail(email); user != nil {
v.users.Delete(user.Account.(*MemoryAccount).Auth)
}
v.mutex.Lock()
defer v.mutex.Unlock()
if _, ok := v.emails[email]; !ok {
return errors.New("User ", email, " not found.")
}
delete(v.emails, email)
for key, user := range v.users {
if user.Email == email {
delete(v.users, key)
break
}
}
return nil
}
func (v *Validator) Get(auth string) *protocol.MemoryUser {
v.mutex.Lock()
defer v.mutex.Unlock()
return v.users[auth]
}
func (v *Validator) GetByEmail(email string) *protocol.MemoryUser {
if email == "" {
return nil
if value, ok := v.users.Load(auth); ok {
return value.(*protocol.MemoryUser)
}
v.mutex.Lock()
defer v.mutex.Unlock()
if _, ok := v.emails[email]; ok {
for _, user := range v.users {
if user.Email == email {
return user
}
}
}
return nil
}
func (v *Validator) GetAll() []*protocol.MemoryUser {
v.mutex.Lock()
defer v.mutex.Unlock()
users := make([]*protocol.MemoryUser, 0, len(v.users))
for _, user := range v.users {
users = append(users, user)
}
return users
func (v *Validator) GetByEmail(email string) (user *protocol.MemoryUser) {
v.users.Range(func(key, value any) bool {
if value.(*protocol.MemoryUser).Email == email {
user = value.(*protocol.MemoryUser)
return false
}
return true
})
return
}
func (v *Validator) GetCount() int64 {
v.mutex.Lock()
defer v.mutex.Unlock()
return int64(len(v.users))
func (v *Validator) GetAll() (users []*protocol.MemoryUser) {
v.users.Range(func(key, value any) bool {
users = append(users, value.(*protocol.MemoryUser))
return true
})
return
}
func (v *Validator) GetCount() (count int64) {
v.users.Range(func(key, value any) bool {
count++
return true
})
return
}
func (v *Validator) NotEmpty() (not_empty bool) {
v.users.Range(func(key, value any) bool {
not_empty = true
return false
})
return
}
+4 -13
View File
@@ -24,8 +24,7 @@ const (
type ClientConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
Server *protocol.ServerEndpoint `protobuf:"bytes,2,opt,name=server,proto3" json:"server,omitempty"`
Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -60,13 +59,6 @@ func (*ClientConfig) Descriptor() ([]byte, []int) {
return file_proxy_hysteria_config_proto_rawDescGZIP(), []int{0}
}
func (x *ClientConfig) GetVersion() int32 {
if x != nil {
return x.Version
}
return 0
}
func (x *ClientConfig) GetServer() *protocol.ServerEndpoint {
if x != nil {
return x.Server
@@ -122,10 +114,9 @@ var File_proxy_hysteria_config_proto protoreflect.FileDescriptor
const file_proxy_hysteria_config_proto_rawDesc = "" +
"\n" +
"\x1bproxy/hysteria/config.proto\x12\x13xray.proxy.hysteria\x1a!common/protocol/server_spec.proto\x1a\x1acommon/protocol/user.proto\"f\n" +
"\fClientConfig\x12\x18\n" +
"\aversion\x18\x01 \x01(\x05R\aversion\x12<\n" +
"\x06server\x18\x02 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"@\n" +
"\x1bproxy/hysteria/config.proto\x12\x13xray.proxy.hysteria\x1a!common/protocol/server_spec.proto\x1a\x1acommon/protocol/user.proto\"L\n" +
"\fClientConfig\x12<\n" +
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"@\n" +
"\fServerConfig\x120\n" +
"\x05users\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x05usersB[\n" +
"\x17com.xray.proxy.hysteriaP\x01Z(github.com/xtls/xray-core/proxy/hysteria\xaa\x02\x13Xray.Proxy.Hysteriab\x06proto3"
+1 -2
View File
@@ -10,8 +10,7 @@ import "common/protocol/server_spec.proto";
import "common/protocol/user.proto";
message ClientConfig {
int32 version = 1;
xray.common.protocol.ServerEndpoint server = 2;
xray.common.protocol.ServerEndpoint server = 1;
}
message ServerConfig {
+10 -7
View File
@@ -59,12 +59,12 @@ func (s *Server) HysteriaInboundValidator() *account.Validator {
return s.validator
}
func (s *Server) AddUser(ctx context.Context, u *protocol.MemoryUser) error {
return s.validator.Add(u)
func (s *Server) AddUser(ctx context.Context, user *protocol.MemoryUser) error {
return s.validator.Add(user)
}
func (s *Server) RemoveUser(ctx context.Context, e string) error {
return s.validator.Del(e)
func (s *Server) RemoveUser(ctx context.Context, email string) error {
return s.validator.DelByEmail(email)
}
func (s *Server) GetUser(ctx context.Context, email string) *protocol.MemoryUser {
@@ -91,9 +91,12 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con
iConn := stat.TryUnwrapStatsConn(conn)
type User interface{ User() *protocol.MemoryUser }
if v, ok := iConn.(User); ok && v.User() != nil {
inbound.User = v.User()
if v, ok := iConn.(interface{ User() *protocol.MemoryUser }); ok {
user := v.User()
if user != nil {
inbound.User = user
inbound.VlessRoute = user.Account.(*account.MemoryAccount).VR
}
}
if _, ok := iConn.(*hysteria.InterConn); ok {
+7 -81
View File
@@ -3,8 +3,6 @@ package tun
import (
"context"
"net"
"sort"
"strings"
"sync"
"github.com/xtls/xray-core/common/errors"
@@ -31,95 +29,23 @@ func (updater *InterfaceUpdater) Update() {
updater.Lock()
defer updater.Unlock()
if updater.iface != nil {
iface, err := net.InterfaceByIndex(updater.iface.Index)
if err == nil && iface.Name == updater.iface.Name {
return
}
}
updater.iface = nil
interfaces, err := net.Interfaces()
got, err := findOutboundInterface(updater.tunIndex, updater.fixedName)
if err != nil {
errors.LogInfoInner(context.Background(), err, "[tun] failed to update interface")
updater.iface = nil
return
}
var got *net.Interface
if updater.fixedName != "" {
for _, iface := range interfaces {
if iface.Index == updater.tunIndex {
continue
}
if iface.Name == updater.fixedName {
got = &iface
break
}
}
} else {
var ifs []struct {
index int
score int
}
for i, iface := range interfaces {
if iface.Index == updater.tunIndex {
continue
}
if strings.Contains(iface.Name, "vEthernet") {
continue
}
if iface.Flags&net.FlagUp == 0 {
continue
}
if iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil || len(addrs) == 0 {
continue
}
ifs = append(ifs, struct {
index int
score int
}{i, score(&iface, addrs)})
}
sort.Slice(ifs, func(i, j int) bool {
if ifs[i].score != ifs[j].score {
return ifs[i].score > ifs[j].score
}
return interfaces[ifs[i].index].Name < interfaces[ifs[j].index].Name
})
if len(ifs) > 0 {
iface := interfaces[ifs[0].index]
got = &iface
}
}
if got == nil {
errors.LogInfo(context.Background(), "[tun] failed to update interface > got == nil")
updater.iface = nil
return
}
if updater.iface != nil && updater.iface.Index == got.Index && updater.iface.Name == got.Name {
return
}
updater.iface = got
errors.LogInfo(context.Background(), "[tun] update interface ", got.Name, " ", got.Index)
}
func score(iface *net.Interface, addrs []net.Addr) int {
score := 0
name := strings.ToLower(iface.Name)
if strings.Contains(name, "wlan") || strings.Contains(name, "wi-fi") {
score += 2
}
for _, addr := range addrs {
if strings.HasPrefix(addr.String(), "192.168.") {
score += 1
break
}
}
return score
}
+36 -1
View File
@@ -17,6 +17,7 @@ import (
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/policy"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/stats"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat"
@@ -32,6 +33,8 @@ type Handler struct {
dispatcher routing.Dispatcher
tag string
sniffingRequest session.SniffingRequest
uplinkCounter stats.Counter
downlinkCounter stats.Counter
}
// ConnectionHandler interface with the only method that stack is going to push new connections to
@@ -59,6 +62,23 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
t.policyManager = pm
t.dispatcher = dispatcher
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundUplink {
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + t.tag + ">>>traffic>>>uplink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
t.uplinkCounter = c
}
}
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundDownlink {
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + t.tag + ">>>traffic>>>downlink"
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
t.downlinkCounter = c
}
}
return nil
}
@@ -143,7 +163,22 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
defer cancel()
ctx = c.ContextWithID(ctx, session.NewID())
source := net.DestinationFromAddr(conn.RemoteAddr())
// if the connection is already closed, conn.RemoteAddr() will be nil
// due to gvisor weird behavior
remote := conn.RemoteAddr()
if remote == nil {
errors.LogInfo(t.ctx, "dropped quickly closed connection")
return
}
source := net.DestinationFromAddr(remote)
if t.uplinkCounter != nil || t.downlinkCounter != nil {
conn = &stat.CounterConnection{
Connection: conn,
ReadCounter: t.uplinkCounter,
WriteCounter: t.downlinkCounter,
}
}
inbound := session.Inbound{
Name: "tun",
Tag: t.tag,
+133
View File
@@ -0,0 +1,133 @@
package tun
import (
"bytes"
"context"
"net"
"sync/atomic"
"testing"
"time"
"github.com/xtls/xray-core/common/buf"
xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport"
)
type testCounter struct {
value int64
}
func (c *testCounter) Value() int64 {
return atomic.LoadInt64(&c.value)
}
func (c *testCounter) Set(value int64) int64 {
return atomic.SwapInt64(&c.value, value)
}
func (c *testCounter) Add(value int64) int64 {
return atomic.AddInt64(&c.value, value) - value
}
type testConn struct {
reader *bytes.Reader
writer bytes.Buffer
}
func newTestConn(input []byte) *testConn {
return &testConn{reader: bytes.NewReader(input)}
}
func (c *testConn) Read(payload []byte) (int, error) {
return c.reader.Read(payload)
}
func (c *testConn) Write(payload []byte) (int, error) {
return c.writer.Write(payload)
}
func (c *testConn) Close() error {
return nil
}
func (c *testConn) LocalAddr() net.Addr {
return &net.TCPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 1080}
}
func (c *testConn) RemoteAddr() net.Addr {
return &net.TCPAddr{IP: net.IPv4(10, 0, 0, 2), Port: 12345}
}
func (c *testConn) SetDeadline(time.Time) error {
return nil
}
func (c *testConn) SetReadDeadline(time.Time) error {
return nil
}
func (c *testConn) SetWriteDeadline(time.Time) error {
return nil
}
type testDispatcher struct {
writePayload []byte
readBytes int32
}
func (d *testDispatcher) Type() interface{} {
return routing.DispatcherType()
}
func (d *testDispatcher) Start() error {
return nil
}
func (d *testDispatcher) Close() error {
return nil
}
func (d *testDispatcher) Dispatch(context.Context, xnet.Destination) (*transport.Link, error) {
return nil, nil
}
func (d *testDispatcher) DispatchLink(ctx context.Context, dest xnet.Destination, link *transport.Link) error {
mb, err := link.Reader.ReadMultiBuffer()
if err != nil {
return err
}
atomic.StoreInt32(&d.readBytes, mb.Len())
buf.ReleaseMulti(mb)
return link.Writer.WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(d.writePayload)})
}
func TestHandlerCountsTunConnectionTraffic(t *testing.T) {
uplinkCounter := new(testCounter)
downlinkCounter := new(testCounter)
dispatcher := &testDispatcher{writePayload: []byte("downlink")}
conn := newTestConn([]byte("uplink"))
handler := &Handler{
ctx: context.Background(),
config: &Config{},
dispatcher: dispatcher,
uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter,
}
handler.HandleConnection(conn, xnet.TCPDestination(xnet.LocalHostIP, 443))
if got := uplinkCounter.Value(); got != int64(len("uplink")) {
t.Fatalf("unexpected uplink counter: got %d, want %d", got, len("uplink"))
}
if got := downlinkCounter.Value(); got != int64(len("downlink")) {
t.Fatalf("unexpected downlink counter: got %d, want %d", got, len("downlink"))
}
if got := int(atomic.LoadInt32(&dispatcher.readBytes)); got != len("uplink") {
t.Fatalf("dispatcher read unexpected bytes: got %d, want %d", got, len("uplink"))
}
if got := conn.writer.String(); got != "downlink" {
t.Fatalf("connection write mismatch: got %q, want %q", got, "downlink")
}
}
+14
View File
@@ -81,3 +81,17 @@ func (t *AndroidTun) newEndpoint() (stack.LinkEndpoint, error) {
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
return unix.BindToDevice(int(fd), iface.Name)
}
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
if fixedName == "" {
return nil, errors.New("automatic outbound interface selection is not supported on this platform")
}
iface, err := net.InterfaceByName(fixedName)
if err != nil {
return nil, err
}
if iface.Index == tunIndex {
return nil, errors.New("outbound interface cannot be the TUN interface")
}
return iface, nil
}
+272 -2
View File
@@ -3,16 +3,20 @@
package tun
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"os"
"strconv"
"sync"
"unsafe"
"github.com/xtls/xray-core/common/buf"
xerrors "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
@@ -42,6 +46,10 @@ type DarwinTun struct {
options *Config
tunFd int
ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system)
routeMonitor *os.File
routeMonitorOnce sync.Once
systemRoutes []netip.Prefix
}
var (
@@ -92,15 +100,53 @@ func NewTun(options *Config) (Tun, error) {
}
func (t *DarwinTun) Start() error {
if !t.ownsFd {
return nil
}
if err := t.setSystemRoutes(); err != nil {
return err
}
if updater != nil {
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
_ = t.unsetSystemRoutes()
return err
}
t.routeMonitor = os.NewFile(uintptr(fd), "xray-route-monitor")
go t.monitorRouteChanges()
}
return nil
}
func (t *DarwinTun) Close() error {
t.routeMonitorOnce.Do(func() {
if t.routeMonitor != nil {
_ = t.routeMonitor.Close()
}
})
routeErr := t.unsetSystemRoutes()
if t.ownsFd {
return t.tunFile.Close()
return xerrors.Combine(routeErr, t.tunFile.Close())
}
// iOS: don't close the fd, it's owned by NetworkExtension
return nil
return routeErr
}
func (t *DarwinTun) monitorRouteChanges() {
buffer := make([]byte, 64*1024)
for {
if _, err := t.routeMonitor.Read(buffer); err != nil {
if !errors.Is(err, os.ErrClosed) {
xerrors.LogInfoInner(context.Background(), err, "[tun] failed to monitor route changes")
}
return
}
if updater != nil {
updater.Update()
}
}
}
func (t *DarwinTun) Name() (string, error) {
@@ -388,3 +434,227 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
return errors.Join(err1, err2)
}
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
if fixedName != "" {
iface, err := net.InterfaceByName(fixedName)
if err != nil {
return nil, err
}
if iface.Index == tunIndex {
return nil, errors.New("outbound interface cannot be the TUN interface")
}
return iface, nil
}
rib, err := route.FetchRIB(unix.AF_UNSPEC, route.RIBTypeRoute, 0)
if err != nil {
return nil, err
}
messages, err := route.ParseRIB(route.RIBTypeRoute, rib)
if err != nil {
return nil, err
}
var ipv6Index int
for _, message := range messages {
routeMessage, ok := message.(*route.RouteMessage)
if !ok || routeMessage.Index == tunIndex {
continue
}
if routeMessage.Flags&unix.RTF_UP == 0 || routeMessage.Flags&unix.RTF_GATEWAY == 0 {
continue
}
family, ok := defaultRouteFamily(routeMessage)
if !ok {
continue
}
if family == unix.AF_INET {
return usableDarwinInterface(routeMessage.Index)
}
if family == unix.AF_INET6 && ipv6Index == 0 {
ipv6Index = routeMessage.Index
}
}
if ipv6Index != 0 {
return usableDarwinInterface(ipv6Index)
}
return nil, errors.New("default route not found")
}
func defaultRouteFamily(message *route.RouteMessage) (int, bool) {
if len(message.Addrs) <= unix.RTAX_NETMASK {
return 0, false
}
switch destination := message.Addrs[unix.RTAX_DST].(type) {
case *route.Inet4Addr:
mask, ok := message.Addrs[unix.RTAX_NETMASK].(*route.Inet4Addr)
if !ok || destination.IP != netip.IPv4Unspecified().As4() {
return 0, false
}
ones, bits := net.IPMask(mask.IP[:]).Size()
return unix.AF_INET, ones == 0 && bits == 32
case *route.Inet6Addr:
mask, ok := message.Addrs[unix.RTAX_NETMASK].(*route.Inet6Addr)
if !ok || destination.IP != netip.IPv6Unspecified().As16() {
return 0, false
}
ones, bits := net.IPMask(mask.IP[:]).Size()
return unix.AF_INET6, ones == 0 && bits == 128
default:
return 0, false
}
}
func usableDarwinInterface(index int) (*net.Interface, error) {
iface, err := net.InterfaceByIndex(index)
if err != nil {
return nil, err
}
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
return nil, errors.New("default route interface is not usable")
}
return iface, nil
}
func (t *DarwinTun) setSystemRoutes() error {
routes, err := buildDarwinSystemRoutes(t.options.AutoSystemRoutingTable)
if err != nil {
return err
}
if len(routes) == 0 {
return nil
}
tunIndex, err := t.Index()
if err != nil {
return err
}
for _, destination := range routes {
if err := execDarwinRoute(unix.RTM_ADD, tunIndex, destination); err != nil {
_ = t.unsetSystemRoutes()
return xerrors.New("failed to add system route ", destination).Base(err)
}
t.systemRoutes = append(t.systemRoutes, destination)
}
return nil
}
func (t *DarwinTun) unsetSystemRoutes() error {
var errs []error
tunIndex, indexErr := t.Index()
if indexErr != nil && len(t.systemRoutes) > 0 {
errs = append(errs, indexErr)
}
for i := len(t.systemRoutes) - 1; i >= 0; i-- {
destination := t.systemRoutes[i]
if err := execDarwinRoute(unix.RTM_DELETE, tunIndex, destination); err != nil && !errors.Is(err, unix.ESRCH) {
errs = append(errs, xerrors.New("failed to delete system route ", destination).Base(err))
}
}
t.systemRoutes = nil
return xerrors.Combine(errs...)
}
func buildDarwinSystemRoutes(configured []string) ([]netip.Prefix, error) {
routes := make([]netip.Prefix, 0, len(configured))
seen := make(map[netip.Prefix]struct{})
appendRoute := func(prefix netip.Prefix) {
prefix = prefix.Masked()
if _, found := seen[prefix]; found {
return
}
seen[prefix] = struct{}{}
routes = append(routes, prefix)
}
for _, value := range configured {
prefix, err := netip.ParsePrefix(value)
if err != nil {
return nil, xerrors.New("invalid system route ", value).Base(err)
}
prefix = prefix.Masked()
if prefix.Bits() == 0 {
for _, protected := range darwinProtectedDefaultRoutes(prefix.Addr().Is4()) {
appendRoute(protected)
}
continue
}
appendRoute(prefix)
}
return routes, nil
}
func darwinProtectedDefaultRoutes(ipv4 bool) []netip.Prefix {
routes := make([]netip.Prefix, 0, 8)
for i := 0; i < 8; i++ {
if ipv4 {
var address [4]byte
address[0] = 1 << i
routes = append(routes, netip.PrefixFrom(netip.AddrFrom4(address), 8-i))
} else {
var address [16]byte
address[0] = 1 << i
routes = append(routes, netip.PrefixFrom(netip.AddrFrom16(address), 8-i))
}
}
return routes
}
func execDarwinRoute(messageType int, interfaceIndex int, destination netip.Prefix) error {
message := route.RouteMessage{
Type: messageType,
Version: unix.RTM_VERSION,
Flags: unix.RTF_STATIC | unix.RTF_GATEWAY,
Seq: 1,
}
if messageType == unix.RTM_ADD {
message.Flags |= unix.RTF_UP
}
if destination.Addr().Is4() {
gatewayPrefix := netip.MustParsePrefix(gateway)
message.Addrs = []route.Addr{
unix.RTAX_DST: &route.Inet4Addr{IP: destination.Addr().As4()},
unix.RTAX_NETMASK: &route.Inet4Addr{IP: prefixMask4(destination.Bits())},
unix.RTAX_GATEWAY: &route.Inet4Addr{IP: gatewayPrefix.Addr().As4()},
}
} else {
message.Flags &^= unix.RTF_GATEWAY
message.Index = interfaceIndex
message.Addrs = []route.Addr{
unix.RTAX_DST: &route.Inet6Addr{IP: destination.Addr().As16()},
unix.RTAX_NETMASK: &route.Inet6Addr{IP: prefixMask6(destination.Bits())},
unix.RTAX_GATEWAY: &route.LinkAddr{Index: interfaceIndex},
}
}
request, err := message.Marshal()
if err != nil {
return err
}
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
return err
}
defer unix.Close(fd)
_, err = unix.Write(fd, request)
return err
}
func prefixMask4(bits int) [4]byte {
var mask [4]byte
copy(mask[:], net.CIDRMask(bits, 32))
return mask
}
func prefixMask6(bits int) [16]byte {
var mask [16]byte
copy(mask[:], net.CIDRMask(bits, 128))
return mask
}
+14
View File
@@ -42,3 +42,17 @@ func (t *DefaultTun) newEndpoint() (stack.LinkEndpoint, error) {
func setinterface(string, string, uintptr, *net.Interface) error {
return errors.New("Tun is not supported on your platform")
}
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
if fixedName == "" {
return nil, errors.New("automatic outbound interface selection is not supported on this platform")
}
iface, err := net.InterfaceByName(fixedName)
if err != nil {
return nil, err
}
if iface.Index == tunIndex {
return nil, errors.New("outbound interface cannot be the TUN interface")
}
return iface, nil
}
+14
View File
@@ -147,3 +147,17 @@ func (t *FreeBSDTun) newEndpoint() (stack.LinkEndpoint, error) {
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
return nil
}
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
if fixedName == "" {
return nil, errors.New("automatic outbound interface selection is not supported on this platform")
}
iface, err := net.InterfaceByName(fixedName)
if err != nil {
return nil, err
}
if iface.Index == tunIndex {
return nil, errors.New("outbound interface cannot be the TUN interface")
}
return iface, nil
}
+153 -2
View File
@@ -3,8 +3,11 @@
package tun
import (
"context"
"net"
"net/netip"
"strconv"
"sync"
"github.com/vishvananda/netlink"
"github.com/xtls/xray-core/common/errors"
@@ -22,6 +25,10 @@ type LinuxTun struct {
tunLink netlink.Link
options *Config
ownsTun bool
systemRoutes []netlink.Route
routeMonitorStop chan struct{}
routeMonitorOnce sync.Once
}
// LinuxTun implements Tun
@@ -161,16 +168,32 @@ func (t *LinuxTun) Start() error {
return nil
}
err := netlink.LinkSetUp(t.tunLink)
if err != nil {
if err := netlink.LinkSetUp(t.tunLink); err != nil {
return err
}
if err := t.setSystemRoutes(); err != nil {
return err
}
if updater != nil {
t.routeMonitorStop = make(chan struct{})
go t.monitorRouteChanges()
}
return nil
}
// Close is called to shut down the tun interface
func (t *LinuxTun) Close() error {
t.routeMonitorOnce.Do(func() {
if t.routeMonitorStop != nil {
close(t.routeMonitorStop)
}
})
_ = t.unsetSystemRoutes()
if t.ownsTun {
_ = netlink.LinkSetDown(t.tunLink)
}
@@ -199,3 +222,131 @@ func (t *LinuxTun) newEndpoint() (stack.LinkEndpoint, error) {
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
return unix.BindToDevice(int(fd), iface.Name)
}
func (t *LinuxTun) setSystemRoutes() error {
if len(t.options.AutoSystemRoutingTable) == 0 {
return nil
}
tunIndex := t.tunLink.Attrs().Index
for _, cidr := range t.options.AutoSystemRoutingTable {
prefix, err := netip.ParsePrefix(cidr)
if err != nil {
return errors.New("invalid system route ", cidr).Base(err)
}
prefix = prefix.Masked()
_, ipNet, _ := net.ParseCIDR(prefix.String())
route := netlink.Route{
LinkIndex: tunIndex,
Dst: ipNet,
Priority: 1,
}
if err := netlink.RouteAdd(&route); err != nil {
_ = t.unsetSystemRoutes()
return errors.New("failed to add system route ", cidr).Base(err)
}
t.systemRoutes = append(t.systemRoutes, route)
}
return nil
}
func (t *LinuxTun) unsetSystemRoutes() error {
var errs []error
for i := len(t.systemRoutes) - 1; i >= 0; i-- {
route := t.systemRoutes[i]
if err := netlink.RouteDel(&route); err != nil {
errs = append(errs, errors.New("failed to delete system route").Base(err))
}
}
t.systemRoutes = nil
return errors.Combine(errs...)
}
func (t *LinuxTun) monitorRouteChanges() {
routeCh := make(chan netlink.RouteUpdate)
if err := netlink.RouteSubscribe(routeCh, t.routeMonitorStop); err != nil {
errors.LogInfoInner(context.Background(), err, "[tun] failed to subscribe route changes")
return
}
linkCh := make(chan netlink.LinkUpdate)
if err := netlink.LinkSubscribe(linkCh, t.routeMonitorStop); err != nil {
errors.LogInfoInner(context.Background(), err, "[tun] failed to subscribe link changes")
return
}
for {
select {
case _, ok := <-routeCh:
if !ok {
return
}
if updater != nil {
updater.Update()
}
case _, ok := <-linkCh:
if !ok {
return
}
if updater != nil {
updater.Update()
}
case <-t.routeMonitorStop:
return
}
}
}
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
if fixedName != "" {
iface, err := net.InterfaceByName(fixedName)
if err != nil {
return nil, err
}
if iface.Index == tunIndex {
return nil, errors.New("outbound interface cannot be the TUN interface")
}
return iface, nil
}
probeIPs := []net.IP{
net.ParseIP("8.8.8.8"),
net.ParseIP("2001:4860:4860::8888"),
}
for _, ip := range probeIPs {
routes, err := netlink.RouteGet(ip)
if err != nil || len(routes) == 0 {
continue
}
route := routes[0]
if route.LinkIndex == tunIndex {
continue
}
link, err := netlink.LinkByIndex(route.LinkIndex)
if err != nil {
continue
}
attrs := link.Attrs()
if attrs.Flags&net.FlagUp == 0 {
continue
}
operState := attrs.OperState
if operState != netlink.OperUp && operState != netlink.OperUnknown {
continue
}
if route.Src == nil || route.Src.IsLoopback() || route.Src.IsLinkLocalUnicast() {
continue
}
iface, err := net.InterfaceByIndex(route.LinkIndex)
if err != nil {
continue
}
return iface, nil
}
return nil, errors.New("no usable outbound interface found")
}
+76
View File
@@ -8,6 +8,8 @@ import (
go_errors "errors"
"net"
"net/netip"
"sort"
"strings"
"sync"
"unsafe"
@@ -307,3 +309,77 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
return errors.Combine(err1, err2, err3, err4)
}
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
interfaces, err := net.Interfaces()
if err != nil {
return nil, err
}
if fixedName != "" {
for _, iface := range interfaces {
if iface.Index != tunIndex && iface.Name == fixedName {
return &iface, nil
}
}
return nil, nil
}
var candidates []struct {
index int
score int
}
for i, iface := range interfaces {
if iface.Index == tunIndex {
continue
}
if strings.Contains(iface.Name, "vEthernet") {
continue
}
if iface.Flags&net.FlagUp == 0 {
continue
}
if iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil || len(addrs) == 0 {
continue
}
candidates = append(candidates, struct {
index int
score int
}{i, scoreWindowsInterface(&iface, addrs)})
}
sort.Slice(candidates, func(i, j int) bool {
if candidates[i].score != candidates[j].score {
return candidates[i].score > candidates[j].score
}
return interfaces[candidates[i].index].Name < interfaces[candidates[j].index].Name
})
if len(candidates) == 0 {
return nil, nil
}
iface := interfaces[candidates[0].index]
return &iface, nil
}
func scoreWindowsInterface(iface *net.Interface, addrs []net.Addr) int {
score := 0
name := strings.ToLower(iface.Name)
if strings.Contains(name, "wlan") || strings.Contains(name, "wi-fi") {
score += 2
}
for _, addr := range addrs {
if strings.HasPrefix(addr.String(), "192.168.") {
score++
break
}
}
return score
}
+59
View File
@@ -1 +1,60 @@
package wireguard
import (
"encoding/hex"
"net/netip"
"github.com/xtls/xray-core/common/protocol"
"google.golang.org/protobuf/proto"
)
func (p *PeerConfig) AsAccount() (protocol.Account, error) {
pub, err := ParseKey(p.PublicKey)
if err != nil {
return nil, err
}
allowedIPs := make([]netip.Prefix, 0, len(p.AllowedIps))
for i := range p.AllowedIps {
p, err := netip.ParsePrefix(p.AllowedIps[i])
if err != nil {
return nil, err
}
allowedIPs = append(allowedIPs, p)
}
return &MemoryAccount{
Pub: *pub,
AllowedIPs: allowedIPs,
PreSharedKey: p.PreSharedKey,
KeepAlive: p.KeepAlive,
}, nil
}
type MemoryAccount struct {
Pub [32]byte
AllowedIPs []netip.Prefix
PreSharedKey string
KeepAlive string
}
func (a *MemoryAccount) Equals(other protocol.Account) bool {
if b, ok := other.(*MemoryAccount); ok {
return a.Pub == b.Pub
}
return false
}
func (a *MemoryAccount) ToProto() proto.Message {
allowedIPs := make([]string, 0, len(a.AllowedIPs))
for i := range a.AllowedIPs {
allowedIPs = append(allowedIPs, a.AllowedIPs[i].String())
}
return &PeerConfig{
PublicKey: hex.EncodeToString(a.Pub[:]),
AllowedIps: allowedIPs,
PreSharedKey: a.PreSharedKey,
KeepAlive: a.KeepAlive,
}
}
+21 -9
View File
@@ -7,6 +7,7 @@
package wireguard
import (
protocol "github.com/xtls/xray-core/common/protocol"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
@@ -157,6 +158,7 @@ type DeviceConfig struct {
SecretKey string `protobuf:"bytes,1,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"`
Endpoint []string `protobuf:"bytes,2,rep,name=endpoint,proto3" json:"endpoint,omitempty"`
Peers []*PeerConfig `protobuf:"bytes,3,rep,name=peers,proto3" json:"peers,omitempty"`
Users []*protocol.User `protobuf:"bytes,5,rep,name=users,proto3" json:"users,omitempty"`
Mtu int32 `protobuf:"varint,4,opt,name=mtu,proto3" json:"mtu,omitempty"`
Reserved []byte `protobuf:"bytes,6,opt,name=reserved,proto3" json:"reserved,omitempty"`
DomainStrategy DeviceConfig_DomainStrategy `protobuf:"varint,7,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.proxy.wireguard.DeviceConfig_DomainStrategy" json:"domain_strategy,omitempty"`
@@ -217,6 +219,13 @@ func (x *DeviceConfig) GetPeers() []*PeerConfig {
return nil
}
func (x *DeviceConfig) GetUsers() []*protocol.User {
if x != nil {
return x.Users
}
return nil
}
func (x *DeviceConfig) GetMtu() int32 {
if x != nil {
return x.Mtu
@@ -256,7 +265,7 @@ var File_proxy_wireguard_config_proto protoreflect.FileDescriptor
const file_proxy_wireguard_config_proto_rawDesc = "" +
"\n" +
"\x1cproxy/wireguard/config.proto\x12\x14xray.proxy.wireguard\"\xad\x01\n" +
"\x1cproxy/wireguard/config.proto\x12\x14xray.proxy.wireguard\x1a\x1acommon/protocol/user.proto\"\xad\x01\n" +
"\n" +
"PeerConfig\x12\x1d\n" +
"\n" +
@@ -266,12 +275,13 @@ const file_proxy_wireguard_config_proto_rawDesc = "" +
"\n" +
"keep_alive\x18\x04 \x01(\tR\tkeepAlive\x12\x1f\n" +
"\vallowed_ips\x18\x05 \x03(\tR\n" +
"allowedIps\"\xaa\x03\n" +
"allowedIps\"\xdc\x03\n" +
"\fDeviceConfig\x12\x1d\n" +
"\n" +
"secret_key\x18\x01 \x01(\tR\tsecretKey\x12\x1a\n" +
"\bendpoint\x18\x02 \x03(\tR\bendpoint\x126\n" +
"\x05peers\x18\x03 \x03(\v2 .xray.proxy.wireguard.PeerConfigR\x05peers\x12\x10\n" +
"\x05peers\x18\x03 \x03(\v2 .xray.proxy.wireguard.PeerConfigR\x05peers\x120\n" +
"\x05users\x18\x05 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\x12\x10\n" +
"\x03mtu\x18\x04 \x01(\x05R\x03mtu\x12\x1a\n" +
"\breserved\x18\x06 \x01(\fR\breserved\x12Z\n" +
"\x0fdomain_strategy\x18\a \x01(\x0e21.xray.proxy.wireguard.DeviceConfig.DomainStrategyR\x0edomainStrategy\x12\x1b\n" +
@@ -305,15 +315,17 @@ var file_proxy_wireguard_config_proto_goTypes = []any{
(DeviceConfig_DomainStrategy)(0), // 0: xray.proxy.wireguard.DeviceConfig.DomainStrategy
(*PeerConfig)(nil), // 1: xray.proxy.wireguard.PeerConfig
(*DeviceConfig)(nil), // 2: xray.proxy.wireguard.DeviceConfig
(*protocol.User)(nil), // 3: xray.common.protocol.User
}
var file_proxy_wireguard_config_proto_depIdxs = []int32{
1, // 0: xray.proxy.wireguard.DeviceConfig.peers:type_name -> xray.proxy.wireguard.PeerConfig
0, // 1: xray.proxy.wireguard.DeviceConfig.domain_strategy:type_name -> xray.proxy.wireguard.DeviceConfig.DomainStrategy
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
3, // 1: xray.proxy.wireguard.DeviceConfig.users:type_name -> xray.common.protocol.User
0, // 2: xray.proxy.wireguard.DeviceConfig.domain_strategy:type_name -> xray.proxy.wireguard.DeviceConfig.DomainStrategy
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_proxy_wireguard_config_proto_init() }
+3
View File
@@ -6,6 +6,8 @@ option go_package = "github.com/xtls/xray-core/proxy/wireguard";
option java_package = "com.xray.proxy.wireguard";
option java_multiple_files = true;
import "common/protocol/user.proto";
message PeerConfig {
string public_key = 1;
string pre_shared_key = 2;
@@ -25,6 +27,7 @@ message DeviceConfig {
string secret_key = 1;
repeated string endpoint = 2;
repeated PeerConfig peers = 3;
repeated xray.common.protocol.User users = 5;
int32 mtu = 4;
bytes reserved = 6;
+156 -16
View File
@@ -2,16 +2,20 @@ package wireguard
import (
"context"
"encoding/hex"
"fmt"
"net/netip"
"reflect"
"strings"
"sync"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
c "github.com/xtls/xray-core/common/ctx"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/policy"
@@ -20,6 +24,7 @@ import (
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat"
"golang.org/x/crypto/curve25519"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun"
"gvisor.dev/gvisor/pkg/tcpip/stack"
@@ -42,6 +47,9 @@ type Server struct {
stack *stack.Stack
dev *device.Device
mu sync.Mutex
pub [32]byte
users *sync.Map
}
func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
@@ -72,15 +80,6 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
}
}
if len(conf.Peers) == 0 {
return nil, errors.New("empty peers")
}
for _, peer := range conf.Peers {
if peer.PublicKey == "" {
return nil, errors.New("peer without publickey")
}
}
localAddresses := make([]netip.Addr, 0, len(conf.Endpoint))
for _, localaddress := range conf.Endpoint {
addr, err := netip.ParseAddr(localaddress)
@@ -101,6 +100,19 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
return nil, err
}
pri := common.Must2(ParseKey(conf.SecretKey))
var pub [32]byte
curve25519.ScalarBaseMult(&pub, pri)
users := &sync.Map{}
for _, u := range conf.Users {
user, err := u.ToMemoryUser()
if err != nil {
return nil, err
}
users.Store(user.Account.(*MemoryAccount).Pub, user)
}
return &Server{
conf: conf,
ctx: core.ToBackgroundDetachedContext(ctx),
@@ -116,9 +128,100 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
tun: tun,
stack: stack,
pub: pub,
users: users,
}, nil
}
func (s *Server) AddUser(ctx context.Context, user *protocol.MemoryUser) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.dev == nil {
return errors.New("too early")
}
peer := user.Account.(*MemoryAccount)
if peer.Pub == s.pub {
return errors.New("invalid public key")
}
var sb strings.Builder
sb.WriteString("public_key=" + hex.EncodeToString(peer.Pub[:]) + "\n")
sb.WriteString("replace_allowed_ips=true\n")
for i := range peer.AllowedIPs {
sb.WriteString("allowed_ip=" + peer.AllowedIPs[i].String() + "\n")
}
if peer.PreSharedKey != "" {
sb.WriteString("preshared_key=" + peer.PreSharedKey + "\n")
}
if peer.KeepAlive != "" {
sb.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n")
}
err := s.dev.IpcSet(sb.String())
if err != nil {
return err
}
s.users.Store(peer.Pub, user)
return nil
}
func (s *Server) RemoveUser(ctx context.Context, email string) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.dev == nil {
return errors.New("too early")
}
if user := s.GetUser(ctx, email); user != nil {
peer := user.Account.(*MemoryAccount)
err := s.dev.IpcSet("public_key=" + hex.EncodeToString(peer.Pub[:]) + "\nremove=true\n")
if err != nil {
return err
}
s.users.Delete(peer.Pub)
}
return nil
}
func (s *Server) GetUser(ctx context.Context, email string) (user *protocol.MemoryUser) {
s.users.Range(func(key, value any) bool {
if value.(*protocol.MemoryUser).Email == email {
user = value.(*protocol.MemoryUser)
return false
}
return true
})
return
}
func (s *Server) GetUserByAddr(ctx context.Context, addr netip.Addr) (user *protocol.MemoryUser) {
s.users.Range(func(key, value any) bool {
peer := value.(*protocol.MemoryUser).Account.(*MemoryAccount)
for i := range peer.AllowedIPs {
if peer.AllowedIPs[i].Contains(addr) {
user = value.(*protocol.MemoryUser)
return false
}
}
return true
})
return
}
func (s *Server) GetUsers(ctx context.Context) (users []*protocol.MemoryUser) {
s.users.Range(func(key, value interface{}) bool {
users = append(users, value.(*protocol.MemoryUser))
return true
})
return
}
func (s *Server) GetUsersCount(context.Context) (count int64) {
s.users.Range(func(key, value interface{}) bool {
count++
return true
})
return
}
// Network implements proxy.Inbound.Network.
func (*Server) Network() []net.Network {
return []net.Network{}
@@ -196,18 +299,20 @@ func (s *Server) Start() error {
dev := device.NewDevice(s.tun, bind, logger)
var cfg strings.Builder
cfg.WriteString("private_key=" + s.conf.SecretKey + "\n")
for _, peer := range s.conf.Peers {
cfg.WriteString("public_key=" + peer.PublicKey + "\n")
s.users.Range(func(key, value any) bool {
peer := value.(*protocol.MemoryUser).Account.(*MemoryAccount)
cfg.WriteString("public_key=" + hex.EncodeToString(peer.Pub[:]) + "\n")
for i := range peer.AllowedIPs {
cfg.WriteString("allowed_ip=" + peer.AllowedIPs[i].String() + "\n")
}
if peer.PreSharedKey != "" {
cfg.WriteString("preshared_key=" + peer.PreSharedKey + "\n")
}
for _, ip := range peer.AllowedIps {
cfg.WriteString("allowed_ip=" + ip + "\n")
}
if peer.KeepAlive != "" {
cfg.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n")
}
}
return true
})
err := dev.IpcSet(cfg.String())
if err != nil {
return err
@@ -227,12 +332,36 @@ func (s *Server) HandleConnection(conn net.Conn, dest net.Destination) {
defer cancel()
ctx = c.ContextWithID(ctx, session.NewID())
source := net.DestinationFromAddr(conn.RemoteAddr())
remote := conn.RemoteAddr()
if remote == nil {
errors.LogError(context.Background(), "nil remote")
return
}
var addr netip.Addr
switch v := remote.(type) {
case *net.TCPAddr:
addr, _ = netip.AddrFromSlice(v.IP)
case *net.UDPAddr:
addr, _ = netip.AddrFromSlice(v.IP)
default:
errors.LogError(context.Background(), "invalid addr type ", reflect.TypeOf(v))
return
}
user := s.GetUserByAddr(context.TODO(), addr)
if user == nil {
errors.LogError(context.Background(), "nil user form ", remote, " to ", dest)
return
}
source := net.DestinationFromAddr(remote)
inbound := session.Inbound{
Name: "wireguard",
Tag: s.tag,
CanSpliceCopy: 3,
Source: source,
User: user,
}
ctx = session.ContextWithInbound(ctx, &inbound)
@@ -257,3 +386,14 @@ func (s *Server) HandleConnection(conn net.Conn, dest net.Destination) {
errors.LogError(ctx, errors.New("connection closed").Base(err))
}
}
func ParseKey(str string) (*[32]byte, error) {
slice, err := hex.DecodeString(str)
if err != nil {
return nil, err
}
if len(slice) != 32 {
return nil, errors.New("len(slice) != 32")
}
return (*[32]byte)(slice), nil
}
@@ -372,7 +372,6 @@ func runHysteria2Case(t *testing.T, bin string, mode trafficMode, payloadSize in
{
ProtocolName: "hysteria",
Settings: serial.ToTypedMessage(&hytransport.Config{
Version: 2,
Auth: auth,
UdpIdleTimeout: 60,
}),
@@ -421,7 +420,6 @@ func runHysteria2Case(t *testing.T, bin string, mode trafficMode, payloadSize in
Outbound: []*core.OutboundHandlerConfig{
{
ProxySettings: serial.ToTypedMessage(&hyproxy.ClientConfig{
Version: 2,
Server: &protocol.ServerEndpoint{
Address: xnet.NewIPOrDomain(xnet.LocalHostIP),
Port: uint32(relayPort),
@@ -437,7 +435,6 @@ func runHysteria2Case(t *testing.T, bin string, mode trafficMode, payloadSize in
{
ProtocolName: "hysteria",
Settings: serial.ToTypedMessage(&hytransport.Config{
Version: 2,
Auth: auth,
UdpIdleTimeout: 60,
}),
+3 -12
View File
@@ -23,7 +23,6 @@ const (
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
Auth string `protobuf:"bytes,2,opt,name=auth,proto3" json:"auth,omitempty"`
UdpIdleTimeout int64 `protobuf:"varint,3,opt,name=udp_idle_timeout,json=udpIdleTimeout,proto3" json:"udp_idle_timeout,omitempty"`
MasqType string `protobuf:"bytes,4,opt,name=masq_type,json=masqType,proto3" json:"masq_type,omitempty"`
@@ -68,13 +67,6 @@ func (*Config) Descriptor() ([]byte, []int) {
return file_transport_internet_hysteria_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetVersion() int32 {
if x != nil {
return x.Version
}
return 0
}
func (x *Config) GetAuth() string {
if x != nil {
return x.Auth
@@ -149,9 +141,8 @@ var File_transport_internet_hysteria_config_proto protoreflect.FileDescriptor
const file_transport_internet_hysteria_config_proto_rawDesc = "" +
"\n" +
"(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\xa3\x04\n" +
"\x06Config\x12\x18\n" +
"\aversion\x18\x01 \x01(\x05R\aversion\x12\x12\n" +
"(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\x8f\x04\n" +
"\x06Config\x12\x12\n" +
"\x04auth\x18\x02 \x01(\tR\x04auth\x12(\n" +
"\x10udp_idle_timeout\x18\x03 \x01(\x03R\x0eudpIdleTimeout\x12\x1b\n" +
"\tmasq_type\x18\x04 \x01(\tR\bmasqType\x12\x1b\n" +
@@ -166,7 +157,7 @@ const file_transport_internet_hysteria_config_proto_rawDesc = "" +
"\x17masq_string_status_code\x18\v \x01(\x05R\x14masqStringStatusCode\x1aD\n" +
"\x16MasqStringHeadersEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x82\x01\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x01\x10\x02B\x82\x01\n" +
"$com.xray.transport.internet.hysteriaP\x01Z5github.com/xtls/xray-core/transport/internet/hysteria\xaa\x02 Xray.Transport.Internet.Hysteriab\x06proto3"
var (
+1 -1
View File
@@ -7,7 +7,7 @@ option java_package = "com.xray.transport.internet.hysteria";
option java_multiple_files = true;
message Config {
int32 version = 1;
reserved 1;
string auth = 2;
int64 udp_idle_timeout = 3;
+1 -1
View File
@@ -58,7 +58,7 @@ func (h *httpHandler) AuthHTTP(w http.ResponseWriter, r *http.Request) bool {
var user *protocol.MemoryUser
var ok bool
if h.validator != nil && h.validator.GetCount() > 0 {
if h.validator != nil && h.validator.NotEmpty() {
user = h.validator.Get(auth)
} else if h.config.Auth != "" {
ok = auth == h.config.Auth
+1 -3
View File
@@ -584,9 +584,7 @@ func (ln *Listener) Addr() net.Addr {
// Close implements net.Listener.Close().
func (ln *Listener) Close() error {
if ln.h3server != nil {
if err := ln.h3server.Close(); err != nil {
return err
}
return ln.h3server.Close()
} else if ln.listener != nil {
return ln.listener.Close()
}
+36 -57
View File
@@ -6,27 +6,25 @@ package splithttp
import (
"container/heap"
"io"
"runtime"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/signal/done"
)
type Packet struct {
Reader io.ReadCloser
Reader *httpServerConn
Payload []byte
Seq uint64
}
type uploadQueue struct {
reader io.ReadCloser
nomore bool
pushedPackets chan Packet
writeCloseMutex sync.Mutex
heap uploadHeap
nextSeq uint64
closed bool
maxPackets int
reader atomic.Pointer[httpServerConn]
pushedPackets chan Packet
heap uploadHeap
nextSeq uint64
maxPackets int
closed *done.Instance
}
func NewUploadQueue(maxPackets int) *uploadQueue {
@@ -34,73 +32,53 @@ func NewUploadQueue(maxPackets int) *uploadQueue {
pushedPackets: make(chan Packet, maxPackets),
heap: uploadHeap{},
nextSeq: 0,
closed: false,
closed: done.New(),
maxPackets: maxPackets,
}
}
func (h *uploadQueue) Push(p Packet) error {
h.writeCloseMutex.Lock()
defer h.writeCloseMutex.Unlock()
if h.closed {
return errors.New("packet queue closed")
}
if h.nomore {
if h.reader.Load() != nil || (p.Reader != nil && !h.reader.CompareAndSwap(nil, p.Reader)) {
return errors.New("h.reader already exists")
}
if p.Reader != nil {
h.nomore = true
select {
case h.pushedPackets <- p: // no panic
if h.closed.Done() {
return errors.New("packet queue closed")
}
return nil
case <-h.closed.Wait():
return errors.New("packet queue closed")
}
h.pushedPackets <- p
return nil
}
func (h *uploadQueue) Close() error {
h.writeCloseMutex.Lock()
defer h.writeCloseMutex.Unlock()
if !h.closed {
h.closed = true
runtime.Gosched() // hope Read() gets the packet
f:
for {
select {
case p := <-h.pushedPackets:
if p.Reader != nil {
h.reader = p.Reader
}
default:
break f
}
}
close(h.pushedPackets)
}
if h.reader != nil {
return h.reader.Close()
h.closed.Close()
if reader := h.reader.Load(); reader != nil {
return reader.Close()
}
return nil
}
func (h *uploadQueue) Read(b []byte) (int, error) {
if h.reader != nil {
return h.reader.Read(b)
if reader := h.reader.Load(); reader != nil {
return reader.Read(b)
}
if h.closed {
if h.closed.Done() {
return 0, io.EOF
}
if len(h.heap) == 0 {
packet, more := <-h.pushedPackets
if !more {
select {
case p := <-h.pushedPackets:
if p.Reader != nil {
return p.Reader.Read(b)
}
heap.Push(&h.heap, p)
case <-h.closed.Wait():
return 0, io.EOF
}
if packet.Reader != nil {
h.reader = packet.Reader
return h.reader.Read(b)
}
heap.Push(&h.heap, packet)
}
for len(h.heap) > 0 {
@@ -131,11 +109,12 @@ func (h *uploadQueue) Read(b []byte) (int, error) {
return 0, errors.New("packet queue is too large")
}
heap.Push(&h.heap, packet)
packet2, more := <-h.pushedPackets
if !more {
select {
case p := <-h.pushedPackets:
heap.Push(&h.heap, p)
case <-h.closed.Wait():
return 0, io.EOF
}
heap.Push(&h.heap, packet2)
}
}