Repository navigation
feat(mtproto): add MTProto proxy backend via telemt - #61
T3ST3ST3R0N wants to merge 16 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughAdds an MTProto backend around the external ChangesMTProto Backend
Sequence Diagram(s)sequenceDiagram
participant Controller
participant MTProto
participant processManager
participant apiClient
participant metricsScraper
Controller->>MTProto: New(ctx, cfg, mtConfig, users)
MTProto->>processManager: newProcessManager(executable, config, pid)
MTProto->>processManager: Start()
MTProto->>apiClient: Health() via waitForHealth()
MTProto-->>Controller: *MTProto
Controller->>MTProto: SyncUser(ctx, user)
MTProto->>apiClient: upsertUser / deleteUser
Controller->>MTProto: SyncUsers(ctx, users)
MTProto->>processManager: Reload() or Restart()
MTProto->>apiClient: waitForRuntimeUsers()
Controller->>MTProto: GetStats(ctx, request)
MTProto->>metricsScraper: Scrape(ctx)
metricsScraper-->>MTProto: per-user metrics
MTProto->>MTProto: trafficTracker.Sync() and stat aggregation
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/mtproto/config.go`:
- Around line 62-67: The trailing JSON check in NewConfig is too permissive
because decoder.Decode(&extra) leaves extra nil for a trailing null, so multiple
JSON documents can slip through. Update the logic around the decoder.Decode
handling in config.go to explicitly detect any successful second decode,
including null, and reject it as a second JSON value rather than relying on
extra != nil. Keep the existing error path and use the NewConfig trailing-decode
flow to ensure only exactly one JSON object is accepted.
In `@backend/mtproto/integration_test.go`:
- Around line 91-93: The GetUser assertion in the integration test only checks
for any error, so it can pass on the wrong failure mode. Update the test around
mt.apiClient.GetUser to assert the returned error is apiStatusError.NotFound()
specifically, using the existing NotFound helper/matcher already used in this
package. Keep the check focused on the removed-user case rather than treating
any non-nil error as success.
In `@backend/mtproto/mtproto_test.go`:
- Around line 35-38: The mtproto_test.go failure check for NewConfig can panic
because it dereferences cfg inside the t.Fatalf path when cfg is nil. Update the
test around NewConfig and the custom inbound tag assertion so it first handles
err (and nil cfg) before referencing cfg.InboundTag, keeping the failure message
in the mtproto_test.go test case informative without risking a nil-pointer
panic.
In `@backend/mtproto/mtproto.go`:
- Around line 103-105: The startup failure path in MTProto construction
currently calls process shutdown directly and bypasses the cleanup logic in
MTProto.Shutdown(), which leaves log resources uncleared when New returns nil.
Update the failure handling in New to invoke mt.Shutdown() instead of
mt.process.Shutdown(), so the full teardown path runs, including CloseLogs() and
any other cleanup owned by MTProto.
- Around line 511-516: The removal convergence check in the mtproto transition
logic is only comparing `previousDesired` against `desired`, so the sentinel
account can be missed when moving from zero desired users to real users. Update
the `expectedAbsent`/convergence logic in `mtproto.go` to explicitly account for
the sentinel runtime user in addition to `previousDesired`, and make sure the
check in this flow rejects success until the sentinel is actually removed.
- Around line 625-634: The rendered telemt config is being written with
world-readable permissions, which exposes the API bearer token and MTProto
secrets. Update writeFileAtomic so the temporary file written by os.WriteFile
uses private permissions instead of 0644, and ensure the surrounding write path
in mtproto.go preserves those restricted permissions when the file is renamed.
Focus on the writeFileAtomic helper and its temp file creation.
- Around line 585-602: The sentinel user creation in newSentinelUser currently
falls back to predictable Username and Secret values when rand.Read fails, so
change it to fail closed by returning an error instead of constructing a
runtimeUser with deterministic bytes. Update New to handle the new error return
from newSentinelUser and propagate it to the caller rather than continuing
startup with known credentials. Make sure any call sites or tests that reference
newSentinelUser or New are adjusted to expect the error path.
In `@backend/mtproto/process.go`:
- Around line 135-136: The log channel is being closed while the stdout/stderr
scanner goroutines may still be sending, which can panic during normal shutdown.
Update the shutdown flow in process.go around process.Start/captureLogs and the
CloseLogs path so p.captureLogs(stdout) and p.captureLogs(stderr) finish before
p.logs is closed, or keep p.logs open for the full manager lifetime. Use the
existing captureLogs and CloseLogs symbols to locate the fix and ensure
process.Shutdown/MTProto.Shutdown cannot race with pending log sends.
- Around line 66-82: `detectVersion()` and `Reload()` currently wait on
`CombinedOutput()` without any deadline, so a stuck `telemt` process can block
startup or reload indefinitely. Update the command creation path in
`processManager` to use a context-aware command (for example via
`exec.CommandContext`) for both the version check and reload execution, and
apply a short timeout appropriate for these operations. Make sure the timeout is
enforced in both `detectVersion` and `Reload`, and return a distinct
timeout-related error so callers can distinguish it from generic command
failures.
In `@Dockerfile.mtproto`:
- Around line 21-31: The runtime stage in the Dockerfile currently leaves the
container running as root, so update the final stage to create and switch to a
dedicated non-root user before ENTRYPOINT. Keep the existing COPY steps for main
and telemt, then add ownership/permission setup as needed and switch user in the
runtime image so the container no longer executes as root; reference the final
stage around FROM alpine:latest, COPY, and ENTRYPOINT.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 48cc9f9f-74c5-4b6a-8547-e4a25311cfbe
⛔ Files ignored due to path filters (2)
common/service.pb.gois excluded by!**/*.pb.gocommon/service_grpc.pb.gois excluded by!**/*.pb.go
📒 Files selected for processing (17)
.env.exampleDockerfile.mtprotoMakefilebackend/mtproto/client.gobackend/mtproto/config.gobackend/mtproto/integration_test.gobackend/mtproto/metrics.gobackend/mtproto/mtproto.gobackend/mtproto/mtproto_test.gobackend/mtproto/process.gobackend/mtproto/process_unix.gobackend/mtproto/process_windows.gobackend/mtproto/toml.gobackend/mtproto/tracker.gocommon/service.protoconfig/config.gocontroller/controller.go
| FROM alpine:latest | ||
|
|
||
| LABEL org.opencontainers.image.source="https://github.com/PasarGuard/node" | ||
|
|
||
| RUN apk update && apk add --no-cache ca-certificates | ||
|
|
||
| WORKDIR /app | ||
| COPY --from=builder /src/main /app/main | ||
| COPY --from=builder /usr/local/bin/telemt /usr/local/bin/telemt | ||
|
|
||
| ENTRYPOINT ["./main"] |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Drop root in the runtime stage.
The final image never switches away from root, so a compromise in either main or telemt gets full root privileges inside the container. Add a dedicated runtime user here; if you need privileged ports, grant only CAP_NET_BIND_SERVICE at deploy time instead of keeping the whole container root.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Dockerfile.mtproto` around lines 21 - 31, The runtime stage in the Dockerfile
currently leaves the container running as root, so update the final stage to
create and switch to a dedicated non-root user before ENTRYPOINT. Keep the
existing COPY steps for main and telemt, then add ownership/permission setup as
needed and switch user in the runtime image so the container no longer executes
as root; reference the final stage around FROM alpine:latest, COPY, and
ENTRYPOINT.
Source: Linters/SAST tools
caf7481 to
fc734c2
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/mtproto/client.go`:
- Around line 72-79: `telemtPatchUserRequest` is omitting zero/empty fields, so
`UpdateUsers()` cannot clear existing telemt settings during a PATCH. Update the
request model and patch flow in `UpdateUsers()` to preserve explicit clears for
`UserAdTag`, `ExpirationRFC3339`, and the limit/quota fields, either by
switching those fields to pointers/nullable values or by adding a separate
clear-path so omitted values are not re-sent as old state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0b8b481a-cd91-45bf-96ea-cc5e38d17b49
⛔ Files ignored due to path filters (2)
common/service.pb.gois excluded by!**/*.pb.gocommon/service_grpc.pb.gois excluded by!**/*.pb.go
📒 Files selected for processing (17)
.env.exampleDockerfile.mtprotoMakefilebackend/mtproto/client.gobackend/mtproto/config.gobackend/mtproto/integration_test.gobackend/mtproto/metrics.gobackend/mtproto/mtproto.gobackend/mtproto/mtproto_test.gobackend/mtproto/process.gobackend/mtproto/process_unix.gobackend/mtproto/process_windows.gobackend/mtproto/toml.gobackend/mtproto/tracker.gocommon/service.protoconfig/config.gocontroller/controller.go
✅ Files skipped from review due to trivial changes (1)
- .env.example
🚧 Files skipped from review as they are similar to previous changes (8)
- config/config.go
- backend/mtproto/process_windows.go
- backend/mtproto/integration_test.go
- common/service.proto
- backend/mtproto/process_unix.go
- controller/controller.go
- backend/mtproto/tracker.go
- Makefile
| type telemtPatchUserRequest struct { | ||
| Secret string `json:"secret,omitempty"` | ||
| UserAdTag string `json:"user_ad_tag,omitempty"` | ||
| MaxTCPConns *int `json:"max_tcp_conns,omitempty"` | ||
| ExpirationRFC3339 string `json:"expiration_rfc3339,omitempty"` | ||
| DataQuotaBytes *uint64 `json:"data_quota_bytes,omitempty"` | ||
| MaxUniqueIPs *int `json:"max_unique_ips,omitempty"` | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== client.go around telemtPatchUserRequest ==\n'
sed -n '60,110p' backend/mtproto/client.go
printf '\n== buildPatchUserRequest ==\n'
sed -n '160,210p' backend/mtproto/client.go
printf '\n== mtproto.go around upsertUser ==\n'
sed -n '1,220p' backend/mtproto/mtproto.go
printf '\n== search for PatchUser / telemtPatchUserRequest usage ==\n'
rg -n "PatchUser|telemtPatchUserRequest|buildPatchUserRequest|UpdateUsers|upsertUser" backend/mtprotoRepository: PasarGuard/node
Length of output: 9240
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== runtimeUser definition and transforms ==\n'
rg -n "type runtimeUser|func .*runtimeUser|optionalPositive|ExpirationRFC3339|UserAdTag|MaxTCPConns|MaxUniqueIPs|DataQuotaBytes" backend/mtproto/client.go backend/mtproto/mtproto.go backend/mtproto/*.go
printf '\n== buildNextDesiredLocked / applyFullDesiredLocked / upsertUser ==\n'
sed -n '300,430p' backend/mtproto/mtproto.go
printf '\n== any clear/reset/delete calls in apiClient ==\n'
rg -n "CreateUser|PatchUser|DeleteUser|Reset|Clear|UpdateUser" backend/mtproto/*.goRepository: PasarGuard/node
Length of output: 16123
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '209,285p' backend/mtproto/config.goRepository: PasarGuard/node
Length of output: 2549
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== search for telemt API docs / handlers ==\n'
rg -n "PatchUser|patch user|user_ad_tag|max_tcp_conns|expiration_rfc3339|data_quota_bytes|max_unique_ips|omitempty" -g '!**/*_test.go' .
printf '\n== list likely doc files ==\n'
git ls-files | rg '(^|/)(README|docs|openapi|swagger|api|telemt|mtproto).*'Repository: PasarGuard/node
Length of output: 17128
PATCH drops explicit clears for runtime limits
telemtPatchUserRequest omits zero/empty values, so a partial update can’t clear an existing user_ad_tag, expiration_rfc3339, or limit/quota field on an already-created user. UpdateUsers() will PATCH the old telemt values back into place instead of removing them. Fix by sending explicit zero/empty values on patch, or by using pointer fields / a separate clear path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/mtproto/client.go` around lines 72 - 79, `telemtPatchUserRequest` is
omitting zero/empty fields, so `UpdateUsers()` cannot clear existing telemt
settings during a PATCH. Update the request model and patch flow in
`UpdateUsers()` to preserve explicit clears for `UserAdTag`,
`ExpirationRFC3339`, and the limit/quota fields, either by switching those
fields to pointers/nullable values or by adding a separate clear-path so omitted
values are not re-sent as old state.
39f65cc to
746cd2f
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/mtproto/mtproto.go`:
- Line 645: The deferred temp-file cleanup in mtproto.go should explicitly
ignore the removal error to satisfy errcheck. Update the deferred
os.Remove(tempPath) cleanup in the affected mtproto logic to use a best-effort
wrapper that discards the returned error intentionally, keeping the cleanup
behavior while making the ignore explicit.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f48cad09-0c52-4681-96c2-a3a30c5cd85b
📒 Files selected for processing (5)
backend/mtproto/config.gobackend/mtproto/integration_test.gobackend/mtproto/mtproto.gobackend/mtproto/mtproto_test.gobackend/mtproto/process.go
🚧 Files skipped from review as they are similar to previous changes (4)
- backend/mtproto/integration_test.go
- backend/mtproto/mtproto_test.go
- backend/mtproto/config.go
- backend/mtproto/process.go
|
@coderabbitai review |
✅ Action performedReview finished.
|
Adds BackendType.MTPROTO, a Mtproto proxy message (secret plus optional ad_tag, max_tcp_conns, expiration_rfc3339, data_quota_bytes, max_unique_ips), and Proxy.mtproto. Regenerated pb.go.
Runs the telemt binary as a node-managed subprocess (run/reload/version), renders the panel JSON config to telemt TOML, manages users over telemt's REST API, scrapes Prometheus metrics for per-user up/down traffic, and uses a sentinel user to satisfy telemt's non-empty access.users requirement. Includes unit and env-gated integration tests.
Adds the MTPROTO case to StartBackend and the TELEMT_EXECUTABLE_PATH config option.
Pinned telemt 3.4.19 install target with sha256 verification, an MTProto Docker image, and env documentation.
Wait for the stdout/stderr capture goroutines (WaitGroup + sync.Once) before closing the log channel, so a late non-blocking send cannot hit a closed channel during shutdown.
Use exec.CommandContext with a 30s deadline for the version probe and live reload, returning a distinct timeout error, so a stuck telemt cannot block startup or reload indefinitely.
The rendered telemt config contains the API bearer token and per-user secrets; create the directory 0700 and the file 0600 via CreateTemp instead of world-readable 0644.
newSentinelUser now returns an error instead of falling back to predictable credentials, and New propagates it. On startup-health failure New calls mt.Shutdown() (which closes the log channel) instead of only stopping the process.
When transitioning from zero desired users to real users the sentinel is dropped from the rendered config but is absent from previousDesired, so waitForRuntimeUsers now requires it to be gone before reporting convergence.
NewConfig now rejects a second JSON document, including a bare null (which the previous extra != nil check let through).
The removed-user lookup now asserts an apiStatusError NotFound rather than any error, and the custom-inbound-tag assertion no longer dereferences a possibly-nil config in the failure path.
Wrap the deferred os.Remove of the config temp file so the discarded error is intentional (satisfies errcheck).
Remove data_quota_bytes and expiration_rfc3339 from the Mtproto proxy message and from config rendering and the telemt API client. Quota and expiry are enforced by the panel (usage reporting plus user sync), the same as for xray and wireguard, so telemt must not enforce them. Real-time connection limits (max_tcp_conns, max_unique_ips) and user_ad_tag remain, since only telemt can enforce those live. The node still forbids the panel from setting user_data_quota/user_expirations in the config template.
5916be2 to
098d828
Compare
Keep MTPROTO backend support while picking up routing API and other dev changes. Regenerated common/service.pb.go from the merged proto so both BackendType.MTPROTO and the routing messages coexist. Co-authored-by: Cursor <cursoragent@cursor.com>
Bump the pinned binary, send JSON null on PATCH so ad-tag and limits can be cleared, return empty stats when Prometheus is not bound yet, and disable the sentinel user so it cannot authenticate. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Superseded by #120, which ports this onto dev and includes the review fixes. |
…es r2-C found scripts/upstream_drift_test.sh goes from 9 passed / 16 failed to 24 passed / 0 failed. - D1 (HIGH, fail-open): the merge check piped `git show --cc` into `grep -q` under pipefail. grep exited on the first line, git died of SIGPIPE, and a merge with a large combined diff was skipped as having no changes of its own. The real upstream merge fbb0110 on pr-61 (a 38,960-byte combined diff) was missed in 5 of 5 runs. Both diffs are now read in full, and a git failure stops the run. fbb0110 is now found in 5 of 5 runs. - D3: a merge that drops one side's change has an empty --cc diff but a non-empty --remerge-diff, so both are checked. The script refuses to run on a git without --remerge-diff (2.36+). - D2: a pending waiver now needs exactly one until:YYYY-MM-DD. The date must be a real calendar date, at most 30 days after today; a missing, second, spaced, unpadded or impossible date is a parse error (exit 2). An issue: reference is optional and must be issue:<owner>/<repo>#<n>; it no longer replaces the date, so no waiver is permanent. - D4: patch headers are read from the captured patch text instead of piping git show into an early-exiting awk. The go.mod and Dockerfile ARG readers no longer exit early either. - D5: an Upstream line in a patch header counts only when the Makefile applies patches/xray-*.patch and the Dockerfile at the checked commit runs git apply on that file. It counts only for the xray component; node, sing-box, sing-quic and mtg are ported by commit trailers only. - D6: an ours-merge that an upstream ref also contains was invisible to the fork-entry check. Every merge in the target history that no upstream tag contains, and whose recorded result differs from git's automatic merge (--remerge-diff), must now be allowlisted as allowed-merge. - D7: advisory paging used &page=N, which both endpoints ignore, so 100 or more advisories looped until the job timed out. It now follows the rel="next" Link header of the last response. Only links under the API URL are accepted, and it stops with an error after DRIFT_MAX_API_PAGES pages (default 50). Ledger entries the new checks need: - two allowed-merge entries, both pre-fork-point merges whose only rewrite is an ordinary conflict resolution: bd8ceea (Merge branch 'dev', backend/xray/api/stats.go) and 61932a4 (Merge branch 'main' into dev, controller/rest and controller/rpc stats.go); - a rejected waiver for fbb0110, the PR PasarGuard#61 telemt MTProto merge on pr-61, which collides with the fork's MTProto enum like the other PR PasarGuard#61 commits.
…NPORTED
A rejected or not-applicable waiver for work that lives only on a
pull-request or debug branch now carries a branch:<ref> token. The node
commit and node merge checks grant WAIVED only while every upstream ref
that contains the commit is one of the named branches. Once the commit
is reachable from any other ref, a release tag or a mainline branch
after a merge, the item turns UNPORTED with a message naming the refs
that now reach it.
The waiver parser rejects a malformed branch: token, and it refuses the
whole file when a waiver says the upstream work is unmerged ("not
merged", "never merged", "revisit if upstream merges") without a
branch: scope, so the pattern cannot recur unscoped.
The sixteen PR PasarGuard#61 waivers gain branch:pr-61 and the two debug-branch
waivers gain branch:debug.
* feat(proto): add MTPROTO backend type and Masque and Mtproto proxy messages Add BackendType MTPROTO = 2, the Masque message (matching the released Python bridge 0.9.3) and the Mtproto user message, exposed on Proxy as masque = 7 and mtproto = 8. Regenerate the Go code. * feat(mtproto): add telemt MTProto backend Port node PR #61 (feat/mtproto-telemt, head 4eb26fb) onto dev: the backend/mtproto package that drives telemt as a child process, the MTPROTO case in Controller.StartBackend, TELEMT_EXECUTABLE_PATH (default /usr/local/bin/telemt) in config and .env.example, and the install_telemt Makefile target (TELEMT_VERSION still 3.4.25). Ported PR commits: 8542f27, 90c50a1, 419732c, 9f9eac2, a87168d, 77c62fd, 630df12, 0eae051, ef43229, cd183a4, a66a322, fa79e3b, 098d828, 2c4fce8, 4eb26fb. The proto part of 8542f27 landed in the previous commit. Dockerfile.mtproto (9f9eac2, 4eb26fb) is not ported. Adapt the backend to dev's Backend interface: the process manager now keeps its output in a logstream.Buffer, the capture goroutines Publish into it, MTProto.SubscribeLogs returns a Subscribe channel, and Shutdown closes the buffer. This replaces Logs(), CloseLogs and the log WaitGroup, so a late log line can no longer be sent on a closed channel. Add a test that runs a stand-in process and checks that its stdout and stderr reach a subscriber and that shutdown closes the subscription cleanly. Co-authored-by: x0sina <bulletsina77@gmail.com> * fix(mtproto): harden rendered telemt config and skip invalid users Config: - Reject panel JSON that sets node-managed or host-reaching telemt keys: include, web, server.api, server.admin_api, server.metrics_port, server.metrics_listen, server.metrics_whitelist, access.user_rate_limits and access.user_source_deny (next to the existing restricted per-user access maps). - Validate object sections and ports (1-65535, integers) and add Config.ListenPorts (server.port, default 443, plus listener ports). - Render forces general.disable_colors, general.telemetry.user_enabled and server.api.read_only=false, and fails when a listen port equals the node's telemt API or metrics port. TOML: - Render strings and keys as TOML basic strings (\uXXXX for control characters) instead of strconv.Quote, whose \a, \v and \x escapes are not TOML. - Drop null object fields; a null array element stays an error. - Always quote a key named include: telemt expands any `include = "..."` line as a file include before parsing. Users: - ActiveUsersFromCommon logs and skips invalid users and returns their emails, so one bad user no longer fails Start or a full sync. Partial updates treat an invalid user as not included. SyncUser still returns the validation error for its single user. Tests use github.com/pelletier/go-toml v1.9.5 (already in go.sum) as an independent TOML decoder; it is now a direct requirement. * fix(mtproto): force stderr logging and reject web listeners - Render forces logging.destination = "stderr": the node reads telemt's stderr for its log stream and startup errors, and a panel config with destination "file" or "syslog" would leave both empty. logging must be an object when present. - NewConfig rejects a server.listeners[].transport other than "mtproxy". telemt WEB listeners need [[web.vhosts]], which the node rejects, so they would only make telemt exit at startup. * fix(mtproto): supervise telemt 3.5.14 safely across start, crash and shutdown Process manager (process.go): - Refuse telemt older than 3.5.14 (parsed from `telemt X.Y.Z`). - Run `telemt run <config>` with no --pid-file, so a stale pid file can no longer block startup; reload sends SIGHUP to the child (Windows returns an error and the caller restarts). - Started() checks a waitDone channel and never reads ProcessState (data race under -race). Stop marks the run as stopping; any other exit is logged and published as "telemt exited unexpectedly: <err>", with an OOM hint on SIGKILL. - Read stdout and stderr to EOF before Wait so a crash's last lines are kept; overlong lines are cut instead of stalling the reader. - Keep the last 50 lines since Start for startup errors (StartupTail). - Kill an orphaned telemt whose command line runs exactly this config path before starting a new one. - Add Exited(), Generation(), Close(); Start after Close fails. Lifecycle (lifecycle.go): - New picks the API and metrics ports again while they clash with each other or with the config's listen ports, creates <generated>/telemt with mode 0700 and resolves symlinks (telemt 3.5.8+ rejects a symlinked config path). - Wait up to 90 s for /v1/health (telemt binds its API only after the Telegram DC check), then fail with telemt's recent output instead of a bare exit status. /v1/health/ready gets a 30 s budget and only logs a warning. - A supervisor restarts telemt after an unexpected exit with backoff from 1 s doubling to 60 s, re-rendering the desired users. - syncMu serializes every mutating call, the supervisor and Shutdown. Shutdown sets closed, stops telemt, closes the log buffer and deletes config.toml; later syncs return errClosed instead of starting a new telemt and crashing the node on a closed log channel. Also bump TELEMT_VERSION to 3.5.14 and give the integration tests a 150 s context. Unit tests drive a fake telemt (the test binary re-run as a helper process) for slow APIs, crashes, orphans and startup failures. * fix(mtproto): detach telemt startup from request deadlines - New starts telemt on context.WithoutCancel(ctx) bounded by telemtStartupTimeout, so a telemt that binds its API after the panel's 10 s Start deadline still comes up and the panel's retry attaches to it. Once spawned, startLocked waits for health on a detached ctx too; only the startup timeout and Shutdown (errClosed) end that wait. - The /v1/health/ready probe moved to a background watcher bound to the backend lifetime and to that telemt's exit: it warns after 30 s and logs when telemt becomes ready later. Start and every restart return right after health instead of holding syncMu and the panel deadline. - A sync whose ctx is already done (it queued on syncMu past its deadline) returns ctx.Err() before touching the config; reloadLocked never restarts telemt when the caller gave up, and startLocked spawns nothing for a done ctx or after Shutdown. - The supervisor and readiness watchers share one WaitGroup that Shutdown waits on. * fix(mtproto): apply user changes without per-user reads and confirm reloads User changes are now decided from the confirmed desired state, never with a GET per user. Unchanged users are skipped; up to 32 changes go through telemt's user API (PATCH falls back to POST on 404, POST to PATCH on 409 user_exists, DELETE of a missing user is done). Larger batches, changes to or from zero users, a failed partial apply and every full sync render the config once and reload it through POST /v1/system/reload (instant, rollback), polling the reload status for the outcome and retrying while another reload is in progress. desired changes only after telemt confirmed the new users. A rolled_back, failed or refused reload re-renders the previous users to disk and returns telemt's error. A reload whose caller gave up leaves telemt running and forces the next sync to apply all users. Only a reload request that cannot reach telemt restarts it. The online IP list uses one GET /v1/stats/users/active-ips call. The SIGHUP reload and waitForRuntimeUsers are removed, and DELETE responses (a JSON string) decode again instead of failing into the full-render fallback. * fix(mtproto): confirm hot reloads with a marker user instead of the reload API telemt's API reload swaps the runtime generation and, in instant mode, stops every session of the old one. A hot reload (SIGHUP, or telemt's config file watcher) updates the users live and cancels only changed or removed ones, so full applies go back to it. Every render now carries one disabled marker user with a fresh random name (pgm_<16 hex>). After writing the config the node sends SIGHUP (on Windows it relies on telemt's file watcher) and polls GET /v1/users until the marker is in telemt's running config, for up to 30 s, detached from the caller and ended by Shutdown. A config telemt did not load by then is rejected: the previous users are rendered again with a new marker and the error carries telemt's last output. A caller that gives up leaves telemt running and forces the next sync to apply all users; only an API that gets no answer restarts telemt. The marker replaces the zero-user sentinel and keeps telemt's user list non-empty, so the last real user is deleted, and the first one created, through the user API. The reload API client code is removed. * fix(mtproto): settle hot reloads independently of callers and clear stale overrides telemt stages an admission override for every user API write and clears it only when a published config carries the same value. A full render that changed such a user again before telemt published the API's own write left the user refused while the node reported success. The node now records users written through the API since the last confirmed render or telemt start, and before a render writes again each one whose rendered state differs, so the render's publish clears the override. A write that fails stays unknown and keeps the state unconfirmed. A full apply now runs to telemt's verdict in the background under syncMu, which a context-aware lock lets callers wait for only until their own deadline. A caller that gives up gets its ctx error at once; the apply still confirms, or restores the previous users, and logs the outcome. A rejection goes to telemt's log stream, which the panel shows. A 5xx "failed to load config" from GET /v1/users is an immediate rejection with telemt's message. telemt is restarted only after 3 polls in a row get no answer or when it exited, not on one failed request. Reload code moves to reload.go and the per-user API writes to userapi.go. * fix(mtproto): account traffic per stat family without double counting - Give users, outbound and inbound stats their own baseline per user, so the panel's 10 s user poll and 30 s outbound poll no longer consume each other's traffic. - Track every user telemt reports, desired or not. A user missing from a snapshot keeps its counters, so a removed and re-added user is not billed again and its last interval is still reported. Absent users are dropped after 25 h (telemt forgets idle stats after 24 h), keeping the outbound and inbound traffic still pending. - Count from zero after a telemt restart (process generation) or a counter decrease, carrying the pending delta of every family. - Serialize refreshes with refreshMu and share a snapshot younger than 1 s, so concurrent polls never apply an older scrape after a newer one. A scrape that overlaps a telemt restart is discarded. - Read telemt 3.5.14's metric names, filter by name before parsing labels, and switch to GET /v1/users totals (reported as downlink) while telemt_telemetry_user_series_suppressed is set; a source switch reseeds without double counting. - Report distinct client IPs as the online count, drop marker users from every result, and log failed refreshes at most once a minute. - Move the stats endpoints into stats.go. * fix(mtproto): carry exact traffic across a stats source switch The reseed on a switch between per-user metrics and API totals dropped the bytes telemt counted between the last sample of one source and the first of the other, in every family. Both sources count the same telemt stats entry (total_octets is octets_from_client + octets_to_client), so within one telemt process the increment is known exactly: credit it as downlink. A total below the last sample means telemt dropped the entry in between, which counts from zero like a restart. The suppression trigger also covers more than 4096 configured users; say so in the comments and the switch log line. * build(mtproto): ship pinned telemt 3.5.14 in the node image - Dockerfile: the builder runs make install_telemt after install_xray, and the final stage carries /usr/local/bin/telemt plus the telemt license files in /usr/share/licenses/telemt/. - Makefile: install_telemt checks the tarball against our own SHA256 per arch (x86_64, aarch64) instead of the release's .sha256 file, refuses "latest", and needs TELEMT_SHA256 for any version other than the pinned one. TELEMT_INSTALL_DIR selects the target dir; sudo is used only when that dir is not writable. --version runs only for a host-arch binary. - CI: install telemt and point MTPROTO_TELEMT_BIN at it, so the gated MTProto integration tests run. - third_party/telemt: telemt 3.5.14 LICENSE and the canonical English license text, copied verbatim, with a note on the unmodified binary. - README: MTProto backend, Xray and MTProto on one host, third-party note. * fix(mtproto): read telemt 3.5.14's answer to a user delete telemt 3.5.14 answers DELETE /v1/users/{name} with an object, {"username": ..., "in_runtime": ...} (status 202 while the running config still has the user), not with the username as a JSON string. The node failed to decode every successful delete, logged the batch as failed and fell back to a full render and hot reload for each user removal. The fake telemt API now answers deletes like telemt 3.5.14, and a client test pins the answer. Found by the live tests against telemt 3.5.14. * fix(mtproto): reject a render telemt cannot read without waiting 30 s telemt reads the config file and its includes before it parses them, and answers GET /v1/users with 500 "failed to read config graph: ..." when that read fails, for example on a rendered file above its 8 MiB limit per config file. The node only took "failed to load config" (a parse error) as a rejection, so a read failure kept the sync waiting the whole markerConfirmTimeout under the sync lock before the restore. Both answers now reject the render at once with telemt's message. Found by the live tests against telemt 3.5.14. * fix(mtproto): do not SIGHUP telemt before it handles the signal telemt installs its SIGHUP handler in its config watcher, which waits for the runtime to activate after the API is up, reads the whole config and sets up its file watches first. A SIGHUP in that window ends telemt with the default action. The node assumed the opposite order, so a sync right after a start (or a restore after a quick rejection) could kill telemt: the reload then found the API gone, restarted telemt and dropped every session. For telemtSignalGrace (5 s) after telemt's API first answers, the node now sends no SIGHUP. The watcher loads most renders of that time by itself, but misses one written while it sets up its watches, so a reload whose marker telemt has not loaded when the grace ends gets its SIGHUP then. The fake telemt can now play the watcher, including that blind spot, and a late SIGHUP handler. Found by the live tests against telemt 3.5.14, where nine telemts started at once made the window wide. * test(mtproto): cover telemt 3.5.14 lifecycle, reload and removal end to end The integration tests (run when MTPROTO_TELEMT_BIN names a telemt binary) now check what telemt does, not only what its config file says. A fake MTProto client performs the obfuscated2 handshake with a user's secret, and telemt's per-user connection gauge tells whether it admitted the client. telemt's dc_overrides point the client's DC at a local listener, so sessions stay open without Telegram. Covered live: start, version 3.5.14, health and stats, then Shutdown removing config.toml and leaving no telemt process; user create, secret change and delete through the user API; full syncs down to the marker alone and back on the same process, with the watcher and with SIGHUP; unchanged users keeping their sessions across a hot reload while removed users lose theirs; API deletes cutting sessions, including the last real user; a start without users; a secret written through the API and changed by a render before telemt published it; a symlinked generated path; and a render above telemt's 8 MiB limit, rejected at once with telemt's reason while the previous users and their sessions stay. The tests run in parallel, each with its own telemt. * refactor(mtproto): keep mtproto.go to the backend struct and drop dead code mtproto.go now holds the MTProto struct and its one-line accessors. The helpers it collected move next to their callers: Restart and the API token to lifecycle.go, the marker user to reload.go, the atomic file write and user map clone to sync.go. markerUsername, read only by tests, moves to the tests, and the unused config field goes. Also removes the never-built tomlLiteral type, the always-nil error of classifyTOMLSection, and decoded telemt API fields the node never reads. No behavior change. * chore(mtproto): fix the Windows test build and document telemt limits - Build integration_test.go only off Windows: it uses waitFor from the Unix-only fake telemt helpers, so GOOS=windows go vet failed. - Move the TestUsersAPIUnreachableRestartsTelemt doc comment back above its test. - README: Start can take up to 90 s and the panel's retry attaches; above 4096 users telemt stops per-user series and the node bills API totals as downlink; telemt refuses configs above 8 MiB (about 150k to 190k users). * fix(mtproto): send the first SIGHUP once telemt handles it telemt's config watcher installs its SIGHUP handler at the end of its own setup, which runs concurrently with the API coming up and can finish after the API first answers; a SIGHUP before that ends telemt, and once installed the handler stays for the process lifetime. On live telemt 3.5.14 the handler appeared about 3 ms after the API first answered, but a slow STUN probe, TLS front or large config can stretch that past any fixed grace. On Linux the node now sends SIGHUP only once bit 0 of the SigCgt mask in /proc/<pid>/status is set, checked on each marker poll and before the restore render's signal. The 5 s grace after the first API answer stays as the fallback on other platforms or when /proc cannot be read. The fake telemt now models the mask too: with a signal delay it resets SIGHUP to its default action until signal.Notify, since the Go runtime otherwise catches SIGHUP from the start. The grace test forces the fallback, a new Linux test leaves only the mask to let the signal through, and the live tests wait for the handler instead of the clock. * fix(mtproto): restart telemt whose API hangs during a reload A telemt whose API accepts connections but never answers was diagnosed as a rejected reload: each users poll ran into the 10 s client timeout, so the 30 s marker budget ran out before three unanswered polls, the node restored the previous render and reported a rejection, and nothing restarted telemt. Each marker poll now gets its own timeout, a quarter of the budget (7.5 s), so three hung polls in a row reach the existing restart branch at about 22.5 s. A shorter timeout would misread a large node: telemt reads its whole config for each poll, and listing 150k users took about 2.5 s on live telemt 3.5.14. * test(mtproto): cover Shutdown during a full apply in the marker wait A full apply blocked waiting for telemt to load its render must not hold Shutdown for the 30 s marker budget. The test starts a sync against a fake telemt that never loads the render, waits until the marker wait polls GET /v1/users (the fake now logs those requests), and calls Shutdown: Shutdown returns promptly, the apply ends with errClosed, needsFullApply is set on the closed backend, desired keeps the confirmed users and telemt is not restarted. * test(controller): cover a slow MTProto Start outliving its caller Dev now runs every core start under a context that keeps the request's values but not its cancellation (#116), and a same-config Start is a heartbeat only when the running backend passes GetSysStats (#117). The panel relies on both for MTProto: telemt may need up to 90 s to bind its API, the panel gives up on Start after 10 s, and its retry must attach to the telemt that came up instead of restarting it. Run the controller against a fake telemt (the test binary re-executed through a wrapper script, so the node starts and reaps a real child) that binds its API after a delay: a Start whose caller already went away ends with telemt running, telemt keeps running after the start context ends, and a retry that waits behind the start attaches to the same backend. * fix(mtproto): let Disconnect abort a telemt start in progress New detached the telemt start from its context (WithoutCancel plus its own 90 s bound) so a slow start would outlive the panel's 10 s Start request. Dev's controller now does that for every backend (#116): it runs StartOrAttach under a context without the request's cancellation and cancels it only from Disconnect, which process shutdown calls without LockControl, so it does not wait for a start in progress. The second detachment in New and startLocked ignored that abort. A Disconnect during a telemt start that never binds its API (Telegram unreachable) blocked on the controller mutex for the whole 90 s telemtStartupTimeout, and the telemt being started kept running until then. New and startLocked now follow their context; waitForHealth still bounds the wait by telemtStartupTimeout and reports Shutdown as errClosed. Every other start already passes a context the caller cannot cancel: Restart and the supervisor the backend lifetime, a full apply its detached apply context, the restart after a hung reload a WithoutCancel context. The panel sees no change: the Start outlives its request through the controller, as the controller tests show. * test(controller): restart telemt when an MTProto Start changes the config Dev's StartOrAttach now offers a changed config to a running backend that implements backend.LiveConfigApplier before it restarts it (#105). Only xray does: telemt has no live path for its core config, so a Start with another MTProto config must still stop the old telemt and start a new one on the new config. Cover that against the fake telemt, and fail loudly if MTProto ever gains the interface without a review of what a live apply means for telemt. * test: pin the node answers the panel relies on for MTProto The panel reads three answers of the node as fixed: - "invalid backend type" from a node without the MTProto backend, which it turns into an upgrade message; - "the users are still being applied" and "context deadline exceeded" from a sync that is still applying, which it retries later; - a retry or repair Start that attaches to a running telemt, which since #117 needs GetSysStats to answer within the 3 s heartbeat budget. Assert the exact texts, check that the unknown backend type error stays unwrapped under the detached start (also when the request is gone), and check that GetSysStats answers while telemt is frozen (SIGSTOP, so its API cannot answer) and syncMu is held: it stays a local read of the process, so a stuck API or a full apply never turns a heartbeat into a restart. * fix(mtproto): mark sync errors of a busy or restarting telemt as retry-later Bridge 0.9.4 delivers user deltas through SyncUsersChunked and treats an INTERNAL "failed to update users" answer (REST: 500) as a rejection of the users it sent: it splits the batch in halves, sends again and holds each user it isolates with a backoff of 2 s up to 60 s (an add every 300 s after 10 minutes). MTProto answered that way for failures that have nothing to do with the users: the backend shut down while a request held it, telemt that does not start for a full apply, a reload whose restart failed, a caller that gave up while it queued behind an apply. A busy or restarting telemt therefore multiplied the requests and delayed every user of the batch. Add backend.RetryLaterError, a marker that keeps the error and its text, and wrap every MTProto sync failure in it except a render telemt read and refused (errReloadRejected): the users sent may have caused that one, so splitting the batch can find them. An invalid user synced alone stays a plain error too. The texts the panel matches ("the users are still being applied", "context deadline exceeded") are unchanged. Xray and WireGuard never return the marker. * fix(controller): answer retry-later sync errors as unavailable A sync error a backend marked retry-later (only MTProto does: telemt is busy with an earlier apply, restarting or shut down) now reaches the client as gRPC Unavailable or HTTP 503 instead of INTERNAL (SyncUser, SyncUsersChunked), Unknown (SyncUsers) or 500 (REST), with the same text. Bridge 0.9.4 retries such an answer as a whole batch with its 1 s to 30 s backoff, the same path a timed-out sync already takes, instead of splitting the batch and holding each user it isolates. The texts the panel matches ("the users are still being applied", "context deadline exceeded") stay in the answer, so the panel's manual sync keeps reporting a still-applying MTProto node as retry-later. Every other error keeps its code, so Xray and WireGuard answers do not change. Tests: the mapping in both services, and the rpc and rest handlers end to end against an MTProto backend on the fake telemt whose full apply is still waiting for telemt when the caller's deadline passes (the fake now catches SIGHUP like telemt). * refactor(mtproto): render the telemt config with go-toml/v2 Replace the hand-written TOML encoder with github.com/pelletier/go-toml/v2, as asked in review. The test-only go-toml v1 dependency goes away. - JSON numbers from the panel config are converted to int64 or float64 first: the encoder writes a json.Number as a string. An integer outside the TOML 64-bit range is an error naming the field. - JSON nulls are still dropped from tables; a null array element is an error that names its path. - telemt expands any line "include = <path>" as a file include before it parses TOML, in every table. The old encoder quoted such keys; go-toml writes them bare, so the rendered text is now checked with telemt's own line matcher and refused, and the username "include" is skipped like any other invalid user. - Test helpers and the fake telemts decode the config with go-toml instead of parsing double-quoted lines by hand. * refactor(mtproto): use omitzero for the telemt user requests The create request now sends the limits as plain uint32 with omitzero instead of *int with omitempty, so the pointer and the int range conversion go away; a zero limit is still left out. The patch request keeps pointers without omitzero on the clearable fields: JSON Merge Patch needs nil sent as null to clear an override. * fix(mtproto): reject include keys at any depth of the panel config A key named include in any table becomes a telemt file include, and the render already refuses it; NewConfig now rejects it too, naming the path, so the panel gets the error up front instead of a failed Start. Also from review: - the TOML conversion refuses container types it does not walk, since they could hide a json.Number that would render as a string - a golden test pins one full render, so a go-toml upgrade that changes the output shows up - table test for checkNoIncludeLines against telemt's matcher, signed zero, and the request JSON without a secret - parseConfigUsers fails loudly when a config does not decode * feat(mtproto): accept telemt WEB listeners and vhosts behind a private front Accept the web section and listeners with transport "web" on a loopback or private address and their own port. Vhosts may only use an http_upstream decoy on a loopback or private IP literal, must not define profiles, and may carry a user_profile template that the node keeps per vhost and strips from the config. web.debug and web.limits.max_profiles stay node managed. * feat(mtproto): render one telemt WEB profile per user * fix(mtproto): apply user changes by render when WEB profiles are in use * fix(mtproto): render web.limits.max_profiles within telemt's memory envelope telemt 3.5.14 reserves 64 bytes of its WEB memory envelope per max_profiles entry, and its default envelope (1280 MiB) has only about 4.1 MiB left after its other default reservations. With 262144 profiles (16 MiB) every WEB config failed at start with "web.limits memory reservations must fit memory_envelope_bytes within 4 GiB". Render 65536 (4 MiB), which telemt accepts with its default limits. A render with more profiles than that (users times vhosts) is rejected by telemt and the node restores the previous users, as for any rejected render. * test(mtproto): cover WEB decoy, profiles and empty start against telemt 3.5.14 startLiveTelemtWithConfig lets a live test pass its own panel config; startLiveTelemt keeps today's config through it. TestIntegrationWebDecoyAndProfiles runs a native and a WEB listener on 127.0.0.1 with one vhost and a local decoy: a plain request for the vhost reaches the decoy, GET /v1/config lists a profile per user and follows a full sync, and a native session stays open across that sync. TestIntegrationWebStartWithoutUsers starts with no real users: telemt accepts the disabled marker user's profile, so no fallback is needed. * test(mtproto): check WEB profiles in telemt's running config GET /v1/config reads telemt's config file on disk, so it showed only that the node rendered the profiles. The WEB tests now also send the bridge request Telegram Desktop sends for a dd link (capability HMAC-SHA256 over the context and host, keyed by the client secret) and check telemt's answer: the bridge page for a user with a running profile, the decoy once a sync removed it, and telemt's private 404 for the disabled marker, whose profile telemt runs but serves no bridge to. The /v1/config check stays, named as a check of the rendered file. * docs(mtproto): explain WEB mode and an example HTTPS front * docs(mtproto): add tls_domain to the WEB example config * fix(mtproto): match telemt's decoy origin rule and keep decoys off telemt's own ports A WEB decoy upstream must now be a bare http origin (no https, path, query, fragment, credentials or port 0), as telemt's parse_origin requires, so the node refuses it before telemt fails at Start. Render also refuses a loopback decoy on the node's telemt API or metrics port, which would publish that endpoint on the WEB domain. Also fix the max_profiles rejection reason and the UpdateUsers comment, and say in the README that web_trusted_proxy_cidrs is required and where telemt's per-profile limits stop. * fix(mtproto): check WEB profile totals and base paths before telemt does telemt counts WEB profiles across all vhosts and refuses a config above web.limits.max_profiles, and it refuses a vhost base_path that is not empty or slash-separated [A-Za-z0-9][A-Za-z0-9_-]* segments of at most 128 ASCII bytes. Render now refuses more than 65536 profiles before writing the file, naming the user and vhost counts, and NewConfig applies telemt's base_path rule. The README notes that `http2 on;` needs nginx 1.25.1 or newer. --------- Co-authored-by: x0sina <bulletsina77@gmail.com>
The owner ordered the Telegram MTProto proxy removed from the fork; its users lose the proxy from their configs. Production was already cleared before this change (hosts 140/142/141, node 69, core 10). backend/mtproto and controller/mtproto_shutdown_stream_race_test.go go entirely, 52 files and 9740 lines, middleproxy among them: no instance of core 10 ever carried an ad_tag, so the per-user middleproxy path was dead code in production. BackendType MTPROTO = 4 and Proxy mtproto = 9 are reserved by number and by name instead of renumbered, so nothing reuses them and a node still running the previous image stays wire-compatible during the rollout. SING_BOX = 2, Proxy.hysteria2 = 7 and open_vpn = 8 do not move. The controller tests that used MTProto as the only backend needing no external binary keep their assertions: the multi-backend and lifecycle tests move to OPEN_VPN, the two Start interleaving tests build their core through the c.newBackend seam already used in this package, and the REST multi-backend tests move to SING_BOX. The race step now passes SINGBOX_SMOKE_BINARY to ./controller/..., which the job already builds, so TestREST_AddRemoveSingBoxBackend and TestREST_StatsStillWorkWithTwoBackends run there instead of skipping as the sing-box one silently did. Nothing imports mtg any more, so go mod tidy drops the require, the Free-Guy-IR/mtg replace and five transitive dependencies, and the drift check loses its mtg component and the pin it read from go.mod. The upstream waivers for PR PasarGuard#61 and PasarGuard#120 keep refusing them, now because upstream's MTPROTO = 2 and mtproto = 8 are the fork's SING_BOX = 2 and open_vpn = 8, not because the fork runs a competing backend. Rebuild guide: work/r11/MED/mtproto.med. NodeVersion goes 0.6.22 to 0.6.23, one patch step from the last value the fork used, because BaseInfoResponse.supported_backends loses "mtproto" and a panel reads that list to decide what a node can run. The three upstream-waiver rows that cite the constant are updated with it. The controller race step points SINGBOX_EXECUTABLE_PATH, not SINGBOX_SMOKE_BINARY, at the sing-box the job builds: requireSingBoxBinary only decided whether to skip, while the backend forks the path config.SingBoxExecutablePath holds, which no CI step installs. With only SINGBOX_SMOKE_BINARY set, TestREST_AddRemoveSingBoxBackend and TestREST_StatsStillWorkWithTwoBackends stop skipping and fail on "fork/exec /usr/local/bin/sing-box: no such file or directory"; both pass against the pinned v1.13.21 build with SINGBOX_EXECUTABLE_PATH set. The helper now reads that same path, and the three race-step mutations of scripts/test_workflow_policy.sh, which still matched the single-line step, are rewritten for the split step and joined by two more, for the backend race run and for the sing-box path.
What this adds
A new MTPROTO backend type that runs the telemt MTProto proxy (an external Rust binary) as a node managed subprocess. It is mutually exclusive with xray and wireguard, following the existing one backend at a time model.
Changes
BackendType.MTPROTO, aMtprotoproxy message (secret, user_ad_tag, max_tcp_conns, expiration_rfc3339, data_quota_bytes, max_unique_ips), andProxy.mtprotobackend/mtproto: parses the panel JSON config and renders telemt TOML, manages the telemt subprocess (run, reload, version), uses telemt's REST control API for user create/patch/delete, scrapes the Prometheus endpoint for per user traffic, tracks per user up/down deltas with reset and counter restart handling, and injects a sentinel user because telemt refuses to start with an empty[access.users]MTPROTOstart caseTELEMT_EXECUTABLE_PATHmake install_telemttarget (telemt 3.4.19) andDockerfile.mtprotoMTPROTO_TELEMT_BIN)Verification
Builds and vets clean, unit tests pass. The integration test was run against a real telemt 3.4.19 and confirms process start, version, user create/patch/delete/get, online IP list, system stats, and the empty users sentinel path.
Per user traffic and online count stats come from telemt's Prometheus endpoint, which only binds once telemt's data plane is up (it needs Telegram connectivity). Those paths were verified at the unit level and kept best effort in the integration test, so they should be confirmed once more on a host with Telegram egress.
Panel side (separate change, not in this PR)
The panel needs to send a JSON telemt config, set the backend type to
MTPROTO, tag MTProto users with the configuredinbound_tag, populateproxies.mtproto.secret(and optional limits), and build the proxy links.Notes
telemt ships releases roughly weekly. The version is pinned in the Makefile and Dockerfile so it can be bumped deliberately.
Summary by CodeRabbit
TELEMT_EXECUTABLE_PATH(default:/usr/local/bin/telemt) for selecting the TeleMT binary.