mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-07 13:45:01 +03:00
fix(node): keep pushed inbounds off the node host's own resources
A node-sync token, or a master enrolled with the mTLS certificate, may push inbounds, and the node stored them as already validated. An inbound is effectively config for a core running as root, so a push could serve the host's files (hysteria file masquerade), relay to its own services or the core API (masquerade proxy, fallback dest, REALITY target), write files (masterKeyLog), use local or templated XDRIVE storage, fetch URLs (ECH, realm masks), or run tun/http/mixed inbounds. A push now refuses those fields unless the node already stores the same value, which keeps rows configured by the node's own admin syncing: relay targets must be public or another of this node's inbounds, and only node-eligible protocols are accepted. Every caller is refused env: references, which the core expands from the panel's environment, and the inbound listen address is JSON-encoded into the config instead of quoted by hand.
This commit is contained in:
@@ -215,3 +215,67 @@ func TestAddInboundRefusesACertificatePathThatIsNoRegularFile(t *testing.T) {
|
||||
t.Fatal("refused push reached the runtime")
|
||||
}
|
||||
}
|
||||
|
||||
// A node-sync token or the master's certificate pushes inbounds into this node's core,
|
||||
// which runs as root: a push must not reach this host's files, sockets or own services.
|
||||
func TestAddInboundRefusesPushedFieldsThatReachThisHost(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
mgr := useTestRuntimeManager(t)
|
||||
mgr.SetLocalRuntimeOverride(&fakeNodeRuntime{})
|
||||
push := &InboundService{FromNodeSync: true}
|
||||
if _, _, err := push.AddInbound(&model.Inbound{Tag: "web", Enable: true, Port: 44400, Protocol: model.VLESS, Settings: `{"clients":[]}`}); err != nil {
|
||||
t.Fatalf("seed a local inbound: %v", err)
|
||||
}
|
||||
vless := func(settings, stream string) *model.Inbound {
|
||||
return &model.Inbound{Enable: true, Port: 44401, Protocol: model.VLESS, Settings: settings, StreamSettings: stream}
|
||||
}
|
||||
for name, ib := range map[string]*model.Inbound{
|
||||
"file masquerade behind a repeated key": {
|
||||
Enable: true, Port: 44401, Protocol: model.Hysteria, Settings: `{"clients":[]}`,
|
||||
StreamSettings: `{"network":"hysteria","HysteriaSettings":{"masquerade":{"Type":"FILE","dir":"/"}},"hysteriaSettings":{"masquerade":{"type":"404"}}}`,
|
||||
},
|
||||
"masterKeyLog": vless(`{"clients":[]}`, `{"network":"tcp","security":"tls","tlsSettings":{"masterKeyLog":"/root/.ssh/authorized_keys"}}`),
|
||||
"tun inbound": {Enable: true, Port: 44401, Protocol: model.Protocol("tun"), Settings: `{}`},
|
||||
"fallback to the core API": vless(`{"clients":[],"fallbacks":[{"dest":62789}]}`, `{"network":"tcp"}`),
|
||||
"REALITY target on loopback": vless(`{"clients":[]}`, `{"network":"tcp","security":"reality","realitySettings":{"target":"127.0.0.1:2053"}}`),
|
||||
"local xdrive storage": vless(`{"clients":[]}`, `{"network":"xdrive","xdriveSettings":{"service":"local","remoteFolder":"/etc/x-ui"}}`),
|
||||
} {
|
||||
if _, _, err := push.AddInbound(ib); err == nil {
|
||||
t.Errorf("%s: pushed inbound accepted", name)
|
||||
}
|
||||
}
|
||||
for name, ib := range map[string]*model.Inbound{
|
||||
"fallback to another inbound here": vless(`{"clients":[],"fallbacks":[{"dest":44400}]}`, `{"network":"tcp"}`),
|
||||
"public REALITY target": {Enable: true, Port: 44402, Protocol: model.VLESS, Settings: `{"clients":[]}`, StreamSettings: `{"network":"tcp","security":"reality","realitySettings":{"target":"www.example.com:443"}}`},
|
||||
} {
|
||||
if _, _, err := push.AddInbound(ib); err != nil && strings.Contains(err.Error(), "pushed") {
|
||||
t.Errorf("%s: refused: %v", name, err)
|
||||
}
|
||||
}
|
||||
if _, _, err := (&InboundService{}).AddInbound(&model.Inbound{Enable: true, Listen: "env:XUI_DB_DSN", Port: 44403, Protocol: model.VLESS, Settings: `{"clients":[]}`}); err == nil || !strings.Contains(err.Error(), "env:") {
|
||||
t.Fatalf("env: listen = %v, want refused for every caller", err)
|
||||
}
|
||||
}
|
||||
|
||||
// What this node's own admin configured stays pushable: the master echoes the stored row
|
||||
// back, and refusing it would stop that inbound syncing at all.
|
||||
func TestUpdateInboundKeepsAHostReachTheNodeAlreadyStores(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
mgr := useTestRuntimeManager(t)
|
||||
mgr.SetLocalRuntimeOverride(&fakeNodeRuntime{})
|
||||
stream := `{"network":"hysteria","hysteriaSettings":{"masquerade":{"type":"proxy","url":"https://news.example.com"}}}`
|
||||
stored, _, err := (&InboundService{}).AddInbound(&model.Inbound{Tag: "hy", Enable: true, Port: 44410, Protocol: model.Hysteria, Settings: `{"clients":[]}`, StreamSettings: stream})
|
||||
if err != nil {
|
||||
t.Fatalf("admin add: %v", err)
|
||||
}
|
||||
push := &InboundService{FromNodeSync: true}
|
||||
echo := *stored
|
||||
if _, _, err := push.UpdateInbound(&echo); err != nil {
|
||||
t.Fatalf("pushing back the stored row: %v", err)
|
||||
}
|
||||
changed := *stored
|
||||
changed.StreamSettings = `{"network":"hysteria","hysteriaSettings":{"masquerade":{"type":"proxy","url":"http://127.0.0.1:2053"}}}`
|
||||
if _, _, err := push.UpdateInbound(&changed); err == nil {
|
||||
t.Fatal("a push moved the masquerade proxy to the panel's own port")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user