Commit Graph

11 Commits

Author SHA1 Message Date
MHSanaei b177b98ce8 feat(mtproto): expose mtg-multi's per-IP pending-handshake limit
mtg-multi v1.16.0 adds [defense.pending-handshakes] max-per-ip, which
closes new connections from an IP already holding that many
unauthenticated handshakes, so one address cannot pin the workers other
clients need. The MTProto inbound form now carries it as
pendingHandshakesMaxPerIp; the key is written only when set above 0, so
existing inbounds render a byte-identical config and are not restarted.
A change to it moves the structural fingerprint and restarts the sidecar.

dry-run is left out on purpose: it only feeds the pending_handshake_limit
Prometheus metric, which the panel does not surface. The hint warns that
clients behind carrier NAT share the limit.
2026-10-14 16:46:20 +02:00
BlindMaster24 84c5aef4a1 fix(panel): probe UDP outbounds and hide the block outbound from the mtproto egress picker (#6525)
* fix(panel): match the probe and egress readers to what the core loads

Two readers left over from the case-sensitivity sweep still disagreed with
the core, both raised reviewing #6523.

isUdpOutbound compared the protocol id and the transport name exactly. The
core lowercases both before it resolves them (infra/conf/loader.go:46 for
the id, TransportProtocol.Build at infra/conf/transport_internet.go:16-17
for the name), so an outbound spelled "WireGuard" or a stream named "KCP"
still built a UDP handler but was probed with a dial-only TCP request, and
Test All Outbounds reported a working outbound as down.

The mtproto egress picker asked for outbound tags without excludeBlackhole,
so the block outbound stayed selectable there. Choosing it looks like a
working selection and discards that inbound's Telegram traffic.

* fix(panel): recognise the mkcp transport alias and pin the picker's field id

Review findings on #6525.

TransportProtocol.Build resolves both "kcp" and "mkcp" to the same mKCP
transport, so comparing the transport name against "kcp" alone left a
template spelling "network": "mkcp" in the TCP lane and reported a working
outbound as down — the same trigger this PR already fixed for the "KCP"
capitalisation.

The egress picker now carries an explicit id, the way the inbound form's
protocol select does, so the test addresses that field rather than the first
searchable select on the page and reuses the shared dropdown helper instead
of duplicating it.
2026-09-14 16:00:13 +02:00
Sanaei 92fb94d856 Move to TypeScript 7 and the oxc toolchain (oxlint + oxfmt) (#6262)
* chore(frontend,docs): move to TypeScript 7 and replace ESLint with oxlint

TypeScript 7 is the native Go port and ships no programmatic compiler
API, so typescript-eslint cannot run at all: it peer-pins
typescript >=4.8.4 <6.1.0 (canary too) and hard-crashes with
"typescript-eslint does not support TS 7.0". Upstream support is
tracked in typescript-eslint#10940 and targets TS >=7.1.

Rather than wait, or carry Microsoft's side-by-side alias (which keeps
a second TS 6 install alive purely to feed the linter), both projects
move to oxlint, which never depended on the TypeScript API.

Typecheck drops from ~9.7s to ~2.2s and 167 packages leave frontend/.

oxlint has no no-restricted-syntax, so the #6121/#6127 cleared-
InputNumber guard is reimplemented as a JS plugin in
frontend/tools/oxlint/. It was verified to still fire in
pages/settings/** and pages/xray/** and to stay exempt in *Modal.tsx.

The type-aware @deprecated sweep survives too, as
`npm run lint:deprecated`: oxlint's type-aware mode runs on
oxlint-tsgolint, which drives the TS 7 typescript-go checker, so the
TS 7 move is what makes it possible.

Behaviour is preserved rather than tightened. jsx-a11y/prefer-tag-over-role
is off in both configs because it was never part of the recommended sets
ESLint actually ran, and oxlint honours the existing eslint-disable
comments, so no source churn was needed.

Two real fixes fell out of the stricter linting:
- outbound-link-parser.test.ts used `out?.streamSettings` behind an `as`
  cast, which hid the optional chain from ESLint and would throw on a
  null parse; the rest of the file already used `out!`.
- InputAddon's conditional role/tabIndex/onKeyDown is genuinely
  accessible but oxlint cannot evaluate it, so it gets a scoped disable.

* chore(docs): replace Prettier with oxfmt

oxfmt is the oxc project's Prettier-compatible formatter, so this pairs
with the oxlint move and drops the last JS-based tool from the docs
toolchain.

The swap is behaviour-preserving. Running Prettier and oxfmt over the
same files, with the existing .prettierrc.json settings migrated via
`oxfmt --migrate=prettier`, produces byte-identical output on every
file. (Comparing them outside the project directory is misleading:
Prettier silently falls back to its defaults when it cannot find its
config, which looks like a mismatch but is not one.)

The 18 files reformatted here were already failing `pnpm format:check`
before this change — Prettier wanted the exact same edits. The check is
not part of docs-ci.yml, which is why the drift went unnoticed.

.prettierignore becomes ignorePatterns in .oxfmtrc.json, keeping the
deliberate MDX exclusion: reflowing MDX prose merges headings into
paragraphs and collapses lists inside Steps/Callout components. Both
that and the generated fumadocs-openapi reference output were verified
untouched.

oxfmt is pinned to 0.63.0 rather than latest. pnpm 11's built-in
minimumReleaseAge policy rejects same-day releases, and 0.64.0 would
have made pnpm silently append 20 waiver lines to pnpm-workspace.yaml.

* style(frontend): adopt oxfmt and format src

frontend/ has never had a formatter, so this reformats 344 of 497 files
in src/. The change is purely whitespace, quoting and line wrapping —
no logic is touched. It is kept in its own commit so it does not bury
the TypeScript 7 / oxlint migration or the git blame for the code
itself.

Settings match docs/ and the code as it was already written: single
quotes, semicolons, trailing commas, 2-space indent, 100 columns. That
was measured rather than assumed — src/ was already uniformly
single-quoted and 2-space indented, with p90 line length at 75.

Formatting is scoped to src/ (mirroring `oxlint src`) and
.oxfmtrc.json ignores src/generated. Both matter: `make gen-check`
compares src/generated and public/openapi.json, and
`make msw-worker-check` byte-compares public/mockServiceWorker.js
against the installed MSW runtime, so reformatting any of them breaks
the gate.

Reflowing also moves `eslint-disable-next-line` comments off the line
they guard, which broke two suppressions that had been silently
correct before:
- clone-inbound-modal.test.tsx: the object literal became multi-line,
  leaving `} as any;` four lines below its no-explicit-any disable.
- ClientsPage.tsx: the useMemo dependency array moved onto its own
  line, out from under its exhaustive-deps disable.
Both comments were relocated onto the line they actually guard, and
verified to still suppress by removing them and watching the errors
return.

* ci: enforce formatting in CI and make verify

Adding oxfmt in the previous two commits gave both projects a formatter
but nothing that checks it, which is how docs/ had already drifted to 18
unformatted files: docs-ci.yml runs typecheck, lint, test and build, but
never format:check, so Prettier's complaints were only ever visible to
whoever ran it by hand.

Wire `format:check` into the frontend job in ci.yml and the docs job in
docs-ci.yml, and add a `format-check` target to `make verify` so the
local gate keeps mirroring CI as the Makefile header promises.

Verified the step actually bites rather than passing vacuously: adding
a badly formatted line to a source file in each project makes both
`make format-check` and `pnpm format:check` fail, and reverting it makes
them pass again.

No workflow referenced ESLint or Prettier by name — they all invoke the
package scripts — so the tooling swap needed no other CI changes.

* ci: trigger CI on Makefile changes

The path filters listed **.go, go.mod, go.sum, frontend/**, .nvmrc and
ci.yml itself, but not the Makefile — so a change to the canonical task
runner that ci.yml is meant to mirror could land without any job
running. The previous commit, which edits both, only triggers because
it happens to touch ci.yml too.

* fix(frontend): replace deprecated Ant Design 6 APIs in the geo components

`npm run lint:deprecated` reported five uses of props Ant Design 6 has
deprecated. All five are gone, and the matching runtime warnings no
longer appear in the test output.

Tag `bordered={false}` becomes `variant="filled"` and Space `direction`
becomes `orientation`; both are the one-to-one replacements named in
antd's own deprecation messages, and `direction`/`orientation` share the
same Orientation type.

Input `addonAfter` is the one that is not a rename. It becomes a
`Space.Compact block` wrapping the Input and the browse Button, which is
antd's documented migration. `block` keeps the field filling its form
row as the addon did. Note this is a deliberate visual change: the
button used to be a borderless `type="text"` icon sitting inside the
addon's grey box, and is now a regular button whose border joins the
input. The tooltip, aria-label, ref, id and onBlur wiring are unchanged,
so the react-hook-form binding in RuleFormModal and the existing tests
still address it the same way.

Only these five were deprecated. The other `bordered` props in the tree
sit on QRCode, Table, Descriptions and Alert, where the prop is not
deprecated, and these were the only two Space `direction` uses in the
codebase.

* fix(frontend): restore lint rules lost in the oxlint migration, and test the guard

Addresses the review on #6262.

The frontend config re-enabled only no-explicit-any and no-unused-vars
and left the rest of tseslint's recommended set to oxlint's correctness
category. It does not cover all of it. Confirmed by linting one probe
file against both configs: docs/ (which enumerates the rules) reports
all nine, frontend/ reported four. So ban-ts-comment,
no-empty-object-type, no-namespace, no-require-imports and
no-unsafe-function-type had silently stopped being enforced — a `//
@ts-ignore` or a `namespace` block would have landed unflagged. The ten
rules are now mirrored from docs/.oxlintrc.json, and src/ still passes.

The #6121/#6127 guard was 57 lines of hand-written AST walking with no
test. It now has one: fixtures for the three banned shapes plus an
onNumber()-wrapped control, asserting the rule fires three times and
that .oxlintrc.json still wires it to the right paths. Verified it fails
for the right reason by making walk() enumerate nothing, which is the
silent-death mode the review described — the traversal depends on
Object.keys() seeing AST children as own enumerable properties.

The fixtures deliberately violate the rule, so their oxlint config is
named guard.oxlintrc.json rather than .oxlintrc.json: oxlint discovers
nested configs by directory, which would otherwise turn the fixtures
into three lint errors. The test passes it explicitly with -c.

Also from the review:
- lint and format now cover tools/ as well as src/, so the one piece of
  hand-written lint logic in the repo is no longer the least covered
  file in it.
- lint-staged runs oxfmt before oxlint --fix. Formatting became a hard
  CI gate in this PR while the hook only ran the linter, so a commit
  could pass the hook and fail CI on formatting alone.
- .oxfmtrc.json ignores public/, so the artefacts that make gen-check
  and make msw-worker-check byte-compare stay safe even if oxfmt is
  invoked without a path argument.
- The MDX and generated-reference rationales that .prettierignore
  carried are back as comments in docs/.oxfmtrc.json — oxlint and oxfmt
  both accept JSONC, so relocating them was unnecessary.

Not applied: the review also suggested restoring ../internal/web/dist to
the ignore lists. Both tools reject `..` patterns outright ("patterns
are resolved within the config file's directory"), and being outside
frontend/ it is unreachable anyway.
2026-08-19 15:36:27 +02:00
Sanaei 61e12e4c29 Frontend dev tooling (Husky, lint-staged, MSW, Storybook) + full React Hook Form migration (#5859)
* chore(frontend): add husky + lint-staged pre-commit gate

Wire a local pre-commit gate that runs eslint --fix on staged
frontend TypeScript via lint-staged. Because the only package.json
lives in frontend/ while the git root is one level up, the prepare
script installs husky hooks at frontend/.husky from the repo root
(cd .. && husky frontend/.husky), and the pre-commit hook cd's into
frontend/ before invoking lint-staged so node_modules resolves.

* test(frontend): add MSW request mocking

Add Mock Service Worker so tests can exercise the real http-init.ts
request pipeline (CSRF acquisition, 403 refetch-and-retry, body
parsing) instead of only stubbing HttpUtil. A node setupServer is
started for the vitest unit project with onUnhandledRequest bypass so
the existing HttpUtil spies and 55 component tests are untouched; the
browser worker is copied to public/ for Storybook and dev use.

* chore(frontend): add Storybook + component stories

Set up Storybook 10 on the React-Vite builder (compatible with the
pinned Vite 8.1.3 and React 19). The preview decorator mirrors the
vitest component harness: an Ant Design ConfigProvider with a
light/dark toolbar toggle and an en-US i18next instance. main.ts
neutralizes the app vite config bits that do not belong in a component
workshop (the three-entry rollup input, renderBuiltUrl, and the shared
dist outDir) so build-storybook can never clobber internal/web/dist.
Seeds stories across the presentational library (viz, ui, clients,
feedback). build-storybook is a local tool and is not wired into the
CI gate.

* feat(frontend): add React Hook Form primitives

Introduce the shared RHF layer that AntD inputs bind through, ahead of
migrating the forms off Ant Design's Form store:
- FormField wraps a Controller in an Ant Design Form.Item shell,
  reconciling the value/onChange shapes of Input, Switch, InputNumber,
  Select and friends via normalizeAntdOnChange, with input/output
  transforms and Zod-issue-key error messages resolved through t().
- useZodForm wires zodResolver (Zod 4) with the AntD-matching modes
  (validate on submit, then live) and shouldUnregister false so hidden
  and unmounted-tab fields keep their values.
- rhfZodValidate covers the rare per-field rule sites.
Covered by a FormField test exercising normalization, transforms, and
resolver error surfacing.

* refactor(frontend): migrate Pattern-B leaf forms to React Hook Form

Move the controlled-useState leaf forms onto RHF via the FormField
primitive, keeping Ant Design components and each form's exact submit
behaviour (same safeParse, same toast on the first Zod issue, same
payload building):
- clients: ClientBulkAdjustModal, BulkAddToGroupModal, ClientBulkAddModal
- xray: RuleFormModal, BalancerFormModal, WarpModal, NordModal

Multi-control widgets that don't fit a single input (inbound dual
select, subId regen, expiry branches, the balancer tag warning) stay as
explicit Controller/setValue. Derived visibility now reads live values
through useWatch. FormField gains a required prop so migrated fields keep
their required-asterisk affordance.

Settings tabs are intentionally excluded: they are control-panel
components that live-patch a parent AllSetting via SettingListItem, not
Ant Design Form submit-forms.

* refactor(frontend): migrate LoginPage to React Hook Form

Replace the Ant Design Form store + antdRule per-field validation with
useForm + FormField. The AntD Form stays as the layout/submit wrapper,
now driving methods.handleSubmit(onSubmit) via onFinish. Username and
password validate through rhfZodValidate(LoginFormSchema.shape.*); the
two-factor field keeps its conditional required rule (only registered
when 2FA is enabled). Submit posts the same values to /login.

* refactor(frontend): migrate ClientFormModal to React Hook Form

Move the client add/edit form off controlled useState onto RHF while
preserving exact submit behaviour (same ClientFormSchema /
ClientCreateFormSchema safeParse, same toast, same payload + attach/
detach diff + external-links build). expiryDate is stored as an epoch
number (never a Dayjs) to survive RHF's value cloning, converted at the
DateTimePicker boundary. externalLinks uses useFieldArray with stable
ids. inboundIds and the derived show*/ss2022 visibility read live via
useWatch. Space.Compact button-group widgets stay manual Controllers so
the joined borders keep working.

* refactor(frontend): migrate Node and DNS modals to React Hook Form

Both are self-contained Pattern-A forms (no shared fragments). Replace
Form.useForm with useForm + FormProvider, Form.useWatch with useWatch,
setFieldValue with setValue, and partial validateFields([...]) with
methods.trigger([...]). Per-field antdRule becomes rhfZodValidate rules;
the Node scheme->tlsVerify cascade moves to FormField onAfterChange; the
DNS domains/expectIPs/unexpectIPs string arrays are driven by
useWatch + setValue. Submit runs through handleSubmit on the modal OK
button, preserving each form's exact validation, payload build, and
save/onConfirm behaviour.

* refactor(frontend): migrate HostFormModal to React Hook Form

The host external-proxy editor's outer form moves to useForm +
FormProvider. Security/tab visibility reads via useWatch; the three
json-form editors (HostMuxForm/HostSockoptForm/HostFinalMaskForm) are
bound as value/onChange black boxes through a Controller (their own
internal forms are unchanged). remark/inboundId keep their validation
via rhfZodValidate; submit runs through handleSubmit and builds the
same payload (isDisabled = !enable) and save call.

* refactor(frontend): migrate OutboundFormModal + fragments to React Hook Form

Move the outbound form cluster off Ant Design's Form store onto RHF.
The parent uses useForm + FormProvider with a watch() subscription for
the protocol reseed cascade and setValue-based network/security/xmux
cascades; the JSON<->Basic bridge and the formValuesToWirePayload
submit are preserved exactly. Every outbound transport/protocol/security
fragment now binds through FormField/useWatch via context.

The shared config editors stay untouched and are bound through small
value/onChange adapters (src/lib/xray/forms/fields: FinalMaskField,
SniffingField, SockoptCustomField) via Controller; HeaderMapEditor binds
directly. The host json-form wrappers that reuse the outbound MuxForm/
SockoptForm (HostMuxForm, HostSockoptForm, OutboundSubtreeJsonForm) move
to a local RHF provider to match. Outbound render/link tests pass
unchanged.

* refactor(frontend): migrate InboundFormModal + fragments to React Hook Form

Move the inbound add/edit form (the largest form in the panel) and its
transport/protocol/security fragments off Ant Design's Form store onto
RHF, mirroring the outbound migration. The parent uses useForm +
FormProvider with a watch() subscription for the protocol reseed
cascade (type==='change' guard so programmatic resets don't reseed) and
setValue-based network/security cascades; useSecurityActions drives the
TLS/Reality keypair + scan through setValue. Hidden pass-through
Form.Items are dropped (their values ride in the reset object and
survive via shouldUnregister:false), so getValues() still returns the
settings.clients subtree untouched. accounts / certificates / tun lists
use useFieldArray; the shared FinalMask/Sniffing/Sockopt editors bind
through the value/onChange adapters.

Submit keeps the manual InboundFormSchema.safeParse + formatInboundValidation
toast + formValuesToWirePayload exactly. The golden link/full fixtures
pass byte-for-byte, confirming identical wire output. inbound-form-blocks
test harness rewritten from a Form.useForm harness to an RHF provider.

* refactor(frontend): retire antdRule; document the RHF form pattern

All forms now build on React Hook Form, so the AntD-Form Zod adapter
antdRule (src/utils/zodForm.ts) has no remaining callers — remove it.
Update frontend/CLAUDE.md: forms use useZodForm + FormField from
components/form/rhf with zodResolver/rhfZodValidate validation; AntD
<Form> is layout-only; the shared FinalMask/Sniffing/Sockopt editors
stay AntD islands wrapped as value/onChange adapters bound via a
Controller.

* chore(frontend): cover esbuild in the allowScripts allowlist

esbuild (pulled in transitively by Vite/Vitest/Storybook) ships a
postinstall that npm's allow-scripts flags as uncovered on every
install. Its platform binary is delivered through the @esbuild/<platform>
optionalDependencies, so the postinstall isn't needed here; deny it like
the other entries to silence the warning.

* fix(frontend): restore label layout in Sniffing/FinalMask field adapters

The value/onChange adapters that wrap the shared SniffingFields and
FinalMaskForm editors put them in their own isolated AntD Form, but that
Form was missing the label layout the fields used to inherit from the
inbound/outbound parent form. Their labels rendered full-width instead
of the compact right-aligned column, so the Sniffing tab and the TCP
Masks / QUIC Params sections looked broken. Give both adapter forms the
same colon=false, labelCol/wrapperCol span 8/14, labelWrap layout.

* ci: add least-privilege permissions to Docs CI workflow

The docs-ci workflow had no explicit permissions block, so it inherited
the repository default for GITHUB_TOKEN. The build job only checks out
and builds the docs, so restrict it to contents: read, resolving the
CodeQL actions/missing-workflow-permissions alert.
2026-07-08 13:28:37 +02:00
MHSanaei ad7a0f8164 refactor(mtproto): manage ad-tags per client only
The inbound-level ad-tag duplicated the per-client override for no
gain: the fork's global tag applied to every secret anyway, so one
value had two homes and they could drift. The inbound form field, the
settings key, and the global ad-tag in the generated config and in the
PUT /secrets body are gone; the tag is set on each client instead.
Existing inbound-level values are intentionally not migrated; a
leftover settings key is stripped on the next save.
2026-07-07 12:19:26 +02:00
Sanaei 6214ff4edc fix(mtproto): stop dropping connections on client/inbound edits; add live updates + ad-tag (#5838)
* fix(mtproto): split the mtg fingerprint into structural and secrets parts

A reordered clients array in the stored settings used to read as a config
change because the fingerprint concatenated secrets in array order, and one
opaque fingerprint could not tell a restart-worthy change (bind address,
fronting, throttle) from a secret-set change a reload-capable mtg can absorb
in place. Sort the secret pairs so order stops mattering, and split the value
so the upcoming hot-reload path can decide between keeping, reloading, and
restarting the process.

* fix(mtproto): stop restarting mtg on every inbound edit

Saving an mtproto inbound tore down and respawned its mtg sidecar even when
nothing material changed, dropping every live Telegram connection: the update
path pushed DelInbound+AddInbound, and Remove deletes the manager's map entry,
so Ensure's fingerprint no-op gate could never fire. Route mtproto updates
through a single Ensure call so an edit that leaves the generated TOML alone
keeps the process, and only real config changes restart it.

Capturing the pre-edit protocol also fixes a latent leak: changing an
inbound's protocol away from mtproto never stopped the sidecar, because the
snapshot handed to the runtime already carried the new protocol and the
removal took the xray branch, leaving an orphaned mtg holding the port.

An mtproto push failure no longer requests an xray restart - xray cannot fix
the sidecar, and the 10s reconcile job self-heals it.

The regression test fakes mtg by re-executing the test binary, counting
spawns through a pid file: an unchanged save and a remark-only edit must keep
the process, a re-keyed secret must restart it.

* fix(mtproto): exclude depleted clients from the reconcile job to match the sync push

The 10s reconcile job derived mtg secret sets from raw inbound settings while
the interactive push filtered clients through buildRuntimeInboundForAPI, which
drops client_traffics-disabled (depleted or expired) clients. The two paths
therefore disagreed on the fingerprint - each disagreement one needless mtg
restart dropping live connections - and worse, the job kept serving depleted
clients' secrets indefinitely, so running out of traffic never actually cut an
mtproto client's access.

DesiredMtprotoInstances now builds the job's desired state with the same
depletion overlay the push uses (one bulk client_traffics query), drops
inbounds whose every secret is filtered away so their sidecar stops, and
AddInbound pushes the filtered payload too so an imported inbound carrying
disabled stats does not seed a fingerprint the next reconcile disagrees with.

* feat(mtproto): hot-reload mtg secrets in place instead of restarting

A client add, removal, re-key, or enable-toggle changes only the [secrets]
section of the generated config, yet the panel could apply it only by killing
and respawning the mtg sidecar, dropping every Telegram connection on that
inbound. Split the ensure decision three ways: an identical config is a no-op,
a secrets-only change rewrites the TOML on the same api port and asks mtg to
hot-swap it via POST /reload, and a structural change (or a failed reload)
falls back to the full stop-and-start.

The reload endpoint is served by the mhsanaei/mtg-multi fork; against an older
binary the POST 404s and the manager restarts exactly as before, so panel and
binary upgrades stay order-independent.

* feat(mtproto): apply single-client edits to the sidecar immediately

Client CRUD on an mtproto inbound was a runtime no-op, so an add, delete,
re-key, or enable-toggle only reached mtg on the next 10s reconcile. With the
sidecar now able to hot-reload, push the change straight after the edit commits:
applyLocalMtproto rebuilds the inbound's filtered client set and re-applies it,
so a new client works within a moment (and, on a reload-capable binary, without
disturbing the others) and deleting the last client stops the process.

The three interactive single-client paths (add, update, delete) call it; bulk
operations still ride the reconcile job, which converges to the same state.

* chore(mtproto): pin mtg-multi to the mhsanaei fork v1.13.3

The reload endpoint the panel now uses lives in the mhsanaei/mtg-multi fork, so
point the source-build pin (DockerInit.sh + both release.yml matrices) at it and
bump to v1.13.3. The install still produces the same mtg-multi binary name, so
the mtg-<os>-<arch> rename and everything downstream are unchanged. Docs and the
package comment note the hot-reload path and its restart fallback.

* feat(mtproto): apply live secret updates via the management API and add ad-tag

Two capabilities the mhsanaei/mtg-multi v1.13.3 fork exposes are now surfaced by
the sidecar manager.

Live updates go through PUT /secrets on the fork's management API instead of
POST /reload: the panel already holds the whole desired set per inbound, so it
sends secrets and the advertising tag as one JSON call that mtg applies
atomically, keeping every unchanged connection and closing only removed or
re-keyed ones. The config file is still written first so a restart or crash
recovery reproduces the state, and any non-200 (an older binary, a refused
connection) still falls back to a full restart.

Per-inbound ad-tag adds an optional 32-hex Telegram advertising tag plus
public-ipv4/public-ipv6 overrides. The ad-tag rides the reloadable secrets
fingerprint, so changing it hot-applies without dropping connections; the public
IPs are proxy-construction parameters and sit in the structural fingerprint, so a
change there restarts the process. Empty public IPs are omitted so mtg
auto-detects the reachable address.

* feat(inbounds): expose the mtproto ad-tag and public IP in the inbound form

Adds an Ad-tag field (validated as 32 hex characters) plus optional Public IPv4
and Public IPv6 overrides to the MTProto inbound form, backed by the same-named
settings the sidecar writes into the mtg config. The public IPs are optional —
left blank, mtg auto-detects the reachable address the ad-tag middle proxy needs.
English strings are added to every locale; the non-English ones carry the
English text until translated and fall back to it meanwhile.

* ci(mtproto): install mtg-multi from prebuilt release binaries

The fork now publishes release archives for every platform we package, so
download and unpack the matching mtg-multi-<ver>-<os>-<arch> binary instead of
compiling it from source with go install. Faster builds and no toolchain step,
and the archive's platform labels line up with our matrix; the produced
mtg-<os>-<arch> filenames are unchanged.

* i18n(mtproto): localize the ad-tag and public IP strings

The six mtgAdTag*/mtgPublicIp* keys shipped with English text in every locale as
a placeholder. Translate them into the twelve non-English locales (Arabic,
Spanish, Persian, Indonesian, Japanese, Portuguese-BR, Russian, Turkish,
Ukrainian, Vietnamese, and Simplified/Traditional Chinese); en-US is unchanged.

* retired goreportcard.com
2026-07-07 01:13:24 +02:00
MHSanaei d97bd8643e feat(mtproto): adopt dolonet/mtg-multi and make MTProto inbounds multi-client
Replace the upstream 9seconds/mtg sidecar with the dolonet/mtg-multi fork so a single MTProto inbound can serve many per-user secrets. Each panel client is now one named FakeTLS secret in the fork's [secrets] section: clients are first-class (attach/detach, limits, expiry, per-client tg:// links) exactly like every other protocol, mirroring the WireGuard multi-client model. Per-client traffic and online status come from the fork's /stats JSON API (its Prometheus output has no per-user label), fed into the existing email-keyed client_traffics accumulator; an optional throttle caps concurrent connections. A one-time seeder converts each legacy single-secret inbound into a one-client inbound.

The fork ships only linux/darwin amd64/arm64 binaries but is pure Go, so provisioning builds it from source for every supported platform (release.yml, DockerInit.sh) while keeping the panel-expected mtg-<os>-<arch> filename and the 'run' verb, so process.go is untouched. Also fixes a pre-existing update.sh gap that never renamed the mtg binary for armv6/armv7 updates.
2026-07-06 16:04:32 +02:00
nima1024m 71aca2018a feat(a11y): screen-reader & keyboard accessibility across the panel (#5486) (#5652)
* feat(a11y): label list, toolbar & dashboard actions for screen readers

Phase 1 of #5486 (Android TalkBack support). Icon-only controls across
the management surfaces previously announced only their untranslated
icon name (e.g. "edit", "ellipsis") or nothing at all.

- Add aria-label to icon-only row-action and toolbar buttons across
  inbounds, clients, groups, hosts, nodes and xray
  (outbounds/routing/dns/balancers) lists, plus the dashboard cards.
- Make clickable bare icons and AntD Card actions keyboard-operable via
  role/tabIndex + Enter/Space (new activateOnKey helper); convert mobile
  dropdown triggers to buttons so they open from the keyboard.
- Fix the sidebar hamburger's mislabeled aria-label (was the dashboard
  label) and translate previously-hardcoded outbound menu labels.

New i18n keys in all 13 locales: sort, menu.openMenu,
pages.xray.outbound.moveToTop.

* feat(a11y): label modal, QR and copy/download controls for screen readers

Phase 2 of #5486. Modal and overlay controls relied on tooltips (not a
reliable accessible name) or were bare clickable icons with no keyboard
or screen-reader support.

- Add aria-label to copy/QR/download/info icon buttons in the inbound and
  client info modals, sub-links modal, QR panel, backup/log modals, and
  to the bare search/select inputs of the attach/detach client modals.
- Make click-to-copy QR codes and the IP-log refresh/clear, geofile
  reload and log refresh icons keyboard-operable (role/tabIndex +
  Enter/Space) with translated labels.
- Label the 2FA code input; drop the QrPanel download-image string
  fallback now that the key exists.

New i18n key in all 13 locales: downloadImage.

* feat(a11y): label form fields and shared form components for screen readers

Phase 3 of #5486. Form controls and shared form widgets were largely
unlabelled, and several remove controls were not keyboard-operable.

- SettingListItem now ties its title to the control via aria-labelledby,
  giving accessible names to the ~90 settings-tab inputs at once.
- InputAddon gains button semantics (role/tabIndex/Enter+Space) and an
  ariaLabel prop when used as an interactive remove control.
- Sparkline charts expose a role="img" summary of their latest values.
- Add aria-label to add/remove/regenerate icon buttons and bare
  inputs/selects across inbound, client and xray (dns/routing/balancer/
  outbound) forms; make clickable remove icons keyboard-operable; mark
  decorative help/target icons aria-hidden; label the JSON editor,
  date-time clear button, header-map remove, notification select-all and
  remark token chips.

New i18n keys in all 13 locales: regenerate, jsonEditor,
pages.xray.balancer.{costMatch,costValue,costRegexp}.

* chore(a11y): add eslint-plugin-jsx-a11y harness and fix flagged interactions

Phase 4 of #5486. Adds eslint-plugin-jsx-a11y (recommended ruleset,
scoped to .tsx) so screen-reader/keyboard regressions fail lint.

- Make the mobile node-card header a proper keyboard disclosure
  (role=button, aria-expanded, Enter/Space activation that ignores
  clicks on the nested action buttons) and drop the now-redundant
  stop-propagation click handlers the linter flagged on card-action
  wrappers in the node, client and inbound mobile cards.
- Disable jsx-a11y/no-autofocus: the autofocus on the login field and
  modal primary inputs is intentional focus management that helps
  screen-reader and keyboard users land on the right control.

make lint passes with the a11y ruleset enforced.

* feat(a11y): cover remaining deferred spots (settings tabs, sockopt, API docs)

Completes the panel sweep for #5486 by labelling the spots previously
left out of phases 1-4:

- NotifyTimeField (Telegram notifications): the mode, interval, unit and
  custom-cron inputs now carry aria-labels.
- The Sockopt toggle in transport options.
- Settings category tabs in icons-only (mobile) mode now expose the tab
  name as the icon's aria-label instead of the raw icon name.
- The Swagger API-docs view is wrapped in a labelled region landmark.

New i18n keys in all 13 locales: pages.settings.notifyTime.{interval,unit}.

* feat(a11y): label shared xray form components and remark field

Code review surfaced frontend/src/lib/xray/forms/ — shared form components
used by the host and inbound JSON forms — which the initial audit missed.

- FinalMaskForm (TCP/UDP final-mask editor): label the icon-only add and
  regenerate buttons and make all six remove icons keyboard-operable
  (role/tabIndex/Enter+Space); adds useTranslation to its sub-components.
- CustomSockoptList: the remove icon is now keyboard-operable.
- SniffingFields: aria-label on the otherwise label-less destOverride select.
- RemarkTemplateField: aria-label on the remark-variable picker button.

New i18n key in all 13 locales: pages.inbounds.sniffingDestOverride.

* feat(a11y): label client info modal and WireGuard config block

After rebasing onto the WireGuard client-config feature, re-apply the
ClientInfoModal copy/QR/IP-log aria-labels (the modal was restructured
upstream, so the original labels did not carry over) and label the new
ConfigBlock component's copy/download/QR actions. ConfigBlock's action
wrapper keeps its stop-propagation handler (a non-interactive guard for
the Collapse header) under a scoped jsx-a11y exception.

* fix(frontend): let npm install jsx-a11y under ESLint 10

eslint-plugin-jsx-a11y@6.10.2 declares a peer range that stops at ESLint 9,
but the panel is on ESLint 10, so `npm ci` aborts with ERESOLVE even though
the plugin runs fine on ESLint 10 with flat config. Add an npm override so
jsx-a11y accepts the project's ESLint version. This keeps normal peer
resolution (recharts' react-is peer still auto-installs) — no global
legacy-peer-deps and no manual react-is pin needed.

* fix(a11y): size mobile row triggers and move node expand role to chevron

Address automated review on #5652:
- add size="small" to the inbound/client/node mobile-card "more" dropdown
  triggers so they match the adjacent small Switch and the established
  desktop RowActions pattern.
- move the node card-head disclosure semantics (role/tabIndex/aria-expanded/
  keyboard) onto the chevron affordance so the expand control is no longer a
  role="button" wrapping the Switch, info button and dropdown. Mouse
  click-anywhere-to-expand is preserved on the header div.
2026-06-29 12:51:29 +02:00
MHSanaei 5eec178483 feat(mtproto): route Telegram egress through Xray routing rules
Add a per-inbound "Route through Xray" toggle (off by default) plus an
optional outbound picker on MTProto inbounds. mtg only supports a SOCKS5
upstream, so when enabled the panel injects a loopback SOCKS bridge into
the generated Xray config — tagged with the inbound's own tag — and mtg
dials Telegram through it via a [network] proxies upstream. The router
then governs Telegram egress: matchable in the Routing tab, or forced to a
chosen outbound/balancer via the picker.

- mtproto: Instance carries RouteThroughXray + XrayRoutePort (in the
  fingerprint); InstanceFromInbound parses them; renderConfig emits the
  socks5 [network] upstream; freeLocalPort exported as FreeLocalPort.
- xray.go: injectMtprotoEgress appends the loopback SOCKS bridge and
  prepends an optional inboundTag->outbound/balancer rule, hot-appliable
  like injectPanelEgress.
- inbound.go: backend-owned egress port persisted in settings, allocated
  once and carried across edits (stored value wins); stripped with the
  inert outboundTag when routing is off; allocation failure fails the save;
  routed add/update/del force a config regen.
- mtproto_job: skip folding mtg metrics for routed inbounds (the bridge,
  carrying the inbound tag, is metered by xray_traffic_job) to avoid
  double-counting.
- frontend: toggle + outbound/balancer Select (useOutboundTags) on the
  MTProto form; i18n keys for all locales.
2026-06-12 17:58:45 +02:00
MHSanaei 6c1594693d feat(mtproto): add domain-fronting and essential mtg options
Expose mtg's [domain-fronting] section (ip/port/proxy-protocol) plus
proxy-protocol-listener, prefer-ip, and debug on MTProto inbounds. Each
key is written to the generated mtg-<id>.toml only when set, so mtg's own
defaults apply otherwise. The instance fingerprint now covers these
fields, so editing an option restarts the sidecar.

Since MTProto is mtg-served (not Xray), sniffing does not apply: hide the
Sniffing tab and the Advanced sniffing sub-editor, drop it from the
Advanced "All" JSON view, and emit empty sniffing in the wire payload,
all gated by a new canEnableSniffing predicate.
2026-06-09 12:44:04 +02:00
Sanaei 1ca5924a44 feat(mtproto): add MTProto (FakeTLS) protocol via managed mtg sidecar (#5076)
* feat(mtproto): add MTProto (FakeTLS) protocol via managed mtg sidecar

Xray-core has no mtproto proxy, so mtproto inbounds run as standalone
mtg (9seconds/mtg) sidecar processes managed by the panel — one per
inbound — and are excluded from the generated Xray config entirely.

- model: MTProto protocol constant, validator, and FakeTLS secret
  helpers (GenerateFakeTLSSecret/HealMtprotoSecret)
- mtproto package: per-inbound mtg process manager with reconcile,
  graceful stop, and best-effort Prometheus traffic scraping
- runtime: delegate mtproto inbounds to the mtg manager instead of the
  Xray gRPC API; skip mtproto when building the Xray config
- web: boot reconcile + StopAll wiring, periodic reconcile/traffic job,
  port-conflict transport, secret healing on inbound add/update
- sub: tg:// proxy share-link generation
- frontend: protocol option, Zod schema, Protocol tab (FakeTLS domain +
  regenerable secret), info-modal link, and i18n
- provisioning: fetch mtg v2.2.8 in install.sh, DockerInit.sh, and the
  Linux + Windows release workflows

* fix

* fix

* fix: address Copilot review comments on mtproto PR

- web/web.go: create NewMtprotoJob once and reuse for cron + initial run
- mtproto/manager.go: StopAll cleans up per-inbound config files on shutdown
- mtproto/manager.go: CollectTraffic releases mutex before HTTP scrapes to
  avoid blocking Ensure/Reconcile/Remove during network I/O
- database/model/model.go: panic on crypto/rand failure in mtprotoRandomMiddle
  instead of silently producing a weak all-zero secret
- install.sh: fix chmod to handle renamed bin/mtg-linux-arm on armv5/v6/v7
2026-06-08 14:28:19 +02:00