mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-05 09:57:14 +00:00
bdee0a2068
Real incident: an AmneziaWG inbound with RouteThroughXray enabled lost
all internet on that connection after a migration. Root-caused on the
live box -- iptables TPROXY counters were incrementing (packets
correctly redirected to 127.0.0.1:63110), but nothing was actually
listening there (ss showed nothing on that port) until a full
`systemctl restart x-ui`, after which the bridge came up immediately.
Xray-core's gRPC AddInbound reports success for a new sockopt.tproxy
inbound (internal/amneziawg's own Xray egress bridge is the only kind
this fork ever generates) but doesn't reliably bind a working listener
for it outside of process startup -- the bridge silently never comes
up, and RouteThroughXray traffic goes nowhere until the next full
restart happens to occur for an unrelated reason.
diffInbounds already has this exact defensive pattern for REALITY
inbounds ("a gRPC remove+add does not reliably rebuild the REALITY
authenticator"), just never extended to TPROXY, and only in the
already-existing-then-changed branch -- the "brand new inbound" branch
had no such guard at all, which is exactly the path a freshly-enabled
RouteThroughXray bridge takes. Added inboundUsesTproxy and wired it
into both branches.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
439 lines
18 KiB
Go
439 lines
18 KiB
Go
package xray
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
|
|
|
|
"github.com/op/go-logging"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
// ComputeHotDiff logs the section that blocks a hot apply; the package
|
|
// logger must exist before any test exercises a blocked path.
|
|
xuilogger.InitLogger(logging.ERROR)
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func makeHotConfig() *Config {
|
|
return &Config{
|
|
LogConfig: json_util.RawMessage(`{"loglevel":"warning"}`),
|
|
RouterConfig: json_util.RawMessage(`{"domainStrategy":"AsIs","rules":[{"type":"field","inboundTag":["api"],"outboundTag":"api"}]}`),
|
|
OutboundConfigs: json_util.RawMessage(`[{"protocol":"freedom","tag":"direct"},{"protocol":"blackhole","tag":"blocked"}]`),
|
|
Policy: json_util.RawMessage(`{}`),
|
|
API: json_util.RawMessage(`{"services":["HandlerService","StatsService","RoutingService"],"tag":"api"}`),
|
|
Stats: json_util.RawMessage(`{}`),
|
|
Metrics: json_util.RawMessage(`{}`),
|
|
InboundConfigs: []InboundConfig{
|
|
{
|
|
Port: 62789,
|
|
Protocol: "tunnel",
|
|
Tag: "api",
|
|
Listen: json_util.RawMessage(`"127.0.0.1"`),
|
|
Settings: json_util.RawMessage(`{}`),
|
|
},
|
|
{
|
|
Port: 1080,
|
|
Protocol: "vless",
|
|
Tag: "inbound-1080",
|
|
Listen: json_util.RawMessage(`"0.0.0.0"`),
|
|
Settings: json_util.RawMessage(`{"clients":[]}`),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_NoChanges(t *testing.T) {
|
|
diff, ok := ComputeHotDiff(makeHotConfig(), makeHotConfig())
|
|
if !ok {
|
|
t.Fatal("identical configs must be hot-appliable")
|
|
}
|
|
if !diff.Empty() {
|
|
t.Fatalf("identical configs must produce an empty diff, got %+v", diff)
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_FormattingOnlyChangeIsEmptyDiff(t *testing.T) {
|
|
oldCfg := makeHotConfig()
|
|
newCfg := makeHotConfig()
|
|
// Reformat every section the way a frontend textarea save would.
|
|
newCfg.LogConfig = json_util.RawMessage("{\n \"loglevel\": \"warning\"\n}")
|
|
newCfg.Policy = json_util.RawMessage("{ }")
|
|
newCfg.API = json_util.RawMessage("{\n \"services\": [\"HandlerService\", \"StatsService\", \"RoutingService\"],\n \"tag\": \"api\"\n}")
|
|
newCfg.OutboundConfigs = json_util.RawMessage("[\n {\"protocol\": \"freedom\", \"tag\": \"direct\"},\n {\"protocol\": \"blackhole\", \"tag\": \"blocked\"}\n]")
|
|
newCfg.InboundConfigs[1].Settings = json_util.RawMessage("{\n \"clients\": []\n}")
|
|
|
|
diff, ok := ComputeHotDiff(oldCfg, newCfg)
|
|
if !ok {
|
|
t.Fatal("formatting-only change must be hot-appliable")
|
|
}
|
|
if len(diff.RemovedInboundTags) != 0 || len(diff.AddedInbounds) != 0 ||
|
|
len(diff.RemovedOutboundTags) != 0 || len(diff.AddedOutbounds) != 0 {
|
|
t.Fatalf("formatting-only change must produce no handler ops, got %+v", diff)
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_CanonicalEquality(t *testing.T) {
|
|
// Key reorder in a static section (the DNS editor rebuilds the object on
|
|
// save) must not read as a change.
|
|
oldCfg := makeHotConfig()
|
|
oldCfg.DNSConfig = json_util.RawMessage(`{"servers":["1.1.1.1"],"queryStrategy":"UseIP","tag":"dns-in"}`)
|
|
newCfg := makeHotConfig()
|
|
newCfg.DNSConfig = json_util.RawMessage(`{"tag":"dns-in","queryStrategy":"UseIP","servers":["1.1.1.1"]}`)
|
|
diff, ok := ComputeHotDiff(oldCfg, newCfg)
|
|
if !ok || !diff.Empty() {
|
|
t.Fatalf("dns key reorder must be an empty hot diff, ok=%v diff=%+v", ok, diff)
|
|
}
|
|
|
|
// Explicit null and an absent section are the same thing.
|
|
newCfg = makeHotConfig()
|
|
newCfg.FakeDNS = json_util.RawMessage(`null`)
|
|
diff, ok = ComputeHotDiff(makeHotConfig(), newCfg)
|
|
if !ok || !diff.Empty() {
|
|
t.Fatalf("fakedns null vs absent must be an empty hot diff, ok=%v diff=%+v", ok, diff)
|
|
}
|
|
|
|
// A real DNS change still forces a restart — there is no reload API.
|
|
newCfg = makeHotConfig()
|
|
newCfg.DNSConfig = json_util.RawMessage(`{"servers":["8.8.8.8"]}`)
|
|
if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
|
|
t.Fatal("real dns change must force a restart")
|
|
}
|
|
|
|
// Large integers keep full precision during normalization: two values
|
|
// that only differ past float64 precision must still read as a change.
|
|
oldCfg = makeHotConfig()
|
|
oldCfg.Policy = json_util.RawMessage(`{"big":9007199254740993}`)
|
|
newCfg = makeHotConfig()
|
|
newCfg.Policy = json_util.RawMessage(`{"big":9007199254740992}`)
|
|
if _, ok := ComputeHotDiff(oldCfg, newCfg); ok {
|
|
t.Fatal("values differing past float64 precision must not compare equal")
|
|
}
|
|
|
|
// Reordered keys inside the first (default) outbound must not force a
|
|
// restart — the form editor rebuilds the object on save.
|
|
oldCfg = makeHotConfig()
|
|
oldCfg.OutboundConfigs = json_util.RawMessage(`[{"protocol":"freedom","settings":{"domainStrategy":"AsIs"},"tag":"direct"},{"protocol":"blackhole","tag":"blocked"}]`)
|
|
newCfg = makeHotConfig()
|
|
newCfg.OutboundConfigs = json_util.RawMessage(`[{"tag":"direct","settings":{"domainStrategy":"AsIs"},"protocol":"freedom"},{"protocol":"blackhole","tag":"blocked"}]`)
|
|
diff, ok = ComputeHotDiff(oldCfg, newCfg)
|
|
if !ok || !diff.Empty() {
|
|
t.Fatalf("first outbound key reorder must be an empty hot diff, ok=%v diff=%+v", ok, diff)
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_StaticSectionChangeNeedsRestart(t *testing.T) {
|
|
newCfg := makeHotConfig()
|
|
newCfg.LogConfig = json_util.RawMessage(`{"loglevel":"debug"}`)
|
|
if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
|
|
t.Fatal("log change must force a restart")
|
|
}
|
|
|
|
newCfg = makeHotConfig()
|
|
newCfg.DNSConfig = json_util.RawMessage(`{"servers":["1.1.1.1"]}`)
|
|
if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
|
|
t.Fatal("dns change must force a restart")
|
|
}
|
|
|
|
newCfg = makeHotConfig()
|
|
newCfg.Observatory = json_util.RawMessage(`{"subjectSelector":["wg"]}`)
|
|
if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
|
|
t.Fatal("observatory change must force a restart")
|
|
}
|
|
|
|
newCfg = makeHotConfig()
|
|
newCfg.Env = json_util.RawMessage(`{"XRAY_DNS_PATH":"/tmp/dns"}`)
|
|
if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
|
|
t.Fatal("env change must force a restart: env vars are read only at process start")
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_InboundAddRemoveChange(t *testing.T) {
|
|
oldCfg := makeHotConfig()
|
|
newCfg := makeHotConfig()
|
|
// change existing beyond the clients list, so no user-level shortcut applies
|
|
newCfg.InboundConfigs[1].Settings = json_util.RawMessage(`{"clients":[],"decryption":"none"}`)
|
|
// add new
|
|
newCfg.InboundConfigs = append(newCfg.InboundConfigs, InboundConfig{
|
|
Port: 2080, Protocol: "vmess", Tag: "inbound-2080",
|
|
Settings: json_util.RawMessage(`{}`),
|
|
})
|
|
|
|
diff, ok := ComputeHotDiff(oldCfg, newCfg)
|
|
if !ok {
|
|
t.Fatal("inbound-only change must be hot-appliable")
|
|
}
|
|
if len(diff.RemovedInboundTags) != 1 || diff.RemovedInboundTags[0] != "inbound-1080" {
|
|
t.Fatalf("expected changed inbound to be removed, got %v", diff.RemovedInboundTags)
|
|
}
|
|
if len(diff.AddedInbounds) != 2 {
|
|
t.Fatalf("expected re-add + new add, got %d", len(diff.AddedInbounds))
|
|
}
|
|
if diff.RoutingConfig != nil || len(diff.AddedOutbounds) != 0 || len(diff.RemovedOutboundTags) != 0 {
|
|
t.Fatalf("unexpected non-inbound operations: %+v", diff)
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_ClientOnlyChangeUsesUserOps(t *testing.T) {
|
|
oldCfg := makeHotConfig()
|
|
oldCfg.InboundConfigs[1].Settings = json_util.RawMessage(`{"clients":[{"email":"a","id":"uuid-a"},{"email":"b","id":"uuid-b"}],"decryption":"none"}`)
|
|
newCfg := makeHotConfig()
|
|
// b expired and is stripped from the generated config (#5712); a's id rotated.
|
|
newCfg.InboundConfigs[1].Settings = json_util.RawMessage(`{"clients":[{"email":"a","id":"uuid-a2"},{"email":"c","id":"uuid-c"}],"decryption":"none"}`)
|
|
|
|
diff, ok := ComputeHotDiff(oldCfg, newCfg)
|
|
if !ok {
|
|
t.Fatal("client-only change must be hot-appliable")
|
|
}
|
|
if len(diff.RemovedInboundTags) != 0 || len(diff.AddedInbounds) != 0 {
|
|
t.Fatalf("client-only change must not replace the handler, got %+v", diff)
|
|
}
|
|
removed := map[string]bool{}
|
|
for _, u := range diff.RemovedUsers {
|
|
if u.Tag != "inbound-1080" || u.Protocol != "vless" {
|
|
t.Fatalf("removed user op has wrong target: %+v", u)
|
|
}
|
|
removed[u.Email] = true
|
|
}
|
|
if len(removed) != 2 || !removed["a"] || !removed["b"] {
|
|
t.Fatalf("expected users a (changed) and b (gone) removed, got %v", removed)
|
|
}
|
|
added := map[string]string{}
|
|
for _, u := range diff.AddedUsers {
|
|
id, _ := u.User["id"].(string)
|
|
added[u.Email] = id
|
|
}
|
|
if len(added) != 2 || added["a"] != "uuid-a2" || added["c"] != "uuid-c" {
|
|
t.Fatalf("expected users a (new id) and c added, got %v", added)
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_ClientChangeFallsBackToReplace(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
mutate func(cfg *Config)
|
|
}{
|
|
{
|
|
name: "unsupported protocol",
|
|
mutate: func(cfg *Config) {
|
|
cfg.InboundConfigs[1].Protocol = "shadowsocks"
|
|
},
|
|
},
|
|
{
|
|
name: "client without email",
|
|
mutate: func(cfg *Config) {
|
|
cfg.InboundConfigs[1].Settings = json_util.RawMessage(`{"clients":[{"id":"uuid-a"}]}`)
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
oldCfg := makeHotConfig()
|
|
newCfg := makeHotConfig()
|
|
tc.mutate(oldCfg)
|
|
tc.mutate(newCfg)
|
|
newCfg.InboundConfigs[1].Settings = json_util.RawMessage(`{"clients":[{"email":"x","id":"uuid-x","password":"pw"}]}`)
|
|
|
|
diff, ok := ComputeHotDiff(oldCfg, newCfg)
|
|
if !ok {
|
|
t.Fatal("change must still be hot-appliable via handler replacement")
|
|
}
|
|
if len(diff.RemovedUsers) != 0 || len(diff.AddedUsers) != 0 {
|
|
t.Fatalf("expected no user ops, got %+v", diff)
|
|
}
|
|
if len(diff.RemovedInboundTags) != 1 || len(diff.AddedInbounds) != 1 {
|
|
t.Fatalf("expected handler replacement, got %+v", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_ApiInboundChangeNeedsRestart(t *testing.T) {
|
|
newCfg := makeHotConfig()
|
|
newCfg.InboundConfigs[0].Port = 62790
|
|
if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
|
|
t.Fatal("api inbound change must force a restart")
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_OutboundChangeAndReorder(t *testing.T) {
|
|
oldCfg := makeHotConfig()
|
|
newCfg := makeHotConfig()
|
|
// change a non-first outbound + add one
|
|
newCfg.OutboundConfigs = json_util.RawMessage(`[{"protocol":"freedom","tag":"direct"},{"protocol":"blackhole","settings":{},"tag":"blocked"},{"protocol":"socks","tag":"warp"}]`)
|
|
|
|
diff, ok := ComputeHotDiff(oldCfg, newCfg)
|
|
if !ok {
|
|
t.Fatal("outbound-only change must be hot-appliable")
|
|
}
|
|
if len(diff.RemovedOutboundTags) != 1 || diff.RemovedOutboundTags[0] != "blocked" {
|
|
t.Fatalf("expected changed outbound to be removed, got %v", diff.RemovedOutboundTags)
|
|
}
|
|
if len(diff.AddedOutbounds) != 2 {
|
|
t.Fatalf("expected re-add + new add, got %d", len(diff.AddedOutbounds))
|
|
}
|
|
for _, raw := range diff.AddedOutbounds {
|
|
if !strings.Contains(string(raw), `"tag"`) {
|
|
t.Fatalf("added outbound JSON must be the raw element, got %s", raw)
|
|
}
|
|
}
|
|
|
|
// pure reorder of non-first outbounds must be a no-op
|
|
reordered := makeHotConfig()
|
|
reordered.OutboundConfigs = json_util.RawMessage(`[{"protocol":"freedom","tag":"direct"},{"protocol":"socks","tag":"warp"},{"protocol":"blackhole","tag":"blocked"}]`)
|
|
base := makeHotConfig()
|
|
base.OutboundConfigs = json_util.RawMessage(`[{"protocol":"freedom","tag":"direct"},{"protocol":"blackhole","tag":"blocked"},{"protocol":"socks","tag":"warp"}]`)
|
|
diff, ok = ComputeHotDiff(base, reordered)
|
|
if !ok || !diff.Empty() {
|
|
t.Fatalf("reorder of non-first outbounds must be an empty hot diff, ok=%v diff=%+v", ok, diff)
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_FirstOutboundChangeNeedsRestart(t *testing.T) {
|
|
newCfg := makeHotConfig()
|
|
// change the default (first) outbound content
|
|
newCfg.OutboundConfigs = json_util.RawMessage(`[{"protocol":"freedom","settings":{"domainStrategy":"UseIP"},"tag":"direct"},{"protocol":"blackhole","tag":"blocked"}]`)
|
|
if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
|
|
t.Fatal("changing the default outbound must force a restart")
|
|
}
|
|
|
|
// swap which outbound comes first
|
|
newCfg = makeHotConfig()
|
|
newCfg.OutboundConfigs = json_util.RawMessage(`[{"protocol":"blackhole","tag":"blocked"},{"protocol":"freedom","tag":"direct"}]`)
|
|
if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
|
|
t.Fatal("changing the first outbound must force a restart")
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_TaglessOutboundNeedsRestart(t *testing.T) {
|
|
newCfg := makeHotConfig()
|
|
newCfg.OutboundConfigs = json_util.RawMessage(`[{"protocol":"freedom","tag":"direct"},{"protocol":"blackhole"}]`)
|
|
if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
|
|
t.Fatal("tagless outbound must force a restart")
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_RoutingRulesChange(t *testing.T) {
|
|
newCfg := makeHotConfig()
|
|
newCfg.RouterConfig = json_util.RawMessage(`{"domainStrategy":"AsIs","rules":[{"type":"field","inboundTag":["api"],"outboundTag":"api"},{"type":"field","ip":["geoip:private"],"outboundTag":"blocked"}]}`)
|
|
|
|
diff, ok := ComputeHotDiff(makeHotConfig(), newCfg)
|
|
if !ok {
|
|
t.Fatal("rules-only routing change must be hot-appliable")
|
|
}
|
|
if diff.RoutingConfig == nil {
|
|
t.Fatal("routing diff must carry the new routing section")
|
|
}
|
|
|
|
// balancers are reloadable too
|
|
newCfg = makeHotConfig()
|
|
newCfg.RouterConfig = json_util.RawMessage(`{"domainStrategy":"AsIs","rules":[],"balancers":[{"tag":"b1","selector":["wg"]}]}`)
|
|
if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); !ok {
|
|
t.Fatal("balancer-only routing change must be hot-appliable")
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_RoutingStrategyChangeNeedsRestart(t *testing.T) {
|
|
newCfg := makeHotConfig()
|
|
newCfg.RouterConfig = json_util.RawMessage(`{"domainStrategy":"IPIfNonMatch","rules":[{"type":"field","inboundTag":["api"],"outboundTag":"api"}]}`)
|
|
if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
|
|
t.Fatal("domainStrategy change must force a restart")
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_RealityStreamChangeNeedsRestart(t *testing.T) {
|
|
oldCfg := makeHotConfig()
|
|
oldCfg.InboundConfigs[1].StreamSettings = json_util.RawMessage(`{"network":"tcp","security":"reality","realitySettings":{"privateKey":"old-key","serverNames":["a.example"]}}`)
|
|
newCfg := makeHotConfig()
|
|
newCfg.InboundConfigs[1].StreamSettings = json_util.RawMessage(`{"network":"tcp","security":"reality","realitySettings":{"privateKey":"new-key","serverNames":["a.example"]}}`)
|
|
|
|
if _, ok := ComputeHotDiff(oldCfg, newCfg); ok {
|
|
t.Fatal("a REALITY stream-settings change must force a full restart, not a gRPC hot swap")
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_SecuritySwitchToRealityNeedsRestart(t *testing.T) {
|
|
oldCfg := makeHotConfig()
|
|
oldCfg.InboundConfigs[1].StreamSettings = json_util.RawMessage(`{"network":"tcp","security":"none"}`)
|
|
newCfg := makeHotConfig()
|
|
newCfg.InboundConfigs[1].StreamSettings = json_util.RawMessage(`{"network":"tcp","security":"reality","realitySettings":{"privateKey":"k"}}`)
|
|
|
|
if _, ok := ComputeHotDiff(oldCfg, newCfg); ok {
|
|
t.Fatal("switching security to REALITY must force a full restart")
|
|
}
|
|
}
|
|
|
|
func TestComputeHotDiff_RealityClientOnlyChangeStaysHot(t *testing.T) {
|
|
oldCfg := makeHotConfig()
|
|
oldCfg.InboundConfigs[1].StreamSettings = json_util.RawMessage(`{"network":"tcp","security":"reality","realitySettings":{"privateKey":"k"}}`)
|
|
oldCfg.InboundConfigs[1].Settings = json_util.RawMessage(`{"clients":[{"email":"a","id":"uuid-a"}],"decryption":"none"}`)
|
|
newCfg := makeHotConfig()
|
|
newCfg.InboundConfigs[1].StreamSettings = json_util.RawMessage(`{"network":"tcp","security":"reality","realitySettings":{"privateKey":"k"}}`)
|
|
newCfg.InboundConfigs[1].Settings = json_util.RawMessage(`{"clients":[{"email":"a","id":"uuid-a"},{"email":"b","id":"uuid-b"}],"decryption":"none"}`)
|
|
|
|
diff, ok := ComputeHotDiff(oldCfg, newCfg)
|
|
if !ok {
|
|
t.Fatal("client-only change on a REALITY inbound must stay hot-appliable")
|
|
}
|
|
if len(diff.RemovedInboundTags) != 0 || len(diff.AddedInbounds) != 0 {
|
|
t.Fatalf("client-only change must not replace the handler, got %+v", diff)
|
|
}
|
|
if len(diff.AddedUsers) != 1 || diff.AddedUsers[0].Email != "b" {
|
|
t.Fatalf("expected user b added via AlterInbound, got %+v", diff.AddedUsers)
|
|
}
|
|
}
|
|
|
|
// TestComputeHotDiff_NewTproxyInboundNeedsRestart reproduces a real incident:
|
|
// enabling RouteThroughXray on an AmneziaWG inbound while Xray is already
|
|
// running adds a brand-new dokodemo-door bridge with sockopt.tproxy set.
|
|
// Xray-core's gRPC AddInbound reports success for this but never actually
|
|
// binds a working listener, so TPROXY-redirected peer traffic silently goes
|
|
// nowhere until the next full restart -- confirmed directly on a real box
|
|
// (iptables TPROXY counters incrementing, but `ss` showing nothing listening
|
|
// on the bridge port; the listener only appeared after `systemctl restart
|
|
// x-ui`). This must force a restart instead of a hot add.
|
|
func TestComputeHotDiff_NewTproxyInboundNeedsRestart(t *testing.T) {
|
|
oldCfg := makeHotConfig()
|
|
newCfg := makeHotConfig()
|
|
newCfg.InboundConfigs = append(newCfg.InboundConfigs, InboundConfig{
|
|
Listen: json_util.RawMessage(`"127.0.0.1"`),
|
|
Port: 63110,
|
|
Protocol: "dokodemo-door",
|
|
Tag: "in-443-udp",
|
|
Settings: json_util.RawMessage(`{"allowedNetwork":"tcp,udp","followRedirect":true}`),
|
|
StreamSettings: json_util.RawMessage(`{"sockopt":{"tproxy":"tproxy"}}`),
|
|
})
|
|
|
|
if _, ok := ComputeHotDiff(oldCfg, newCfg); ok {
|
|
t.Fatal("adding a new TPROXY-sockopt inbound must force a full restart, not a gRPC hot add")
|
|
}
|
|
}
|
|
|
|
// TestComputeHotDiff_TproxyStreamChangeNeedsRestart mirrors the REALITY
|
|
// stream-change test above: an existing TPROXY bridge whose port changed
|
|
// (e.g. the AmneziaWG inbound's own id-derived egress port shifted) must not
|
|
// be hot-swapped either, for the same reliability reason.
|
|
func TestComputeHotDiff_TproxyStreamChangeNeedsRestart(t *testing.T) {
|
|
tproxyIb := InboundConfig{
|
|
Listen: json_util.RawMessage(`"127.0.0.1"`),
|
|
Port: 63110,
|
|
Protocol: "dokodemo-door",
|
|
Tag: "in-443-udp",
|
|
Settings: json_util.RawMessage(`{"allowedNetwork":"tcp,udp","followRedirect":true}`),
|
|
StreamSettings: json_util.RawMessage(`{"sockopt":{"tproxy":"tproxy"}}`),
|
|
}
|
|
oldCfg := makeHotConfig()
|
|
oldCfg.InboundConfigs = append(oldCfg.InboundConfigs, tproxyIb)
|
|
newCfg := makeHotConfig()
|
|
changedIb := tproxyIb
|
|
changedIb.Port = 63111
|
|
newCfg.InboundConfigs = append(newCfg.InboundConfigs, changedIb)
|
|
|
|
if _, ok := ComputeHotDiff(oldCfg, newCfg); ok {
|
|
t.Fatal("a TPROXY bridge's port change must force a full restart, not a gRPC hot swap")
|
|
}
|
|
}
|