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
+8 -1
View File
@@ -12,6 +12,10 @@ _openapi:
title: List every host across all inbounds, grouped by inbound then ordered by
sort order.
url: '#list-every-host-across-all-inbounds-grouped-by-inbound-then-ordered-by-sort-order'
- depth: 2
title: List every host row with its numeric id, for id-based pickers such as the
per-client host rule override.
url: '#list-every-host-row-with-its-numeric-id-for-id-based-pickers-such-as-the-per-client-host-rule-override'
- depth: 2
title: Fetch a single host group by Group ID.
url: '#fetch-a-single-host-group-by-group-id'
@@ -52,6 +56,9 @@ _openapi:
- content: List every host across all inbounds, grouped by inbound then ordered by
sort order.
id: list-every-host-across-all-inbounds-grouped-by-inbound-then-ordered-by-sort-order
- content: List every host row with its numeric id, for id-based pickers such as
the per-client host rule override.
id: list-every-host-row-with-its-numeric-id-for-id-based-pickers-such-as-the-per-client-host-rule-override
- content: Fetch a single host group by Group ID.
id: fetch-a-single-host-group-by-group-id
- content: Fetch one inbound's hosts, grouped by host group.
@@ -86,7 +93,7 @@ export default function Layout(props) {
return (
<>
{props.children}
<OpenAPIPage document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/hosts/list","method":"get"},{"path":"/panel/api/hosts/get/{groupId}","method":"get"},{"path":"/panel/api/hosts/byInbound/{inboundId}","method":"get"},{"path":"/panel/api/hosts/tags","method":"get"},{"path":"/panel/api/hosts/add","method":"post"},{"path":"/panel/api/hosts/update/{groupId}","method":"post"},{"path":"/panel/api/hosts/del/{groupId}","method":"post"},{"path":"/panel/api/hosts/setEnable/{groupId}","method":"post"},{"path":"/panel/api/hosts/reorder","method":"post"},{"path":"/panel/api/hosts/bulk/add","method":"post"},{"path":"/panel/api/hosts/bulk/setEnable","method":"post"},{"path":"/panel/api/hosts/bulk/del","method":"post"}]} showTitle />
<OpenAPIPage document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/hosts/list","method":"get"},{"path":"/panel/api/hosts/raw","method":"get"},{"path":"/panel/api/hosts/get/{groupId}","method":"get"},{"path":"/panel/api/hosts/byInbound/{inboundId}","method":"get"},{"path":"/panel/api/hosts/tags","method":"get"},{"path":"/panel/api/hosts/add","method":"post"},{"path":"/panel/api/hosts/update/{groupId}","method":"post"},{"path":"/panel/api/hosts/del/{groupId}","method":"post"},{"path":"/panel/api/hosts/setEnable/{groupId}","method":"post"},{"path":"/panel/api/hosts/reorder","method":"post"},{"path":"/panel/api/hosts/bulk/add","method":"post"},{"path":"/panel/api/hosts/bulk/setEnable","method":"post"},{"path":"/panel/api/hosts/bulk/del","method":"post"}]} showTitle />
</>
);
}
+109
View File
@@ -1730,6 +1730,17 @@
"description": "Auth password (Hysteria)",
"type": "string"
},
"clientHostRuleIds": {
"description": "ClientHostRuleIds binds the client to these host rules, one link each; nil preserves, empty\nclears, and bindings with no usable host leave that inbound out of the subscription.",
"example": [
1,
2
],
"items": {
"type": "integer"
},
"type": "array"
},
"comment": {
"description": "Client comment",
"type": "string"
@@ -1967,6 +1978,16 @@
"auth": {
"type": "string"
},
"clientHostRuleIds": {
"example": [
1,
2
],
"items": {
"type": "integer"
},
"type": "array"
},
"comment": {
"type": "string"
},
@@ -12061,6 +12082,94 @@
}
}
},
"/panel/api/hosts/raw": {
"get": {
"tags": [
"Hosts"
],
"summary": "List every host row with its numeric id, for id-based pickers such as the per-client host rule override.",
"operationId": "get_panel_api_hosts_raw",
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"msg": {
"type": "string"
},
"obj": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Host"
}
}
}
},
"example": {
"success": true,
"obj": [
{
"address": "cdn.example.com",
"allowInsecure": false,
"alpn": [
""
],
"cipherSuites": "",
"createdAt": 0,
"echConfigList": "",
"excludeFromSubTypes": [
""
],
"finalMask": "",
"fingerprint": "",
"groupId": "",
"hostHeader": "",
"id": 1,
"inboundId": 1,
"isDisabled": false,
"isHidden": false,
"keepSniBlank": false,
"mihomoIpVersion": "dual",
"mihomoX25519": false,
"muxParams": null,
"nodeGuids": [
""
],
"overrideSniFromAddress": false,
"path": "",
"pinnedPeerCertSha256": [
""
],
"port": 8443,
"randomCount": 1,
"remark": "cdn-front",
"security": "same",
"serverDescription": "",
"shuffleHost": false,
"sni": "",
"sockoptParams": null,
"sortOrder": 0,
"tags": [
""
],
"updatedAt": 0,
"verifyPeerCertByName": "",
"vlessRoute": "443"
}
]
}
}
}
}
}
}
},
"/panel/api/hosts/get/{groupId}": {
"get": {
"tags": [