feat(clients): bind host rules to individual clients in subscriptions (#6620)

Host rules applied per inbound only, so every client of an inbound got the same
subscription endpoints (#6618). A client can now be bound to one or more host
rules, and its raw, JSON and Clash subscriptions render only those hosts, in
either subscription order; an unbound client is unchanged. A binding whose hosts
are deleted, disabled or excluded from the format fails closed: the client gets
no links there but stays in traffic accounting. Host edits keep row ids, so
bindings survive them; nil leaves a stored binding alone and an empty list
clears it.
This commit is contained in:
Nethmina Yasas
2026-10-24 16:03:02 +05:30
committed by GitHub
parent d103c0c012
commit bc1b750e00
47 changed files with 1940 additions and 168 deletions
+24
View File
@@ -2,6 +2,7 @@ package model
import (
"encoding/json"
"reflect"
"strings"
"testing"
)
@@ -270,3 +271,26 @@ func TestGenXrayInboundConfig_OmitsInboundXmuxButDbRowUnchanged(t *testing.T) {
t.Fatal("inbound row streamSettings must still carry xmux for subscriptions")
}
}
// nil must survive (it means "leave the stored binding alone"), while an empty or
// all-invalid list must come back empty so it clears.
func TestNormalizeClientHostRuleIds(t *testing.T) {
for _, tt := range []struct {
name string
in []int
want []int
}{
{"nil preserves", nil, nil},
{"empty clears", []int{}, []int{}},
{"dedupe keeps order", []int{3, 2, 3, 2}, []int{3, 2}},
{"non-positive dropped", []int{0, -1, 2, 0, 3}, []int{2, 3}},
{"all non-positive clears", []int{0, -2}, []int{}},
} {
t.Run(tt.name, func(t *testing.T) {
got := NormalizeClientHostRuleIds(tt.in)
if (got == nil) != (tt.want == nil) || !reflect.DeepEqual(got, tt.want) {
t.Fatalf("NormalizeClientHostRuleIds(%v) = %#v, want %#v", tt.in, got, tt.want)
}
})
}
}