Compare commits

..

10 Commits

Author SHA1 Message Date
风扇滑翔翼
20825f6f1a Change to TypedSyncMap 2025-07-26 12:05:21 +00:00
RPRX
b6b51c51c8 v25.7.26
Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2025-07-26 09:07:56 +00:00
风扇滑翔翼
fb7a9d8d61 TLS client & server: Support Encrypted Client Hello (ECH) (#3813)
b9a72a4a26

---------

Co-authored-by: yuhan6665 <1588741+yuhan6665@users.noreply.github.com>
2025-07-26 08:47:27 +00:00
nobody
3fe02a658a Commands: Add adu/rmu inbound user management to API (#4943)
Co-authored-by: nobody <nobody@nowhere.mars>
2025-07-26 08:40:04 +00:00
RPRX
5f93ff6c3a REALITY config: mldsa65Seed and privateKey can not be the same value
Otherwise the point of using ML-DSA-65 is lost.
2025-07-26 08:36:58 +00:00
patterniha
10376f5b4d Freedom UDP: Fix some cone uses like STUN,... when address is domain (#4942)
https://github.com/XTLS/Xray-core/issues/2962#issuecomment-3120472154
2025-07-26 01:59:15 +00:00
风扇滑翔翼
1ea00fad81 UDP listener: Allow listening on "localhost" (#4940)
Fixes https://github.com/XTLS/Xray-core/issues/4939
2025-07-26 01:27:35 +00:00
RPRX
cfcf2a63d1 v25.7.25
Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2025-07-25 14:59:43 +00:00
Meow
66025f2889 Workflows: Fix github.ref_name sometimes is empty when building Docker images (#4937)
From https://github.com/XTLS/Xray-core/pull/4809
2025-07-25 14:47:12 +00:00
风扇滑翔翼
c9cd26d6d3 UDP: Remove removeRay()'s error log (#4936)
From https://github.com/XTLS/Xray-core/pull/4899
2025-07-25 14:40:26 +00:00
19 changed files with 379 additions and 130 deletions

View File

@@ -38,6 +38,9 @@ jobs:
if [[ -z "$SOURCE_TAG" ]]; then
SOURCE_TAG="${{ github.ref_name }}"
fi
if [[ -z "$SOURCE_TAG" ]]; then
SOURCE_TAG="${{ github.event.release.tag_name }}"
fi
if [[ -z "$SOURCE_TAG" ]]; then
echo "Error: Could not determine a valid tag source. Input tag and context tag (github.ref_name) are both empty."

View File

@@ -15,8 +15,8 @@ type TypedSyncMap[K, V any] struct {
// K is key type, V is value type
// It is recommended to use pointer types for V because sync.Map might return nil
// If sync.Map methods really returned nil, it will return the zero value of the type V
func NewTypedSyncMap[K any, V any]() *TypedSyncMap[K, V] {
return &TypedSyncMap[K, V]{
func NewTypedSyncMap[K any, V any]() TypedSyncMap[K, V] {
return TypedSyncMap[K, V]{
syncMap: &sync.Map{},
}
}

View File

@@ -19,7 +19,7 @@ import (
var (
Version_x byte = 25
Version_y byte = 7
Version_z byte = 24
Version_z byte = 26
)
var (

View File

@@ -413,7 +413,7 @@ type TLSConfig struct {
ServerNameToVerify string `json:"serverNameToVerify"`
VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"`
ECHConfigList string `json:"echConfigList"`
ECHSeverKeys string `json:"echSeverKeys"`
ECHServerKeys string `json:"echServerKeys"`
}
// Build implements Buildable.
@@ -487,12 +487,12 @@ func (c *TLSConfig) Build() (proto.Message, error) {
config.EchConfigList = c.ECHConfigList
if c.ECHSeverKeys != "" {
EchPrivateKey, err := base64.StdEncoding.DecodeString(c.ECHSeverKeys)
if c.ECHServerKeys != "" {
EchPrivateKey, err := base64.StdEncoding.DecodeString(c.ECHServerKeys)
if err != nil {
return nil, errors.New("invalid ECH Config", c.ECHSeverKeys)
return nil, errors.New("invalid ECH Config", c.ECHServerKeys)
}
config.EchSeverKeys = EchPrivateKey
config.EchServerKeys = EchPrivateKey
}
return config, nil
@@ -625,6 +625,9 @@ func (c *REALITYConfig) Build() (proto.Message, error) {
config.MaxTimeDiff = c.MaxTimeDiff
if c.Mldsa65Seed != "" {
if c.Mldsa65Seed == c.PrivateKey {
return nil, errors.New(`"mldsa65Seed" and "privateKey" can not be the same value: `, c.Mldsa65Seed)
}
if config.Mldsa65Seed, err = base64.RawURLEncoding.DecodeString(c.Mldsa65Seed); err != nil || len(config.Mldsa65Seed) != 32 {
return nil, errors.New(`invalid "mldsa65Seed": `, c.Mldsa65Seed)
}

View File

@@ -23,6 +23,8 @@ var CmdAPI = &base.Command{
cmdRemoveOutbounds,
cmdListInbounds,
cmdListOutbounds,
cmdAddInboundUsers,
cmdRemoveInboundUsers,
cmdInboundUser,
cmdInboundUserCount,
cmdAddRules,

View File

@@ -0,0 +1,144 @@
package api
import (
"context"
"fmt"
"github.com/xtls/xray-core/common/protocol"
handlerService "github.com/xtls/xray-core/app/proxyman/command"
cserial "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/infra/conf/serial"
"github.com/xtls/xray-core/proxy/shadowsocks"
"github.com/xtls/xray-core/proxy/shadowsocks_2022"
"github.com/xtls/xray-core/proxy/trojan"
vlessin "github.com/xtls/xray-core/proxy/vless/inbound"
vmessin "github.com/xtls/xray-core/proxy/vmess/inbound"
"github.com/xtls/xray-core/main/commands/base"
)
var cmdAddInboundUsers = &base.Command{
CustomFlags: true,
UsageLine: "{{.Exec}} api adu [--server=127.0.0.1:8080] <c1.json> [c2.json]...",
Short: "Add users to inbounds",
Long: `
Add users to inbounds.
Arguments:
-s, -server
The API server address. Default 127.0.0.1:8080
-t, -timeout
Timeout seconds to call API. Default 3
Example:
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json
`,
Run: executeAddInboundUsers,
}
func executeAddInboundUsers(cmd *base.Command, args []string) {
setSharedFlags(cmd)
cmd.Flag.Parse(args)
unnamedArgs := cmd.Flag.Args()
inbs := extractInboundsConfig(unnamedArgs)
conn, ctx, close := dialAPIServer()
defer close()
client := handlerService.NewHandlerServiceClient(conn)
success := 0
for _, inb := range inbs {
success += executeInboundUserAction(ctx, client, inb, addInboundUserAction)
}
fmt.Println("Added", success, "user(s) in total.")
}
func addInboundUserAction(ctx context.Context, client handlerService.HandlerServiceClient, tag string, user *protocol.User) error {
fmt.Println("add user:", user.Email)
_, err := client.AlterInbound(ctx, &handlerService.AlterInboundRequest{
Tag: tag,
Operation: cserial.ToTypedMessage(
&handlerService.AddUserOperation{
User: user,
}),
})
return err
}
func extractInboundUsers(inb *core.InboundHandlerConfig) []*protocol.User {
if inb == nil {
return nil
}
inst, err := inb.ProxySettings.GetInstance()
if err != nil || inst == nil {
fmt.Println("failed to get inbound instance:", err)
return nil
}
switch ty := inst.(type) {
case *vmessin.Config:
return ty.User
case *vlessin.Config:
return ty.Clients
case *trojan.ServerConfig:
return ty.Users
case *shadowsocks.ServerConfig:
return ty.Users
case *shadowsocks_2022.MultiUserServerConfig:
return ty.Users
default:
fmt.Println("unsupported inbound type")
}
return nil
}
func extractInboundsConfig(unnamedArgs []string) []conf.InboundDetourConfig {
ins := make([]conf.InboundDetourConfig, 0)
for _, arg := range unnamedArgs {
r, err := loadArg(arg)
if err != nil {
base.Fatalf("failed to load %s: %s", arg, err)
}
conf, err := serial.DecodeJSONConfig(r)
if err != nil {
base.Fatalf("failed to decode %s: %s", arg, err)
}
ins = append(ins, conf.InboundConfigs...)
}
return ins
}
func executeInboundUserAction(ctx context.Context, client handlerService.HandlerServiceClient, inb conf.InboundDetourConfig, action func(ctx context.Context, client handlerService.HandlerServiceClient, tag string, user *protocol.User) error) int {
success := 0
tag := inb.Tag
if len(tag) < 1 {
return success
}
fmt.Println("processing inbound:", tag)
built, err := inb.Build()
if err != nil {
fmt.Println("failed to build config:", err)
return success
}
users := extractInboundUsers(built)
if users == nil {
return success
}
for _, user := range users {
if len(user.Email) < 1 {
continue
}
if err := action(ctx, client, inb.Tag, user); err == nil {
fmt.Println("result: ok")
success += 1
} else {
fmt.Println(err)
}
}
return success
}

View File

@@ -0,0 +1,62 @@
package api
import (
"fmt"
handlerService "github.com/xtls/xray-core/app/proxyman/command"
cserial "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/main/commands/base"
)
var cmdRemoveInboundUsers = &base.Command{
CustomFlags: true,
UsageLine: "{{.Exec}} api rmu [--server=127.0.0.1:8080] -tag=tag <email1> [email2]...",
Short: "Remove users from inbounds",
Long: `
Remove users from inbounds.
Arguments:
-s, -server
The API server address. Default 127.0.0.1:8080
-t, -timeout
Timeout seconds to call API. Default 3
-tag
Inbound tag
Example:
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -tag="vless-in" "xray@love.com" ...
`,
Run: executeRemoveUsers,
}
func executeRemoveUsers(cmd *base.Command, args []string) {
setSharedFlags(cmd)
var tag string
cmd.Flag.StringVar(&tag, "tag", "", "")
cmd.Flag.Parse(args)
emails := cmd.Flag.Args()
if len(tag) < 1 {
base.Fatalf("inbound tag not specified")
}
conn, ctx, close := dialAPIServer()
defer close()
client := handlerService.NewHandlerServiceClient(conn)
success := 0
for _, email := range emails {
fmt.Println("remove user:", email)
_, err := client.AlterInbound(ctx, &handlerService.AlterInboundRequest{
Tag: tag,
Operation: cserial.ToTypedMessage(
&handlerService.RemoveUserOperation{
Email: email,
}),
})
if err == nil {
success += 1
} else {
fmt.Println(err)
}
}
fmt.Println("Removed", success, "user(s) in total.")
}

View File

@@ -13,14 +13,14 @@ import (
)
var cmdECH = &base.Command{
UsageLine: `{{.Exec}} tls ech [--serverName (string)] [--pem] [-i "ECHSeverKeys (base64.StdEncoding)"]`,
UsageLine: `{{.Exec}} tls ech [--serverName (string)] [--pem] [-i "ECHServerKeys (base64.StdEncoding)"]`,
Short: `Generate TLS-ECH certificates`,
Long: `
Generate TLS-ECH certificates.
Set serverName to your custom string: {{.Exec}} tls ech --serverName (string)
Generate into pem format: {{.Exec}} tls ech --pem
Restore ECHConfigs from ECHSeverKeys: {{.Exec}} tls ech -i "ECHSeverKeys (base64.StdEncoding)"
Restore ECHConfigs from ECHServerKeys: {{.Exec}} tls ech -i "ECHServerKeys (base64.StdEncoding)"
`, // Enable PQ signature schemes: {{.Exec}} tls ech --pq-signature-schemes-enabled
}
@@ -28,7 +28,7 @@ func init() {
cmdECH.Run = executeECH
}
var input_echSeverKeys = cmdECH.Flag.String("i", "", "ECHSeverKeys (base64.StdEncoding)")
var input_echServerKeys = cmdECH.Flag.String("i", "", "ECHServerKeys (base64.StdEncoding)")
// var input_pqSignatureSchemesEnabled = cmdECH.Flag.Bool("pqSignatureSchemesEnabled", false, "")
var input_serverName = cmdECH.Flag.String("serverName", "cloudflare-ech.com", "")
@@ -47,7 +47,7 @@ func executeECH(cmd *base.Command, args []string) {
common.Must(err)
var configBuffer, keyBuffer []byte
if *input_echSeverKeys == "" {
if *input_echServerKeys == "" {
configBytes, _ := tls.MarshalBinary(echConfig)
var b cryptobyte.Builder
b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) {
@@ -61,15 +61,15 @@ func executeECH(cmd *base.Command, args []string) {
b2.AddBytes(configBytes)
keyBuffer, _ = b2.Bytes()
} else {
keySetsByte, err := base64.StdEncoding.DecodeString(*input_echSeverKeys)
keySetsByte, err := base64.StdEncoding.DecodeString(*input_echServerKeys)
if err != nil {
os.Stdout.WriteString("Failed to decode ECHSeverKeys: " + err.Error() + "\n")
os.Stdout.WriteString("Failed to decode ECHServerKeys: " + err.Error() + "\n")
return
}
keyBuffer = keySetsByte
KeySets, err := tls.ConvertToGoECHKeys(keySetsByte)
if err != nil {
os.Stdout.WriteString("Failed to decode ECHSeverKeys: " + err.Error() + "\n")
os.Stdout.WriteString("Failed to decode ECHServerKeys: " + err.Error() + "\n")
return
}
var b cryptobyte.Builder
@@ -88,6 +88,6 @@ func executeECH(cmd *base.Command, args []string) {
os.Stdout.WriteString(keyPEM)
} else {
os.Stdout.WriteString("ECH config list: \n" + base64.StdEncoding.EncodeToString(configBuffer) + "\n")
os.Stdout.WriteString("ECH Key sets: \n" + base64.StdEncoding.EncodeToString(keyBuffer) + "\n")
os.Stdout.WriteString("ECH server keys: \n" + base64.StdEncoding.EncodeToString(keyBuffer) + "\n")
}
}

View File

@@ -285,14 +285,18 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De
counter = statConn.ReadCounter
}
if c, ok := iConn.(*internet.PacketConnWrapper); ok {
isAddrChanged := false
if UDPOverride.Address != nil || UDPOverride.Port != 0 || DialDest.Address.Family().IsDomain() {
isAddrChanged = true
isOverridden := false
if UDPOverride.Address != nil || UDPOverride.Port != 0 {
isOverridden = true
}
changedAddress, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
return &PacketReader{
PacketConnWrapper: c,
Counter: counter,
IsAddrChanged: isAddrChanged,
IsOverridden: isOverridden,
InitUnchangedAddr: DialDest.Address,
InitChangedAddr: net.ParseAddress(changedAddress),
}
}
return &buf.PacketReader{Reader: conn}
@@ -301,7 +305,9 @@ func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.De
type PacketReader struct {
*internet.PacketConnWrapper
stats.Counter
IsAddrChanged bool
IsOverridden bool
InitUnchangedAddr net.Address
InitChangedAddr net.Address
}
func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
@@ -315,9 +321,13 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
b.Resize(0, int32(n))
// if udp dest addr is changed, we are unable to get the correct src addr
// so we don't attach src info to udp packet, break cone behavior, assuming the dial dest is the expected scr addr
if !r.IsAddrChanged {
if !r.IsOverridden {
address := net.IPAddress(d.(*net.UDPAddr).IP)
if r.InitChangedAddr == address {
address = r.InitUnchangedAddr
}
b.UDP = &net.Destination{
Address: net.IPAddress(d.(*net.UDPAddr).IP),
Address: address,
Port: net.Port(d.(*net.UDPAddr).Port),
Network: net.Network_UDP,
}
@@ -353,7 +363,7 @@ func NewPacketWriter(conn net.Conn, h *Handler, ctx context.Context, UDPOverride
Handler: h,
Context: ctx,
UDPOverride: UDPOverride,
resolvedUDPAddr: resolvedUDPAddr,
resolvedUDPAddr: &resolvedUDPAddr,
}
}

View File

@@ -2,17 +2,17 @@ package trojan
import (
"strings"
"sync"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/utils"
)
// Validator stores valid trojan users.
type Validator struct {
// Considering email's usage here, map + sync.Mutex/RWMutex may have better performance.
email sync.Map
users sync.Map
email utils.TypedSyncMap[string, *protocol.MemoryUser]
users utils.TypedSyncMap[string, *protocol.MemoryUser]
}
// Add a trojan user, Email must be empty or unique.
@@ -38,7 +38,7 @@ func (v *Validator) Del(e string) error {
return errors.New("User ", e, " not found.")
}
v.email.Delete(le)
v.users.Delete(hexString(u.(*protocol.MemoryUser).Account.(*MemoryAccount).Key))
v.users.Delete(hexString(u.Account.(*MemoryAccount).Key))
return nil
}
@@ -46,7 +46,7 @@ func (v *Validator) Del(e string) error {
func (v *Validator) Get(hash string) *protocol.MemoryUser {
u, _ := v.users.Load(hash)
if u != nil {
return u.(*protocol.MemoryUser)
return u
}
return nil
}
@@ -56,7 +56,7 @@ func (v *Validator) GetByEmail(email string) *protocol.MemoryUser {
email = strings.ToLower(email)
u, _ := v.email.Load(email)
if u != nil {
return u.(*protocol.MemoryUser)
return u
}
return nil
}
@@ -64,8 +64,8 @@ func (v *Validator) GetByEmail(email string) *protocol.MemoryUser {
// Get all users
func (v *Validator) GetAll() []*protocol.MemoryUser {
var u = make([]*protocol.MemoryUser, 0, 100)
v.email.Range(func(key, value interface{}) bool {
u = append(u, value.(*protocol.MemoryUser))
v.email.Range(func(key string, value *protocol.MemoryUser) bool {
u = append(u, value)
return true
})
return u
@@ -74,7 +74,7 @@ func (v *Validator) GetAll() []*protocol.MemoryUser {
// Get users count
func (v *Validator) GetCount() int64 {
var c int64 = 0
v.email.Range(func(key, value interface{}) bool {
v.email.Range(func(key string, value *protocol.MemoryUser) bool {
c++
return true
})

View File

@@ -2,10 +2,10 @@ package vless
import (
"strings"
"sync"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/common/uuid"
)
@@ -21,8 +21,8 @@ type Validator interface {
// MemoryValidator stores valid VLESS users.
type MemoryValidator struct {
// Considering email's usage here, map + sync.Mutex/RWMutex may have better performance.
email sync.Map
users sync.Map
email utils.TypedSyncMap[string, *protocol.MemoryUser]
users utils.TypedSyncMap[uuid.UUID, *protocol.MemoryUser]
}
// Add a VLESS user, Email must be empty or unique.
@@ -48,7 +48,7 @@ func (v *MemoryValidator) Del(e string) error {
return errors.New("User ", e, " not found.")
}
v.email.Delete(le)
v.users.Delete(u.(*protocol.MemoryUser).Account.(*MemoryAccount).ID.UUID())
v.users.Delete(u.Account.(*MemoryAccount).ID.UUID())
return nil
}
@@ -56,7 +56,7 @@ func (v *MemoryValidator) Del(e string) error {
func (v *MemoryValidator) Get(id uuid.UUID) *protocol.MemoryUser {
u, _ := v.users.Load(id)
if u != nil {
return u.(*protocol.MemoryUser)
return u
}
return nil
}
@@ -66,7 +66,7 @@ func (v *MemoryValidator) GetByEmail(email string) *protocol.MemoryUser {
email = strings.ToLower(email)
u, _ := v.email.Load(email)
if u != nil {
return u.(*protocol.MemoryUser)
return u
}
return nil
}
@@ -74,8 +74,8 @@ func (v *MemoryValidator) GetByEmail(email string) *protocol.MemoryUser {
// Get all users
func (v *MemoryValidator) GetAll() []*protocol.MemoryUser {
var u = make([]*protocol.MemoryUser, 0, 100)
v.email.Range(func(key, value interface{}) bool {
u = append(u, value.(*protocol.MemoryUser))
v.email.Range(func(key string, value *protocol.MemoryUser) bool {
u = append(u, value)
return true
})
return u
@@ -84,7 +84,7 @@ func (v *MemoryValidator) GetAll() []*protocol.MemoryUser {
// Get users count
func (v *MemoryValidator) GetCount() int64 {
var c int64 = 0
v.email.Range(func(key, value interface{}) bool {
v.email.Range(func(key string, value *protocol.MemoryUser) bool {
c++
return true
})

View File

@@ -43,7 +43,7 @@ type dialerConf struct {
}
var (
globalDialerMap map[dialerConf]*grpc.ClientConn
globalDialerMap = make(map[dialerConf]*grpc.ClientConn)
globalDialerAccess sync.Mutex
)
@@ -77,9 +77,6 @@ func getGrpcClient(ctx context.Context, dest net.Destination, streamSettings *in
globalDialerAccess.Lock()
defer globalDialerAccess.Unlock()
if globalDialerMap == nil {
globalDialerMap = make(map[dialerConf]*grpc.ClientConn)
}
tlsConfig := tls.ConfigFromStreamSettings(streamSettings)
realityConfig := reality.ConfigFromStreamSettings(streamSettings)
sockopt := streamSettings.SocketSettings

View File

@@ -20,6 +20,7 @@ import (
"github.com/xtls/xray-core/common/net"
http_proto "github.com/xtls/xray-core/common/protocol/http"
"github.com/xtls/xray-core/common/signal/done"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/reality"
"github.com/xtls/xray-core/transport/internet/stat"
@@ -32,7 +33,7 @@ type requestHandler struct {
path string
ln *Listener
sessionMu *sync.Mutex
sessions sync.Map
sessions utils.TypedSyncMap[string, *httpSession]
localAddr net.Addr
}
@@ -47,18 +48,18 @@ type httpSession struct {
func (h *requestHandler) upsertSession(sessionId string) *httpSession {
// fast path
currentSessionAny, ok := h.sessions.Load(sessionId)
currentSession, ok := h.sessions.Load(sessionId)
if ok {
return currentSessionAny.(*httpSession)
return currentSession
}
// slow path
h.sessionMu.Lock()
defer h.sessionMu.Unlock()
currentSessionAny, ok = h.sessions.Load(sessionId)
currentSession, ok = h.sessions.Load(sessionId)
if ok {
return currentSessionAny.(*httpSession)
return currentSession
}
s := &httpSession{
@@ -361,7 +362,7 @@ func ListenXH(ctx context.Context, address net.Address, port net.Port, streamSet
path: l.config.GetNormalizedPath(),
ln: l,
sessionMu: &sync.Mutex{},
sessions: sync.Map{},
sessions: utils.NewTypedSyncMap[string, *httpSession](),
}
tlsConfig := getTLSConfig(streamSettings)
l.isH3 = len(tlsConfig.NextProtos) == 1 && tlsConfig.NextProtos[0] == "h3"

View File

@@ -444,7 +444,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
config.KeyLogWriter = writer
}
}
if len(c.EchConfigList) > 0 || len(c.EchSeverKeys) > 0 {
if len(c.EchConfigList) > 0 || len(c.EchServerKeys) > 0 {
err := ApplyECH(c, config)
if err != nil {
errors.LogError(context.Background(), err)

View File

@@ -218,7 +218,7 @@ type Config struct {
// @Critical
VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"`
EchConfigList string `protobuf:"bytes,18,opt,name=ech_config_list,json=echConfigList,proto3" json:"ech_config_list,omitempty"`
EchSeverKeys []byte `protobuf:"bytes,19,opt,name=ech_sever_keys,json=echSeverKeys,proto3" json:"ech_sever_keys,omitempty"`
EchServerKeys []byte `protobuf:"bytes,19,opt,name=ech_server_keys,json=echServerKeys,proto3" json:"ech_server_keys,omitempty"`
}
func (x *Config) Reset() {
@@ -370,9 +370,9 @@ func (x *Config) GetEchConfigList() string {
return ""
}
func (x *Config) GetEchSeverKeys() []byte {
func (x *Config) GetEchServerKeys() []byte {
if x != nil {
return x.EchSeverKeys
return x.EchServerKeys
}
return nil
}
@@ -408,7 +408,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
0x4e, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a,
0x10, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46,
0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59,
0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xe8, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e,
0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xea, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73,
0x65, 0x63, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c,
0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x65,
@@ -460,17 +460,18 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x49, 0x6e,
0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x24, 0x0a,
0x0e, 0x65, 0x63, 0x68, 0x5f, 0x73, 0x65, 0x76, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18,
0x13, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x65, 0x63, 0x68, 0x53, 0x65, 0x76, 0x65, 0x72, 0x4b,
0x65, 0x79, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e,
0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63,
0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e,
0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61,
0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65,
0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x26, 0x0a,
0x0f, 0x65, 0x63, 0x68, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x73,
0x18, 0x13, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x65, 0x63, 0x68, 0x53, 0x65, 0x72, 0x76, 0x65,
0x72, 0x4b, 0x65, 0x79, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61,
0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79,
0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f,
0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58,
0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e,
0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
}
var (

View File

@@ -94,5 +94,5 @@ message Config {
string ech_config_list = 18;
bytes ech_sever_keys = 19;
bytes ech_server_keys = 19;
}

View File

@@ -20,6 +20,7 @@ import (
"github.com/xtls/reality/hpke"
"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/transport/internet"
"golang.org/x/crypto/cryptobyte"
)
@@ -65,8 +66,8 @@ func ApplyECH(c *Config, config *tls.Config) error {
}
// for server
if len(c.EchSeverKeys) != 0 {
KeySets, err := ConvertToGoECHKeys(c.EchSeverKeys)
if len(c.EchServerKeys) != 0 {
KeySets, err := ConvertToGoECHKeys(c.EchServerKeys)
if err != nil {
return errors.New("Failed to unmarshal ECHKeySetList: ", err)
}
@@ -77,19 +78,34 @@ func ApplyECH(c *Config, config *tls.Config) error {
}
type ECHConfigCache struct {
echConfig atomic.Pointer[[]byte]
expire atomic.Pointer[time.Time]
configRecord atomic.Pointer[echConfigRecord]
// updateLock is not for preventing concurrent read/write, but for preventing concurrent update
updateLock sync.Mutex
UpdateLock sync.Mutex
}
func (c *ECHConfigCache) update(domain string, server string) ([]byte, error) {
c.updateLock.Lock()
defer c.updateLock.Unlock()
type echConfigRecord struct {
config []byte
expire time.Time
}
var (
GlobalECHConfigCache = utils.NewTypedSyncMap[string, *ECHConfigCache]()
clientForECHDOH = utils.NewTypedSyncMap[string, *http.Client]()
)
// Update updates the ECH config for given domain and server.
// this method is concurrent safe, only one update request will be sent, others get the cache.
// if isLockedUpdate is true, it will not try to acquire the lock.
func (c *ECHConfigCache) Update(domain string, server string, isLockedUpdate bool) ([]byte, error) {
if !isLockedUpdate {
c.UpdateLock.Lock()
defer c.UpdateLock.Unlock()
}
// Double check cache after acquiring lock
if c.expire.Load().After(time.Now()) {
configRecord := c.configRecord.Load()
if configRecord.expire.After(time.Now()) {
errors.LogDebug(context.Background(), "Cache hit for domain after double check: ", domain)
return *c.echConfig.Load(), nil
return configRecord.config, nil
}
// Query ECH config from DNS server
errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server)
@@ -97,49 +113,43 @@ func (c *ECHConfigCache) update(domain string, server string) ([]byte, error) {
if err != nil {
return nil, err
}
c.echConfig.Store(&echConfig)
expire := time.Now().Add(time.Duration(ttl) * time.Second)
c.expire.Store(&expire)
return *c.echConfig.Load(), nil
configRecord = &echConfigRecord{
config: echConfig,
expire: time.Now().Add(time.Duration(ttl) * time.Second),
}
c.configRecord.Store(configRecord)
return configRecord.config, nil
}
var (
GlobalECHConfigCache map[string]*ECHConfigCache
GlobalECHConfigCacheAccess sync.Mutex
)
// QueryRecord returns the ECH config for given domain.
// If the record is not in cache or expired, it will query the DNS server and update the cache.
func QueryRecord(domain string, server string) ([]byte, error) {
// Global cache init
GlobalECHConfigCacheAccess.Lock()
if GlobalECHConfigCache == nil {
GlobalECHConfigCache = make(map[string]*ECHConfigCache)
}
echConfigCache := GlobalECHConfigCache[domain]
if echConfigCache == nil {
echConfigCache, ok := GlobalECHConfigCache.Load(domain)
if !ok {
echConfigCache = &ECHConfigCache{}
echConfigCache.expire.Store(&time.Time{}) // zero value means initial state
GlobalECHConfigCache[domain] = echConfigCache
echConfigCache.configRecord.Store(&echConfigRecord{})
echConfigCache, _ = GlobalECHConfigCache.LoadOrStore(domain, echConfigCache)
}
if echConfigCache != nil && echConfigCache.expire.Load().After(time.Now()) {
configRecord := echConfigCache.configRecord.Load()
if configRecord.expire.After(time.Now()) {
errors.LogDebug(context.Background(), "Cache hit for domain: ", domain)
GlobalECHConfigCacheAccess.Unlock()
return *echConfigCache.echConfig.Load(), nil
return configRecord.config, nil
}
GlobalECHConfigCacheAccess.Unlock()
// If expire is zero value, it means we are in initial state, wait for the query to finish
// otherwise return old value immediately and update in a goroutine
if *echConfigCache.expire.Load() == (time.Time{}) {
return echConfigCache.update(domain, server)
// but if the cache is too old, wait for update
if configRecord.expire == (time.Time{}) || configRecord.expire.Add(time.Hour*6).Before(time.Now()) {
return echConfigCache.Update(domain, server, false)
} else {
// If someone already acquired the lock, it means it is updating, do not start another update goroutine
if echConfigCache.updateLock.TryLock() {
go echConfigCache.update(domain, server)
if echConfigCache.UpdateLock.TryLock() {
go func() {
defer echConfigCache.UpdateLock.Unlock()
echConfigCache.Update(domain, server, true)
}()
}
return *echConfigCache.echConfig.Load(), nil
return configRecord.config, nil
}
}
@@ -157,26 +167,30 @@ func dnsQuery(server string, domain string) ([]byte, uint32, error) {
if err != nil {
return []byte{}, 0, err
}
// All traffic sent by core should via xray's internet.DialSystem
// This involves the behavior of some Android VPN GUI clients
tr := &http.Transport{
IdleConnTimeout: 90 * time.Second,
ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dest, err := net.ParseDestination(network + ":" + addr)
if err != nil {
return nil, err
}
conn, err := internet.DialSystem(ctx, dest, nil)
if err != nil {
return nil, err
}
return conn, nil
},
}
client := &http.Client{
Timeout: 5 * time.Second,
Transport: tr,
var client *http.Client
if client, _ = clientForECHDOH.Load(server); client == nil {
// All traffic sent by core should via xray's internet.DialSystem
// This involves the behavior of some Android VPN GUI clients
tr := &http.Transport{
IdleConnTimeout: 90 * time.Second,
ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dest, err := net.ParseDestination(network + ":" + addr)
if err != nil {
return nil, err
}
conn, err := internet.DialSystem(ctx, dest, nil)
if err != nil {
return nil, err
}
return conn, nil
},
}
c := &http.Client{
Timeout: 5 * time.Second,
Transport: tr,
}
client, _ = clientForECHDOH.LoadOrStore(server, c)
}
req, err := http.NewRequest("POST", server, bytes.NewReader(msg))
if err != nil {
@@ -210,7 +224,12 @@ func dnsQuery(server string, domain string) ([]byte, uint32, error) {
defer cancel()
// use xray's internet.DialSystem as mentioned above
conn, err := internet.DialSystem(dnsTimeoutCtx, dest, nil)
defer conn.Close()
defer func() {
err := conn.Close()
if err != nil {
errors.LogDebug(context.Background(), "Failed to close connection: ", err)
}
}()
if err != nil {
return []byte{}, 0, err
}

View File

@@ -70,11 +70,10 @@ func (v *Dispatcher) getInboundRay(ctx context.Context, dest net.Destination) (*
removeRay := func() {
v.Lock()
defer v.Unlock()
// sometimes the entry is already removed by others, don't close again
if entry == v.conn {
cancel()
v.removeRay()
} else {
errors.LogError(ctx, "removeRay trying to remove a conn that not belongs to it, canceling.")
}
}
timer := signal.CancelAfterInactivity(ctx, removeRay, time.Minute)

View File

@@ -40,6 +40,14 @@ func ListenUDP(ctx context.Context, address net.Address, port net.Port, streamSe
opt(hub)
}
if address.Family().IsDomain() && address.Domain() == "localhost" {
address = net.LocalHostIP
}
if address.Family().IsDomain() {
return nil, errors.New("domain address is not allowed for listening: ", address.Domain())
}
var sockopt *internet.SocketConfig
if streamSettings != nil {
sockopt = streamSettings.SocketSettings