Compare commits

...

6 Commits

Author SHA1 Message Date
MHSanaei
df163854bd v2.9.1 2026-04-21 15:02:06 +02:00
MHSanaei
1af795fad8 kcp : default value maxSendingWindow
MaxSendingWindow must be >= Mtu
2026-04-21 15:00:12 +02:00
MHSanaei
085cb8c216 Set CWND multiplier default and min to 1 2026-04-21 14:50:37 +02:00
MHSanaei
2a9ba2badc salamander obfs and remove auth field 2026-04-21 14:13:55 +02:00
MHSanaei
53fb4fe8f9 fix: prevent AddUser panic on nil flow for VLESS XHTTP clients
#4056
2026-04-21 13:04:39 +02:00
MHSanaei
8d512d55e5 revert Fix geosite:ru rule (ram leak)
#4050 #4055
2026-04-21 12:55:16 +02:00
11 changed files with 100 additions and 111 deletions

View File

@@ -1 +1 @@
2.9.0
2.9.1

4
go.mod
View File

@@ -27,7 +27,6 @@ require (
golang.org/x/sys v0.43.0
golang.org/x/text v0.36.0
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
)
@@ -96,7 +95,8 @@ require (
golang.org/x/tools v0.44.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
)

4
go.sum
View File

@@ -256,8 +256,8 @@ golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+Z
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -323,8 +323,8 @@ class KcpStreamSettings extends XrayCommonClass {
tti = 20,
uplinkCapacity = 5,
downlinkCapacity = 20,
cwndMultiplier = 0,
maxSendingWindow = 0,
cwndMultiplier = 1,
maxSendingWindow = 1350,
) {
super();
this.mtu = mtu;
@@ -1830,6 +1830,16 @@ class Inbound extends XrayCommonClass {
if (this.stream.tls.settings.echConfigList?.length > 0) params.set("ech", this.stream.tls.settings.echConfigList.join(','));
if (this.stream.tls.sni?.length > 0) params.set("sni", this.stream.tls.sni);
const udpMasks = this.stream?.finalmask?.udp;
if (Array.isArray(udpMasks)) {
const salamanderMask = udpMasks.find(mask => mask?.type === 'salamander');
const obfsPassword = salamanderMask?.settings?.password;
if (typeof obfsPassword === 'string' && obfsPassword.length > 0) {
params.set("obfs", "salamander");
params.set("obfs-password", obfsPassword);
}
}
const url = new URL(link);
for (const [key, value] of params) {
url.searchParams.set(key, value);

View File

@@ -169,8 +169,8 @@ class KcpStreamSettings extends CommonClass {
tti = 20,
uplinkCapacity = 5,
downlinkCapacity = 20,
cwndMultiplier = 0,
maxSendingWindow = 0,
cwndMultiplier = 1,
maxSendingWindow = 1350,
) {
super();
this.mtu = mtu;

View File

@@ -587,7 +587,7 @@
<a-form-item label="CWND Multiplier">
<a-input-number
v-model.number="outbound.stream.kcp.cwndMultiplier"
min="0"
min="1"
></a-input-number>
</a-form-item>
<a-form-item label="Max Sending Window">

View File

@@ -4,21 +4,6 @@
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
Auth Password
<a-icon
@click="inbound.stream.hysteria.auth = RandomUtil.randomSeq(10)"
type="sync"
></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.stream.hysteria.auth"></a-input>
</a-form-item>
<a-form-item label="UDP Idle Timeout">
<a-input-number
v-model.number="inbound.stream.hysteria.udpIdleTimeout"

View File

@@ -33,7 +33,7 @@
<a-form-item label="CWND Multiplier">
<a-input-number
v-model.number="inbound.stream.kcp.cwndMultiplier"
:min="0"
:min="1"
></a-input-number>
</a-form-item>
<a-form-item label="Max Sending Window">

View File

@@ -34,8 +34,6 @@ import (
"github.com/shirou/gopsutil/v4/load"
"github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/net"
"github.com/xtls/xray-core/app/router"
"google.golang.org/protobuf/proto"
)
// ProcessState represents the current state of a system process.
@@ -1057,48 +1055,6 @@ func (s *ServerService) IsValidGeofileName(filename string) bool {
return matched
}
// NormalizeGeositeCountryCodes reads a geosite .dat file, uppercases all
// country_code fields, and writes it back. This works around a case-sensitivity
// mismatch in Xray-core: the router normalizes codes to uppercase before lookup,
// but the find() function compares bytes case-sensitively. Some geosite.dat
// providers (e.g. Loyalsoldier) store codes in lowercase, causing lookup failures.
func NormalizeGeositeCountryCodes(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read geosite file %s: %w", path, err)
}
var list router.GeoSiteList
if err := proto.Unmarshal(data, &list); err != nil {
return fmt.Errorf("failed to parse geosite file %s: %w", path, err)
}
changed := false
for _, entry := range list.Entry {
upper := strings.ToUpper(entry.CountryCode)
if entry.CountryCode != upper {
entry.CountryCode = upper
changed = true
}
}
if !changed {
return nil
}
normalized, err := proto.Marshal(&list)
if err != nil {
return fmt.Errorf("failed to serialize normalized geosite file %s: %w", path, err)
}
if err := os.WriteFile(path, normalized, 0o644); err != nil {
return fmt.Errorf("failed to write normalized geosite file %s: %w", path, err)
}
logger.Infof("Normalized country codes to uppercase in %s (%d entries)", path, len(list.Entry))
return nil
}
func (s *ServerService) UpdateGeofile(fileName string) error {
type geofileEntry struct {
URL string
@@ -1190,22 +1146,12 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
var errorMessages []string
normalizeIfGeosite := func(destPath, name string) {
if strings.Contains(name, "geosite") {
if err := NormalizeGeositeCountryCodes(destPath); err != nil {
logger.Warningf("Failed to normalize geosite country codes in %s: %v", name, err)
}
}
}
if fileName == "" {
// Download all geofiles
for _, entry := range geofileAllowlist {
destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
if err := downloadFile(entry.URL, destPath); err != nil {
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
} else {
normalizeIfGeosite(destPath, entry.FileName)
}
}
} else {
@@ -1213,8 +1159,6 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
if err := downloadFile(entry.URL, destPath); err != nil {
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
} else {
normalizeIfGeosite(destPath, entry.FileName)
}
}

View File

@@ -12,7 +12,6 @@ import (
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@@ -295,26 +294,9 @@ func (s *Server) initRouter() (*gin.Engine, error) {
return engine, nil
}
// normalizeExistingGeositeFiles normalizes country codes in all geosite .dat
// files found in the bin directory so Xray-core can locate entries correctly.
func normalizeExistingGeositeFiles() {
binDir := config.GetBinFolderPath()
matches, err := filepath.Glob(filepath.Join(binDir, "geosite*.dat"))
if err != nil {
logger.Warningf("Failed to glob geosite files: %v", err)
return
}
for _, path := range matches {
if err := service.NormalizeGeositeCountryCodes(path); err != nil {
logger.Warningf("Failed to normalize geosite country codes in %s: %v", path, err)
}
}
}
// startTask schedules background jobs (Xray checks, traffic jobs, cron
// jobs) which the panel relies on for periodic maintenance and monitoring.
func (s *Server) startTask() {
normalizeExistingGeositeFiles()
s.customGeoService.EnsureOnStartup()
err := s.xrayService.RestartXray(true)
if err != nil {

View File

@@ -37,6 +37,34 @@ type XrayAPI struct {
isConnected bool
}
func getRequiredUserString(user map[string]any, key string) (string, error) {
value, ok := user[key]
if !ok || value == nil {
return "", fmt.Errorf("missing required user field %q", key)
}
strValue, ok := value.(string)
if !ok {
return "", fmt.Errorf("invalid type for user field %q: %T", key, value)
}
return strValue, nil
}
func getOptionalUserString(user map[string]any, key string) (string, error) {
value, ok := user[key]
if !ok || value == nil {
return "", nil
}
strValue, ok := value.(string)
if !ok {
return "", fmt.Errorf("invalid type for user field %q: %T", key, value)
}
return strValue, nil
}
// Init connects to the Xray API server and initializes handler and stats service clients.
func (x *XrayAPI) Init(apiPort int) error {
if apiPort <= 0 || apiPort > math.MaxUint16 {
@@ -104,16 +132,36 @@ func (x *XrayAPI) DelInbound(tag string) error {
// AddUser adds a user to an inbound in the Xray core using the specified protocol and user data.
func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]any) error {
userEmail, err := getRequiredUserString(user, "email")
if err != nil {
return err
}
var account *serial.TypedMessage
switch Protocol {
case "vmess":
userID, err := getRequiredUserString(user, "id")
if err != nil {
return err
}
account = serial.ToTypedMessage(&vmess.Account{
Id: user["id"].(string),
Id: userID,
})
case "vless":
userID, err := getRequiredUserString(user, "id")
if err != nil {
return err
}
userFlow, err := getOptionalUserString(user, "flow")
if err != nil {
return err
}
vlessAccount := &vless.Account{
Id: user["id"].(string),
Flow: user["flow"].(string),
Id: userID,
Flow: userFlow,
}
// Add testseed if provided
if testseedVal, ok := user["testseed"]; ok {
@@ -139,12 +187,27 @@ func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]an
}
account = serial.ToTypedMessage(vlessAccount)
case "trojan":
password, err := getRequiredUserString(user, "password")
if err != nil {
return err
}
account = serial.ToTypedMessage(&trojan.Account{
Password: user["password"].(string),
Password: password,
})
case "shadowsocks":
cipher, err := getOptionalUserString(user, "cipher")
if err != nil {
return err
}
password, err := getRequiredUserString(user, "password")
if err != nil {
return err
}
var ssCipherType shadowsocks.CipherType
switch user["cipher"].(string) {
switch cipher {
case "aes-128-gcm":
ssCipherType = shadowsocks.CipherType_AES_128_GCM
case "aes-256-gcm":
@@ -159,18 +222,23 @@ func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]an
if ssCipherType != shadowsocks.CipherType_NONE {
account = serial.ToTypedMessage(&shadowsocks.Account{
Password: user["password"].(string),
Password: password,
CipherType: ssCipherType,
})
} else {
account = serial.ToTypedMessage(&shadowsocks_2022.ServerConfig{
Key: user["password"].(string),
Email: user["email"].(string),
Key: password,
Email: userEmail,
})
}
case "hysteria":
auth, err := getRequiredUserString(user, "auth")
if err != nil {
return err
}
account = serial.ToTypedMessage(&hysteriaAccount.Account{
Auth: user["auth"].(string),
Auth: auth,
})
default:
return nil
@@ -178,11 +246,11 @@ func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]an
client := *x.HandlerServiceClient
_, err := client.AlterInbound(context.Background(), &command.AlterInboundRequest{
_, err = client.AlterInbound(context.Background(), &command.AlterInboundRequest{
Tag: inboundTag,
Operation: serial.ToTypedMessage(&command.AddUserOperation{
User: &protocol.User{
Email: user["email"].(string),
Email: userEmail,
Account: account,
},
}),