Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
261fe5c
feat(proto): add MTPROTO backend type and Masque and Mtproto proxy me…
T3ST3ST3R0N Oct 19, 2026
4c147e0
feat(mtproto): add telemt MTProto backend
T3ST3ST3R0N Oct 19, 2026
ca7c724
fix(mtproto): harden rendered telemt config and skip invalid users
T3ST3ST3R0N Oct 19, 2026
f9bca96
fix(mtproto): force stderr logging and reject web listeners
T3ST3ST3R0N Oct 19, 2026
a2c71b8
fix(mtproto): supervise telemt 3.5.14 safely across start, crash and …
T3ST3ST3R0N Oct 19, 2026
abd6cd9
fix(mtproto): detach telemt startup from request deadlines
T3ST3ST3R0N Oct 19, 2026
c0ec733
fix(mtproto): apply user changes without per-user reads and confirm r…
T3ST3ST3R0N Oct 19, 2026
47cd161
fix(mtproto): confirm hot reloads with a marker user instead of the r…
T3ST3ST3R0N Oct 19, 2026
0afe951
fix(mtproto): settle hot reloads independently of callers and clear s…
T3ST3ST3R0N Oct 19, 2026
71b16bb
fix(mtproto): account traffic per stat family without double counting
T3ST3ST3R0N Oct 19, 2026
8099149
fix(mtproto): carry exact traffic across a stats source switch
T3ST3ST3R0N Oct 19, 2026
5d9b826
build(mtproto): ship pinned telemt 3.5.14 in the node image
T3ST3ST3R0N Oct 19, 2026
af0fc33
fix(mtproto): read telemt 3.5.14's answer to a user delete
T3ST3ST3R0N Oct 19, 2026
1f79fb5
fix(mtproto): reject a render telemt cannot read without waiting 30 s
T3ST3ST3R0N Oct 19, 2026
c67617e
fix(mtproto): do not SIGHUP telemt before it handles the signal
T3ST3ST3R0N Oct 19, 2026
f69d559
test(mtproto): cover telemt 3.5.14 lifecycle, reload and removal end …
T3ST3ST3R0N Oct 19, 2026
77cf000
refactor(mtproto): keep mtproto.go to the backend struct and drop dea…
T3ST3ST3R0N Oct 19, 2026
03f8529
chore(mtproto): fix the Windows test build and document telemt limits
T3ST3ST3R0N Oct 19, 2026
d036914
fix(mtproto): send the first SIGHUP once telemt handles it
T3ST3ST3R0N Oct 19, 2026
759f480
fix(mtproto): restart telemt whose API hangs during a reload
T3ST3ST3R0N Oct 19, 2026
b48561d
test(mtproto): cover Shutdown during a full apply in the marker wait
T3ST3ST3R0N Oct 19, 2026
730ac6a
test(controller): cover a slow MTProto Start outliving its caller
T3ST3ST3R0N Oct 22, 2026
7e69e36
fix(mtproto): let Disconnect abort a telemt start in progress
T3ST3ST3R0N Oct 22, 2026
9ff6ae0
test(controller): restart telemt when an MTProto Start changes the co…
T3ST3ST3R0N Oct 22, 2026
087134d
test: pin the node answers the panel relies on for MTProto
T3ST3ST3R0N Oct 22, 2026
2eb22c8
fix(mtproto): mark sync errors of a busy or restarting telemt as retr…
T3ST3ST3R0N Oct 22, 2026
b301cd3
fix(controller): answer retry-later sync errors as unavailable
T3ST3ST3R0N Oct 22, 2026
d631cb4
refactor(mtproto): render the telemt config with go-toml/v2
T3ST3ST3R0N Oct 26, 2026
0ca58f2
refactor(mtproto): use omitzero for the telemt user requests
T3ST3ST3R0N Oct 26, 2026
8cfb762
fix(mtproto): reject include keys at any depth of the panel config
T3ST3ST3R0N Oct 26, 2026
4579899
feat(mtproto): accept telemt WEB listeners and vhosts behind a privat…
T3ST3ST3R0N Oct 26, 2026
fe4db93
feat(mtproto): render one telemt WEB profile per user
T3ST3ST3R0N Oct 26, 2026
7936ac0
fix(mtproto): apply user changes by render when WEB profiles are in use
T3ST3ST3R0N Oct 26, 2026
198b3ee
fix(mtproto): render web.limits.max_profiles within telemt's memory e…
T3ST3ST3R0N Oct 26, 2026
4e5b496
test(mtproto): cover WEB decoy, profiles and empty start against tele…
T3ST3ST3R0N Oct 26, 2026
d9ce5a8
test(mtproto): check WEB profiles in telemt's running config
T3ST3ST3R0N Oct 26, 2026
12a86af
docs(mtproto): explain WEB mode and an example HTTPS front
T3ST3ST3R0N Oct 26, 2026
9350e71
docs(mtproto): add tls_domain to the WEB example config
T3ST3ST3R0N Oct 26, 2026
26cff9a
fix(mtproto): match telemt's decoy origin rule and keep decoys off te…
T3ST3ST3R0N Oct 26, 2026
91e8198
fix(mtproto): check WEB profile totals and base paths before telemt does
T3ST3ST3R0N Oct 26, 2026
67ef7fe
Merge branch 'dev' into feat/mtproto
T3ST3ST3R0N Nov 2, 2026
a5cc90d
Merge branch 'dev' into feat/mtproto
T3ST3ST3R0N Nov 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ NODE_HOST = "0.0.0.0"
# pinned by GEOFILES_TAG in the Makefile (not the geo files from the Xray release zip).
# These are defaults: `pg-node geofiles` installs its own files and points XRAY_ASSETS_PATH at them.

# MTProto (telemt) backend binary
# TELEMT_EXECUTABLE_PATH = /usr/local/bin/telemt

SSL_CERT_FILE = /var/lib/pg-node/certs/ssl_cert.pem
SSL_KEY_FILE = /var/lib/pg-node/certs/ssl_key.pem

Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
- name: Install xray-core
run: make install_xray

- name: Install telemt
run: make install_telemt

- name: install wg
run: make install_wg

Expand All @@ -41,6 +44,8 @@ jobs:
make generate_client_cert

- name: Run regular test suite
env:
MTPROTO_TELEMT_BIN: /usr/local/bin/telemt
run: TEST_INTEGRATION=true go test ./... -v -p 1

- name: Check Xray process status races
Expand Down
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ COPY . .
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} make NAME=main build
# install_xray also installs the pinned geo files (Makefile install_geofiles) and fails the build unless xray loads them
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} XRAY_TAG=${XRAY_TAG} make install_xray
# MTProto backend: the telemt version and SHA256 per arch are pinned in the Makefile (amd64 and arm64 only)
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} make install_telemt

FROM alpine:latest

Expand All @@ -27,5 +29,7 @@ WORKDIR /app
COPY --from=builder /src/main /app/main
COPY --from=builder /usr/local/bin/xray /usr/local/bin/xray
COPY --from=builder /usr/local/share/xray /usr/local/share/xray
COPY --from=builder /usr/local/bin/telemt /usr/local/bin/telemt
COPY third_party/telemt/ /usr/share/licenses/telemt/

ENTRYPOINT ["./main"]
85 changes: 84 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,29 @@ GEOFILES_DIR ?= /usr/local/share/xray
GEOFILES_XRAY ?= /usr/local/bin/xray
GEOFILES_CHECK_CONFIG ?= $(CURDIR)/testdata/geofiles-check.json

# telemt (MTProto backend): pinned release, static musl tarball.
# 3.5.14 is also the floor: the node refuses to start an older telemt.
# The tarball is checked against our own SHA256 per arch below, never the .sha256 file of the release.
# Any other version also needs its checksum, and "latest" is always refused:
# make install_telemt TELEMT_VERSION=X.Y.Z TELEMT_SHA256=<sha256 of telemt-<arch>-linux-musl.tar.gz>
# On a bump, update TELEMT_PINNED_VERSION, both checksums and third_party/telemt/ together.
TELEMT_REPOSITORY ?= telemt/telemt
TELEMT_PINNED_VERSION := 3.5.14
TELEMT_VERSION ?= $(TELEMT_PINNED_VERSION)
TELEMT_SHA256 ?=
TELEMT_SHA256_PINNED_x86_64 := 71c1454f79cf755edea0985500629daba553002f9863be754e8e38a418822773
TELEMT_SHA256_PINNED_aarch64 := 21795430c4165cf964405459714d08eea319edaa5c9027beac4de373c356d84d
TELEMT_INSTALL_DIR ?= /usr/local/bin
TELEMT_OS ?=
TELEMT_ARCH ?=
TELEMT_ARCH_MAP_amd64 = x86_64
TELEMT_ARCH_MAP_arm64 = aarch64
TELEMT_GOOS := $(if $(GOOS),$(GOOS),$(shell go env GOOS))
TELEMT_GOARCH := $(if $(GOARCH),$(GOARCH),$(shell go env GOARCH))
TELEMT_OS_EFFECTIVE := $(if $(TELEMT_OS),$(TELEMT_OS),$(TELEMT_GOOS))
TELEMT_ARCH_EFFECTIVE := $(if $(TELEMT_ARCH),$(TELEMT_ARCH),$(TELEMT_ARCH_MAP_$(TELEMT_GOARCH)))
TELEMT_GOARCH_EFFECTIVE := $(TELEMT_GOARCH)

ifeq ($(GOOS),windows)
OUTPUT = $(NAME).exe
ADDITION = go build -o w$(NAME).exe -trimpath -ldflags "-H windowsgui $(LDFLAGS)" -v $(MAIN)
Expand All @@ -58,7 +81,7 @@ ADDITION = GOMIPS=softfloat go build -o $(NAME)_softfloat -trimpath -ldflags "$(
endif

.PHONY: clean build test test-race-wireguard test-integration test-integration-full test-integration-wireguard \
install_geofiles check_geofiles update_geofiles_pin
install_geofiles check_geofiles update_geofiles_pin install_telemt

build:
CGO_ENABLED=0 go build -o $(OUTPUT) $(PARAMS) $(MAIN)
Expand Down Expand Up @@ -270,6 +293,66 @@ else
@exit 1
endif

install_telemt: update_os
ifeq ($(UNAME_S),Linux)
@if [ "$(TELEMT_OS_EFFECTIVE)" != "linux" ]; then \
echo "Unsupported TELEMT target OS: $(TELEMT_OS_EFFECTIVE)"; \
exit 1; \
fi
@if [ -z "$(TELEMT_ARCH_EFFECTIVE)" ]; then \
echo "Unsupported TELEMT target architecture: $(TELEMT_GOARCH_EFFECTIVE) (telemt is installed for amd64 and arm64 only)"; \
exit 1; \
fi
@set -eu; \
telemt_asset="telemt-$(TELEMT_ARCH_EFFECTIVE)-linux-musl.tar.gz"; \
telemt_release="$(TELEMT_VERSION)"; \
telemt_sha256="$(TELEMT_SHA256)"; \
case "$$telemt_release" in \
""|latest|*[!0-9A-Za-z._-]*) \
echo "TELEMT_VERSION must name a telemt release such as $(TELEMT_PINNED_VERSION), got '$$telemt_release'" >&2; \
exit 1;; \
esac; \
if [ -z "$$telemt_sha256" ]; then \
if [ "$$telemt_release" != "$(TELEMT_PINNED_VERSION)" ]; then \
echo "telemt $$telemt_release is not the pinned $(TELEMT_PINNED_VERSION): also pass TELEMT_SHA256=<sha256 of $$telemt_asset>" >&2; \
exit 1; \
fi; \
telemt_sha256="$(TELEMT_SHA256_PINNED_$(TELEMT_ARCH_EFFECTIVE))"; \
if [ -z "$$telemt_sha256" ]; then \
echo "no pinned SHA256 for $$telemt_asset: pass TELEMT_SHA256" >&2; \
exit 1; \
fi; \
fi; \
telemt_sha256="$$(printf '%s' "$$telemt_sha256" | tr 'A-F' 'a-f')"; \
case "$$telemt_sha256" in \
*[!0-9a-f]*) echo "TELEMT_SHA256 must be 64 hex characters" >&2; exit 1;; \
esac; \
if [ "$${#telemt_sha256}" -ne 64 ]; then \
echo "TELEMT_SHA256 must be 64 hex characters" >&2; \
exit 1; \
fi; \
echo "Installing telemt $$telemt_release ($$telemt_asset)"; \
tmp_dir="$$(mktemp -d)"; \
trap 'rm -rf "$$tmp_dir"' EXIT; \
curl -fsSL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 600 \
-o "$$tmp_dir/$$telemt_asset" \
"https://github.com/$(TELEMT_REPOSITORY)/releases/download/$$telemt_release/$$telemt_asset"; \
printf '%s %s\n' "$$telemt_sha256" "$$telemt_asset" > "$$tmp_dir/$$telemt_asset.sha256"; \
( cd "$$tmp_dir" && sha256sum -c "$$telemt_asset.sha256" ); \
tar -xzf "$$tmp_dir/$$telemt_asset" -C "$$tmp_dir" telemt; \
test -f "$$tmp_dir/telemt"; \
if [ -w "$(TELEMT_INSTALL_DIR)" ]; then install_cmd="install"; else install_cmd="sudo install"; fi; \
$$install_cmd -m 0755 "$$tmp_dir/telemt" "$(TELEMT_INSTALL_DIR)/telemt"
@if [ "$(TELEMT_ARCH_EFFECTIVE)" = "$(UNAME_M)" ]; then \
"$(TELEMT_INSTALL_DIR)/telemt" --version; \
else \
echo "Skipping telemt --version: $(TELEMT_ARCH_EFFECTIVE) binary on a $(UNAME_M) host"; \
fi
else
@echo "Unsupported operating system: $(UNAME_S)"
@exit 1
endif

test-integration:
$(MAKE) test-integration-full

Expand Down
184 changes: 184 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,190 @@ The easiest way to install PasarGuard Node is using our automated installation s
sudo bash -c "$(curl -sL https://github.com/PasarGuard/scripts/raw/main/pg-node.sh)" @ install
```

# MTProto backend
Besides Xray and WireGuard, the node can serve Telegram MTProto proxy users. The panel picks the backend per core, and a node runs one backend at a time.

The MTProto backend runs [telemt](https://github.com/telemt/telemt) as a separate process:
- The node image ships telemt 3.5.14 at `/usr/local/bin/telemt` (amd64 and arm64). The node refuses telemt older than 3.5.14.
- Set `TELEMT_EXECUTABLE_PATH` to use another binary.
- Outside Docker, `make install_telemt` installs the pinned release after checking it against the SHA256 pinned in the `Makefile`.
- telemt needs outbound access to Telegram. On start it checks the Telegram data centers, which can take up to a minute.
- Start can take up to 90 s while telemt probes Telegram, and the panel's retry attaches to the backend that is starting or running.
- Above 4096 users with stats or configured users, telemt stops its per-user series and the node bills from telemt's API totals, reported as downlink (no uplink and downlink split).
- telemt refuses a config file above 8 MiB, which is about 196k users on one node without WEB mode (fewer with WEB, see below).

## WEB mode
WEB mode carries MTProxy over real HTTPS (long polling or WebSocket) to your own domain on port 443. Links look like `tg://webproxy?server=proxy.example.com&secret=dd...` and have no port.
- Clients: Telegram Desktop's `WEB` proxy type, and Telegram iOS with the `https` carrier only. Android is not covered by telemt's docs and is untested.
- Secrets are `plain` or `dd`. Fake TLS (`ee`) is not used by WEB.
- telemt does not terminate TLS. A front (Caddy, nginx or HAProxy) holds the certificate on 443 and forwards plain HTTP/1.1 to telemt's WEB listener on a private address, with exactly one `X-Forwarded-For` address. pg-node runs the node with `network_mode: host`, so a front on the same host reaches `127.0.0.1:<port>`.
- telemt's own guide is [`docs/WEB/WEB_PROXY.en.md`](https://github.com/telemt/telemt/blob/3.5.14/docs/WEB/WEB_PROXY.en.md).

```text
Telegram client --HTTPS :443--> front (TLS) --HTTP/1.1--> 127.0.0.1:18080 (telemt WEB listener) --> Telegram
```

### Core config
What the node accepts in the MTProto core JSON:
- A `server.listeners[]` entry with `"transport": "web"`:
- `ip` is a loopback or private address (127/8, ::1, 10/8, 172.16/12, 192.168/16, fc00::/7). `0.0.0.0`, `::` and public addresses are refused.
- `port` is set, and differs from `server.port` and from every native listener's port.
- `web_trusted_proxy_cidrs` is required: the networks your front connects from, for example `["127.0.0.1/32"]`. telemt refuses a WEB listener with an empty list or a `/0` network.
- telemt's other WEB listener keys pass through, for example `web_client_ip_source`.
- A `web` section with `"enabled": true` and at least one `vhosts[]` entry:
- `host`: the public domain. `base_path` is optional: empty, or up to 128 ASCII bytes of `/`-separated segments that each start with a letter or digit and hold only letters, digits, `-` and `_`, with no leading or trailing `/` (for example `tg` or `dobry-cola/super_app`, not `/tg`).
- `public_addr`: the server's public IP on port 443, for example `203.0.113.10:443`.
- `decoy`: `{"mode": "http_upstream", "upstream": "http://<loopback or private IP>[:port]"}`. The upstream is a bare http origin: no `https`, path, query, fragment or credentials. Host names, `static_directory`, and a loopback upstream on the node's telemt API or metrics port are refused. The decoy is the site that ordinary visitors and probes see.
- `user_profile` (PasarGuard only, optional): `secret_mode` is `"dd"` (default) or `"plain"`, plus optional `max_sessions`, `max_streams` and `max_streams_per_session`. It applies to every user on that vhost. telemt refuses at Start a limit of 0 and limits above its global WEB limits: `max_sessions` above `web.limits.max_sessions_global` (default 128), `max_streams` above `web.limits.max_streams_global` (default 4096), `max_streams_per_session` above `web.limits.max_streams_per_session` (default 128) or above `max_streams`. It also refuses more vhosts than `web.limits.max_vhosts` (default 8).
- Refused: `web.vhosts[].profiles`, `web.debug`, `web.limits.max_profiles` and `include` anywhere.

The node writes one telemt WEB profile per user per vhost and sets `web.limits.max_profiles` to 65536. With WEB on, every user change rewrites telemt's config and hot reloads it: open sessions of unchanged users stay, removed users are cut.

telemt creates its default native listener only when no listeners are listed. Adding a WEB listener therefore turns native MTProto off unless you also list native listeners. The panel form writes them when WEB is enabled. If you edit the JSON by hand, add `{"ip": "0.0.0.0"}` and `{"ip": "::"}` (or your own).

Minimal example: native MTProto on 8443, WEB on `127.0.0.1:18080`, a decoy site on `127.0.0.1:18081`:
```json
{
"server": {
"port": 8443,
"listeners": [
{"ip": "0.0.0.0"},
{"ip": "::"},
{
"ip": "127.0.0.1",
"port": 18080,
"transport": "web",
"web_client_ip_source": "x_forwarded_for",
"web_trusted_proxy_cidrs": ["127.0.0.1/32"]
}
]
},
"censorship": {"tls_domain": "www.microsoft.com"},
"web": {
"enabled": true,
"carrier": "https",
"vhosts": [
{
"host": "proxy.example.com",
"public_addr": "203.0.113.10:443",
"decoy": {"mode": "http_upstream", "upstream": "http://127.0.0.1:18081"},
"user_profile": {"secret_mode": "dd"}
}
]
}
}
```
The panel requires `censorship.tls_domain` while fake TLS mode is on (telemt's default); it is the site that native fake TLS clients present. `"carrier": "https"` keeps iOS working. `https-lanes` needs public HTTP/2 and is not supported by iOS.

### Front examples
Both forward the whole domain to telemt, overwrite `X-Forwarded-For` with one address, and turn off response buffering. Timeouts must stay above telemt's 25 s long poll and twice the WebSocket liveness interval; 65 s covers the defaults. Do not enable upstream retries. Keep request URLs and the `Authorization` header out of access logs: they carry the users' WEB credentials.

Caddy (gets and renews the certificate itself):
```caddyfile
{
servers {
protocols h1 h2
}
}

proxy.example.com {
reverse_proxy 127.0.0.1:18080 {
header_up X-Forwarded-For {remote_host}
flush_interval -1
stream_close_delay 5m
}
}
```
Do not add `header_up Host`. `protocols h1 h2` keeps HTTP/3 off, and it applies to every site on that Caddy server.

nginx (the `map` goes in the `http` context):
```nginx
map $http_upgrade $telemt_connection_upgrade {
default upgrade;
'' '';
}

upstream telemt_web {
server 127.0.0.1:18080;
keepalive 64;
}

server {
listen 443 ssl;
http2 on;
server_name proxy.example.com;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
access_log off;

ssl_certificate /etc/letsencrypt/live/proxy.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/proxy.example.com/privkey.pem;

client_max_body_size 2m;

location / {
proxy_pass http://telemt_web;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $telemt_connection_upgrade;

proxy_connect_timeout 5s;
proxy_send_timeout 65s;
proxy_read_timeout 65s;
proxy_request_buffering off;
proxy_buffering off;
proxy_next_upstream off;
}
}
```
`http2 on;` needs nginx 1.25.1 or newer; older nginx fails `nginx -t` with an unknown directive, so drop that line and use `listen 443 ssl http2;` instead.

`client_max_body_size` must be at least telemt's `web.limits.max_body_bytes`.

Firewall the WEB listener port and the decoy port from outside, even on loopback.

### Port 443
The front needs public port 443, and native fake TLS usually wants 443 too. Either:
- move native MTProto to another port (8443 in the example above), or
- split 443 by host name in the front: nginx `stream` with `ssl_preread`, or HAProxy SNI routing. The WEB domain is terminated by the front and other names pass through to telemt's native listener. PasarGuard has not tested this split.

### Several nodes
A WEB domain points at one server, and `public_addr` is that server's IP. With several MTProto nodes, use one core per node.

### Capacity
- telemt holds at most 65536 WEB profiles (users times vhosts): 65536 users with one vhost, 32768 with two. Above that the node refuses the sync with an error and leaves telemt's config file unchanged.
- The 8 MiB config file limit allows about 69k users with one WEB vhost (about 121.6 bytes per user), and about 36k if every user has every optional limit set (about 232.9 bytes per user). Native only is about 196k (about 42.8 bytes per user). Each extra vhost adds an estimated 79 bytes per user (not measured).
- 65536 profiles fit telemt's default WEB memory envelope (1280 MiB, about 4 MiB free for profiles at 64 bytes each). If you raise other `web.limits` reservations or lower `web.limits.memory_envelope_bytes`, telemt refuses to start with `web.limits memory reservations must fit memory_envelope_bytes`.

## Xray and MTProto on one host
One node runs one backend, so serving Xray (or WireGuard) and MTProto from one server takes two node instances, each added to the panel as its own node:
- Each instance needs its own `SERVICE_PORT` and its own data directory (certificates and generated configs). With the installer, `--name` creates a separate instance, for example `... @ install --name pg-node-mtproto`.
- Both instances use the host network, so only one of them can listen on public port 443. Give the other one's inbound another port, for example 8443.

With Docker Compose (trimmed, see `docker-compose.yml` for the full service):
```yaml
services:
node: # Xray or WireGuard
image: pasarguard/node:latest
network_mode: host
environment:
SERVICE_PORT: 62050
volumes:
- /var/lib/pg-node:/var/lib/pg-node
node-mtproto: # MTProto
image: pasarguard/node:latest
network_mode: host
environment:
SERVICE_PORT: 62051
volumes:
- /var/lib/pg-node-mtproto:/var/lib/pg-node
```

## Third-party software
- telemt 3.5.14, TELEMT License 3.3, Copyright (c) 2026 Telemt. Source: https://github.com/telemt/telemt/tree/3.5.14
- The image contains the unmodified upstream release binary. The license text is in [`third_party/telemt/`](third_party/telemt/) and in the image at `/usr/share/licenses/telemt/`.
- The telemt license asks operators of a public service to credit telemt (for example in the service docs), without implying endorsement by the Telemt maintainers.

# Donation
You can help PasarGuard team with your donations, [Click Here](https://donate.pasarguard.org/)

Expand Down
31 changes: 31 additions & 0 deletions backend/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package backend

import "errors"

// RetryLaterError marks a user sync error that comes from the backend's
// state, not from the users sent: the backend is busy with an earlier
// change, its core is restarting, or it was shut down. The node answers it as
// unavailable (gRPC Unavailable, HTTP 503) with the same text, so a client
// retries the batch later instead of splitting it to find a user to blame.
type RetryLaterError struct {
Err error
}

func (e *RetryLaterError) Error() string { return e.Err.Error() }

func (e *RetryLaterError) Unwrap() error { return e.Err }

// RetryLater marks err as a RetryLaterError; nil stays nil.
func RetryLater(err error) error {
if err == nil {
return nil
}
return &RetryLaterError{Err: err}
}

// IsRetryLater reports whether err, or an error it wraps, is a
// RetryLaterError.
func IsRetryLater(err error) bool {
var retryLater *RetryLaterError
return errors.As(err, &retryLater)
}
Loading
Loading