mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-08 13:46:08 +00:00
feat(mtproto): per-client ad-tags, management-API auth, and record secret sync
Catch the panel up to the mtg-multi README (v1.14.0): - Each client can now carry its own 32-hex advertising tag overriding the inbound-level one. The tag lives on the client (settings JSON is the source of truth, clients.ad_tag is the UI projection), is rendered into the fork's [secret-ad-tags] section for active secrets only (mtg rejects a config whose override names an unknown secret), is pushed per entry through PUT /secrets, and is part of the reload fingerprint so a tag edit hot-applies without dropping connections. - The loopback management API can replace the whole secret set, so every mtg process now gets a random per-process api-token; the manager sends it as a bearer token on PUT /secrets and GET /stats and reuses it across config rewrites, because mtg reads the token only at startup. - Malformed tags are rejected at every save path and additionally dropped in InstanceFromInbound: one bad tag would otherwise fail the whole generated config and take every client of the inbound down with it. - SyncInbound never copied a re-keyed mtproto secret into the canonical clients table, so the clients page and subscription links kept serving the old secret, which mtg then rejects. It is now guarded-copied like the other credentials.
This commit is contained in:
@@ -1092,6 +1092,10 @@
|
||||
"Client": {
|
||||
"description": "Client represents a client configuration for Xray inbounds with traffic limits and settings.",
|
||||
"properties": {
|
||||
"adTag": {
|
||||
"example": "0123456789abcdef0123456789abcdef",
|
||||
"type": "string"
|
||||
},
|
||||
"allowedIPs": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -1231,6 +1235,9 @@
|
||||
},
|
||||
"ClientRecord": {
|
||||
"properties": {
|
||||
"adTag": {
|
||||
"type": "string"
|
||||
},
|
||||
"allowedIPs": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1306,6 +1313,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"adTag",
|
||||
"allowedIPs",
|
||||
"auth",
|
||||
"comment",
|
||||
|
||||
@@ -214,6 +214,7 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"token": "new-token-string"
|
||||
},
|
||||
"Client": {
|
||||
"adTag": "0123456789abcdef0123456789abcdef",
|
||||
"allowedIPs": [
|
||||
""
|
||||
],
|
||||
@@ -248,6 +249,7 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"inboundId": 0
|
||||
},
|
||||
"ClientRecord": {
|
||||
"adTag": "",
|
||||
"allowedIPs": "",
|
||||
"auth": "",
|
||||
"comment": "",
|
||||
|
||||
@@ -1066,6 +1066,10 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"Client": {
|
||||
"description": "Client represents a client configuration for Xray inbounds with traffic limits and settings.",
|
||||
"properties": {
|
||||
"adTag": {
|
||||
"example": "0123456789abcdef0123456789abcdef",
|
||||
"type": "string"
|
||||
},
|
||||
"allowedIPs": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -1205,6 +1209,9 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"ClientRecord": {
|
||||
"properties": {
|
||||
"adTag": {
|
||||
"type": "string"
|
||||
},
|
||||
"allowedIPs": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1280,6 +1287,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"adTag",
|
||||
"allowedIPs",
|
||||
"auth",
|
||||
"comment",
|
||||
|
||||
@@ -224,6 +224,7 @@ export interface ApiTokenView {
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
adTag?: string;
|
||||
allowedIPs?: string[];
|
||||
auth?: string;
|
||||
comment: string;
|
||||
@@ -258,6 +259,7 @@ export interface ClientInbound {
|
||||
}
|
||||
|
||||
export interface ClientRecord {
|
||||
adTag: string;
|
||||
allowedIPs: string;
|
||||
auth: string;
|
||||
comment: string;
|
||||
|
||||
@@ -240,6 +240,7 @@ export const ApiTokenViewSchema = z.object({
|
||||
export type ApiTokenView = z.infer<typeof ApiTokenViewSchema>;
|
||||
|
||||
export const ClientSchema = z.object({
|
||||
adTag: z.string().optional(),
|
||||
allowedIPs: z.array(z.string()).optional(),
|
||||
auth: z.string().optional(),
|
||||
comment: z.string(),
|
||||
@@ -276,6 +277,7 @@ export const ClientInboundSchema = z.object({
|
||||
export type ClientInbound = z.infer<typeof ClientInboundSchema>;
|
||||
|
||||
export const ClientRecordSchema = z.object({
|
||||
adTag: z.string(),
|
||||
allowedIPs: z.string(),
|
||||
auth: z.string(),
|
||||
comment: z.string(),
|
||||
|
||||
@@ -119,6 +119,7 @@ interface FormState {
|
||||
wgPreSharedKey: string;
|
||||
wgAllowedIPs: string;
|
||||
secret: string;
|
||||
adTag: string;
|
||||
}
|
||||
|
||||
function emptyForm(): FormState {
|
||||
@@ -148,6 +149,7 @@ function emptyForm(): FormState {
|
||||
wgPreSharedKey: '',
|
||||
wgAllowedIPs: '',
|
||||
secret: '',
|
||||
adTag: '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -253,6 +255,7 @@ export default function ClientFormModal({
|
||||
wgPreSharedKey: client.preSharedKey || '',
|
||||
wgAllowedIPs: client.allowedIPs || '',
|
||||
secret: client.secret || '',
|
||||
adTag: client.adTag || '',
|
||||
};
|
||||
if (et < 0) {
|
||||
next.delayedStart = true;
|
||||
@@ -538,7 +541,13 @@ export default function ClientFormModal({
|
||||
}
|
||||
|
||||
if (showMtproto) {
|
||||
const adTag = form.adTag.trim();
|
||||
if (adTag !== '' && !/^[0-9a-fA-F]{32}$/.test(adTag)) {
|
||||
messageApi.error(t('pages.inbounds.form.mtgAdTagInvalid'));
|
||||
return;
|
||||
}
|
||||
clientPayload.secret = form.secret;
|
||||
clientPayload.adTag = adTag;
|
||||
}
|
||||
|
||||
const externalLinks: ExternalLinkInput[] = form.externalLinks
|
||||
@@ -862,12 +871,22 @@ export default function ClientFormModal({
|
||||
</>
|
||||
)}
|
||||
{showMtproto && (
|
||||
<Form.Item label={t('pages.clients.mtprotoSecret')} extra={t('pages.clients.mtprotoSecretHint')}>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
<Input value={form.secret} style={{ flex: 1 }} onChange={(e) => update('secret', e.target.value)} />
|
||||
<Button aria-label={t('regenerate')} icon={<ReloadOutlined />} onClick={regenerateMtprotoSecret} />
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
<>
|
||||
<Form.Item label={t('pages.clients.mtprotoSecret')} extra={t('pages.clients.mtprotoSecretHint')}>
|
||||
<Space.Compact style={{ display: 'flex' }}>
|
||||
<Input value={form.secret} style={{ flex: 1 }} onChange={(e) => update('secret', e.target.value)} />
|
||||
<Button aria-label={t('regenerate')} icon={<ReloadOutlined />} onClick={regenerateMtprotoSecret} />
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('pages.clients.mtprotoAdTag')} extra={t('pages.clients.mtprotoAdTagHint')}>
|
||||
<Input
|
||||
value={form.adTag}
|
||||
allowClear
|
||||
placeholder="0123456789abcdef0123456789abcdef"
|
||||
onChange={(e) => update('adTag', e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
|
||||
@@ -38,6 +38,7 @@ export const ClientRecordSchema = z.object({
|
||||
preSharedKey: z.string().optional(),
|
||||
keepAlive: z.number().optional(),
|
||||
secret: z.string().optional(),
|
||||
adTag: z.string().optional(),
|
||||
createdAt: z.number().optional(),
|
||||
updatedAt: z.number().optional(),
|
||||
}).loose();
|
||||
|
||||
@@ -17,6 +17,11 @@ export type MtprotoDomainFronting = z.infer<typeof MtprotoDomainFrontingSchema>;
|
||||
// default domain used when generating a new client's secret.
|
||||
export const MtprotoClientSchema = z.object({
|
||||
secret: z.string().default(''),
|
||||
adTag: z
|
||||
.string()
|
||||
.regex(/^[0-9a-fA-F]{32}$/, 'pages.inbounds.form.mtgAdTagInvalid')
|
||||
.or(z.literal(''))
|
||||
.optional(),
|
||||
email: z.string().min(1),
|
||||
limitIp: z.number().int().min(0).default(0),
|
||||
totalGB: z.number().int().min(0).default(0),
|
||||
|
||||
Reference in New Issue
Block a user