diff --git a/CLAUDE.md b/CLAUDE.md index cb33903fc..fe2b10c2b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,9 +14,11 @@ file locations when it can answer in one hop. API. MTProto inbounds run a second managed child — the `mtg-multi` binary (`github.com/mhsanaei/mtg-multi`, a multi-secret fork built from source; `internal/mtproto/`) — outside Xray, one process per inbound serving each - client's FakeTLS secret via the fork's `[secrets]` section. Client edits are - hot-applied through the fork's `POST /reload` so connections survive; the - manager falls back to a process restart on older binaries. + client's FakeTLS secret via the fork's `[secrets]` section (plus per-client + ad-tags via `[secret-ad-tags]`). Client and ad-tag edits are hot-applied + through the fork's management API (`PUT /secrets`, bearer-token guarded) so + connections survive; the manager falls back to a process restart on older + binaries. - Storage: SQLite by default (`/etc/x-ui/x-ui.db` on Linux; the executable dir on Windows), PostgreSQL optional (`XUI_DB_TYPE` / `XUI_DB_DSN`). The CGo SQLite driver (`mattn/go-sqlite3`) needs a C compiler — `CGO_ENABLED=0` builds fail. diff --git a/docs/architecture.md b/docs/architecture.md index b31cf9296..985db50d7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -22,9 +22,10 @@ separate HTTP server serves **subscription links** to end users. The panel supervises **two managed child processes**: Xray-core itself and — when MTProto inbounds exist — the `mtg-multi` Telegram-proxy binary (`github.com/mhsanaei/mtg-multi`, a multi-secret fork built from source; `internal/mtproto/`). One process per inbound serves -every attached client's FakeTLS secret through the fork's `[secrets]` section. A client edit -is hot-applied via the fork's `POST /reload` endpoint (connections survive), with a process -restart as the fallback on older binaries. +every attached client's FakeTLS secret through the fork's `[secrets]` section, plus optional +per-client sponsored-channel ad-tags via `[secret-ad-tags]`. A client or ad-tag edit is +hot-applied via the fork's management API (`PUT /secrets`, guarded by a per-process bearer +token), with a process restart as the fallback on older binaries. Servers and processes, all launched from `main.go`: diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 78e29e0a2..feaf1e2e9 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -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", diff --git a/frontend/src/generated/examples.ts b/frontend/src/generated/examples.ts index d5d241860..1d48a2348 100644 --- a/frontend/src/generated/examples.ts +++ b/frontend/src/generated/examples.ts @@ -214,6 +214,7 @@ export const EXAMPLES: Record = { "token": "new-token-string" }, "Client": { + "adTag": "0123456789abcdef0123456789abcdef", "allowedIPs": [ "" ], @@ -248,6 +249,7 @@ export const EXAMPLES: Record = { "inboundId": 0 }, "ClientRecord": { + "adTag": "", "allowedIPs": "", "auth": "", "comment": "", diff --git a/frontend/src/generated/schemas.ts b/frontend/src/generated/schemas.ts index 3d7fcddca..e644df455 100644 --- a/frontend/src/generated/schemas.ts +++ b/frontend/src/generated/schemas.ts @@ -1066,6 +1066,10 @@ export const SCHEMAS: Record = { "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 = { }, "ClientRecord": { "properties": { + "adTag": { + "type": "string" + }, "allowedIPs": { "type": "string" }, @@ -1280,6 +1287,7 @@ export const SCHEMAS: Record = { } }, "required": [ + "adTag", "allowedIPs", "auth", "comment", diff --git a/frontend/src/generated/types.ts b/frontend/src/generated/types.ts index 256e83a53..09606d08a 100644 --- a/frontend/src/generated/types.ts +++ b/frontend/src/generated/types.ts @@ -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; diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index 0cf29f6c5..e57e2e1ad 100644 --- a/frontend/src/generated/zod.ts +++ b/frontend/src/generated/zod.ts @@ -240,6 +240,7 @@ export const ApiTokenViewSchema = z.object({ export type ApiTokenView = z.infer; 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; export const ClientRecordSchema = z.object({ + adTag: z.string(), allowedIPs: z.string(), auth: z.string(), comment: z.string(), diff --git a/frontend/src/pages/clients/ClientFormModal.tsx b/frontend/src/pages/clients/ClientFormModal.tsx index 6143d7d0b..a76c61c47 100644 --- a/frontend/src/pages/clients/ClientFormModal.tsx +++ b/frontend/src/pages/clients/ClientFormModal.tsx @@ -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 && ( - - - update('secret', e.target.value)} /> -