Merge remote-tracking branch 'upstream/main' into sync-3.6.0

# Conflicts:
#	.github/workflows/claude-bot.yml
#	.github/workflows/release.yml
#	DockerInit.sh
#	frontend/package-lock.json
#	frontend/package.json
#	frontend/src/hooks/useClients.ts
#	frontend/src/layouts/AppSidebar.tsx
#	frontend/src/main.tsx
#	internal/config/version
#	internal/database/model/model.go
#	internal/web/service/client_wireguard.go
#	internal/web/service/inbound.go
This commit is contained in:
Kuzz007
2026-08-01 21:59:29 +03:00
250 changed files with 12904 additions and 5650 deletions
+52 -2
View File
@@ -1,6 +1,7 @@
package service
import (
"fmt"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
@@ -20,11 +21,12 @@ func TestAllocateWireguardAddress(t *testing.T) {
{name: "fills gap", used: []string{"10.0.0.3/32", "10.0.0.4/32"}, base: "10.0.0.0/24", want: "10.0.0.2/32"},
{name: "ignores catch-all", used: []string{"0.0.0.0/0", "::/0"}, base: "10.0.0.0/24", want: "10.0.0.2/32"},
{name: "default base when empty", used: nil, base: "", want: "10.0.0.2/32"},
{name: "exhausted /30", used: []string{"10.9.0.2/32", "10.9.0.3/32"}, base: "10.9.0.0/30", err: true},
{name: "full ipv4 scope widens instead of failing", used: []string{"10.9.0.2/32", "10.9.0.3/32"}, base: "10.9.0.0/30", want: "10.9.0.4/32"},
{name: "exhausted ipv6 scope errors", used: []string{"fd00::2/128", "fd00::3/128"}, base: "fd00::/126", err: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := allocateWireguardAddress(tt.used, tt.base)
got, err := allocateWireguardAddress(tt.used, tt.base, true)
if tt.err {
if err == nil {
t.Fatalf("expected error, got %q", got)
@@ -130,6 +132,54 @@ func TestDefaultWireguardClientsHonorsExistingSubnet(t *testing.T) {
}
}
func TestAllocateWireguardAddressWidensPastFullSlash24(t *testing.T) {
used := make([]string, 0, 254)
for i := 2; i <= 255; i++ {
used = append(used, fmt.Sprintf("10.0.0.%d/32", i))
}
got, err := allocateWireguardAddress(used, "10.0.0.0/24", true)
if err != nil {
t.Fatalf("allocate with a full /24: %v", err)
}
if got != "10.0.1.0/32" {
t.Fatalf("address after a full /24 = %q, want 10.0.1.0/32", got)
}
used = append(used, got)
next, err := allocateWireguardAddress(used, "10.0.0.0/24", true)
if err != nil {
t.Fatalf("allocate after widening: %v", err)
}
if next != "10.0.1.1/32" {
t.Fatalf("second widened address = %q, want 10.0.1.1/32", next)
}
}
func TestAllocateWireguardAddressFillsItsOwnSlash24First(t *testing.T) {
got, err := allocateWireguardAddress([]string{"172.16.0.2/32"}, "172.16.0.0/24", true)
if err != nil {
t.Fatalf("allocateWireguardAddress: %v", err)
}
if got != "172.16.0.3/32" {
t.Fatalf("address = %q, want 172.16.0.3/32 — the inbound's own /24 comes first", got)
}
}
func TestAllocateWireguardAddressWithoutWideningFailsClosed(t *testing.T) {
// AmneziaWG's caller passes allowWidening=false: an address outside the
// kernel interface's own configured subnet would never actually route,
// so exhausting the pool must error instead of silently handing out an
// address from a wider scope (unlike plain WireGuard's allowWidening=true).
used := make([]string, 0, 254)
for i := 2; i <= 255; i++ {
used = append(used, fmt.Sprintf("10.0.0.%d/32", i))
}
if _, err := allocateWireguardAddress(used, "10.0.0.0/24", false); err == nil {
t.Fatal("expected an error on a full /24 with widening disabled, got nil")
}
}
func TestDefaultWireguardClientsAllocatesDistinctIPs(t *testing.T) {
clients := []model.Client{{Email: "x@wg"}, {Email: "y@wg"}}
ifaces := []any{map[string]any{"email": "x@wg"}, map[string]any{"email": "y@wg"}}