Skip to content
This repository was archived by the owner on Apr 5, 2026. It is now read-only.

Commit f5aacb9

Browse files
committed
feat: add secret labels and per-secret metrics (#60)
Associate human-readable labels with configured secrets and surface per-secret connection metrics in logs and stats endpoints. - `-S SECRET:LABEL` syntax (backward compatible, label is optional) - Per-secret active/total connection counters in /stats and /metrics - Matched secret label logged at handshake (no raw secrets in logs) - Docker: SECRET_LABEL_N env vars and inline label support - Works for both ME relay and direct-to-DC modes Closes #60
1 parent 8dbf9dc commit f5aacb9

6 files changed

Lines changed: 180 additions & 20 deletions

File tree

README.md

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ The table below compares it with the original and the two main third-party alter
3333
| Fake-TLS (EE mode) | Yes | Yes | Yes | Yes |
3434
| Direct-to-DC mode | No | Yes | Yes | Yes |
3535
| Ad proxy tag | Yes | Yes | No | Yes |
36-
| Multiple secrets | Yes | Yes (up to 16) | No | Yes |
36+
| Multiple secrets | Yes | Yes (up to 16, with labels) | No | Yes |
3737
| Anti-replay protection | Weak | Yes | Yes | Partial |
3838
| Constant-time HMAC | No | Yes || Yes |
3939
| ***Censorship resistance*** | | | | |
@@ -143,7 +143,7 @@ head -c 16 /dev/urandom | xxd -ps
143143
- `nobody` is the username. `mtproto-proxy` calls `setuid()` to drop privilegies.
144144
- `443` is the port, used by clients to connect to the proxy.
145145
- `8888` is the local port for statistics (requires `--http-stats`). Like `curl http://localhost:8888/stats`. Stats are accessible from private networks (loopback, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) but not from public IPs.
146-
- `<secret>` is the secret generated at step 3. Also you can set multiple secrets: `-S <secret1> -S <secret2>`.
146+
- `<secret>` is the secret generated at step 3. Also you can set multiple secrets: `-S <secret1> -S <secret2>`. Each secret can have an optional label: `-S <secret>:family -S <secret>:friends`. Labels appear in logs and stats instead of raw secrets, making it easy to identify which secret a connection used.
147147
- `--aes-pwd proxy-secret` points to the `proxy-secret` file downloaded at step 1, which contains the encryption key used for MTProto key exchange with Telegram DCs.
148148
- `proxy-secret` and `proxy-multi.conf` are obtained at steps 1 and 2.
149149
- `1` is the number of workers. You can increase the number of workers, if you have a powerful server.
@@ -463,9 +463,11 @@ docker run -d \
463463
- `SECRET`: Proxy secret(s) — 32 hex characters each (auto-generated if not provided)
464464
- Single: `SECRET=cafe1234567890abcdef1234567890ab`
465465
- Multiple (comma-separated): `SECRET=secret1,secret2,secret3`
466+
- With labels: `SECRET=hex1:family,hex2:friends` (see [Secret Labels](#secret-labels))
466467
- Multiple (numbered): `SECRET_1=aabb...`, `SECRET_2=ccdd...` (up to `SECRET_16`)
467468
- If both `SECRET` and `SECRET_N` are set, all are combined
468469
- Maximum 16 secrets (binary limit)
470+
- `SECRET_LABEL_1`, `SECRET_LABEL_2`, ...: Optional labels for numbered secrets (e.g. `SECRET_LABEL_1=family`). See [Secret Labels](#secret-labels)
469471
- `PORT`: Port for client connections (default: 443)
470472
- `STATS_PORT`: Port for statistics endpoint (default: 8888)
471473
- `WORKERS`: Number of worker processes (default: 1)
@@ -489,7 +491,7 @@ curl http://localhost:8888/stats
489491
curl http://localhost:8888/metrics
490492
```
491493

492-
Returns metrics in [Prometheus exposition format](https://prometheus.io/docs/instrumenting/exposition_formats/), ready for scraping. Available on the same `--http-stats` port, restricted to private networks.
494+
Returns metrics in [Prometheus exposition format](https://prometheus.io/docs/instrumenting/exposition_formats/), ready for scraping. Available on the same `--http-stats` port, restricted to private networks. Includes per-secret connection metrics when [secret labels](#secret-labels) are configured.
493495

494496
### Using Docker Compose
495497

@@ -529,6 +531,34 @@ SECRET_2=friends_secret_hex
529531
SECRET_3=public_secret_hex
530532
```
531533

534+
#### Secret Labels
535+
536+
Labels let you identify which secret a connection is using — useful for revoking leaked
537+
secrets or monitoring per-group traffic:
538+
539+
```bash
540+
# Inline labels (CLI)
541+
./mtproto-proxy ... -S cafe1234567890abcdef1234567890ab:family -S dead1234567890abcdef1234567890ef:friends
542+
543+
# Inline labels (Docker)
544+
SECRET=cafe1234567890abcdef1234567890ab:family,dead1234567890abcdef1234567890ef:friends
545+
546+
# Separate label env vars (Docker)
547+
SECRET_1=cafe1234567890abcdef1234567890ab
548+
SECRET_LABEL_1=family
549+
SECRET_2=dead1234567890abcdef1234567890ef
550+
SECRET_LABEL_2=friends
551+
```
552+
553+
Labels appear in:
554+
- **Logs**: `TLS handshake matched secret [family] from 1.2.3.4:12345`
555+
- **Prometheus** (`/metrics`): `mtproxy_secret_connections{secret="family"} 3`
556+
- **Stats** (`/stats`): `secret_family_connections 3`
557+
558+
If no label is given, secrets are auto-labeled `secret_0`, `secret_1`, etc.
559+
560+
Label rules: max 32 characters, alphanumeric plus `_` and `-` only.
561+
532562
And reference it in your `docker-compose.yml`:
533563
```yaml
534564
services:

mtproto/mtproto-proxy.c

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ struct ext_connection_ref {
196196
};
197197

198198
long long ext_connections, ext_connections_created;
199+
long long per_secret_connections[16], per_secret_connections_created[16];
199200

200201
struct ext_connection_ref OutExtConnections[EXT_CONN_TABLE_SIZE];
201202
struct ext_connection *InExtConnectionHash[EXT_CONN_HASH_SIZE];
@@ -410,6 +411,9 @@ struct worker_stats {
410411

411412
long long ext_connections, ext_connections_created;
412413
long long http_queries, http_bad_headers;
414+
415+
long long per_secret_connections[16];
416+
long long per_secret_connections_created[16];
413417
};
414418

415419
struct worker_stats *WStats, SumStats;
@@ -464,8 +468,12 @@ static void update_local_stats_copy (struct worker_stats *S) {
464468
UPD (connections_failed_flood);
465469
UPD (ext_connections);
466470
UPD (ext_connections_created);
467-
UPD (http_queries);
471+
UPD (http_queries);
468472
UPD (http_bad_headers);
473+
{ int _i; for (_i = 0; _i < 16; _i++) {
474+
UPD (per_secret_connections[_i]);
475+
UPD (per_secret_connections_created[_i]);
476+
}}
469477
#undef UPD
470478
__sync_synchronize();
471479
S->cnt++;
@@ -538,10 +546,14 @@ static inline void add_stats (struct worker_stats *W) {
538546
UPD (direct_dc_connections_active);
539547
UPD (connections_failed_lru);
540548
UPD (connections_failed_flood);
541-
UPD (ext_connections);
542-
UPD (ext_connections_created);
543-
UPD (http_queries);
549+
UPD (ext_connections);
550+
UPD (ext_connections_created);
551+
UPD (http_queries);
544552
UPD (http_bad_headers);
553+
{ int _i; for (_i = 0; _i < 16; _i++) {
554+
UPD (per_secret_connections[_i]);
555+
UPD (per_secret_connections_created[_i]);
556+
}}
545557
#undef UPD
546558
}
547559

@@ -735,6 +747,17 @@ void mtfront_prepare_stats (stats_buffer_t *sb) {
735747
S(direct_dc_connections_created),
736748
S(direct_dc_connections_active)
737749
);
750+
751+
{ int _sc = tcp_rpcs_get_ext_secret_count();
752+
int _i;
753+
for (_i = 0; _i < _sc; _i++) {
754+
sb_printf (sb,
755+
"secret_%s_connections\t%lld\n"
756+
"secret_%s_connections_created\t%lld\n",
757+
tcp_rpcs_get_ext_secret_label (_i), S(per_secret_connections[_i]),
758+
tcp_rpcs_get_ext_secret_label (_i), S(per_secret_connections_created[_i]));
759+
}
760+
}
738761
#undef S
739762
#undef S1
740763
#undef SW
@@ -888,6 +911,26 @@ void mtfront_prepare_prometheus_stats (stats_buffer_t *sb) {
888911
S(direct_dc_connections_active)
889912
);
890913

914+
{ int _sc = tcp_rpcs_get_ext_secret_count();
915+
if (_sc > 0) {
916+
int _i;
917+
sb_printf (sb,
918+
"# HELP mtproxy_secret_connections Current connections per configured secret.\n"
919+
"# TYPE mtproxy_secret_connections gauge\n");
920+
for (_i = 0; _i < _sc; _i++) {
921+
sb_printf (sb, "mtproxy_secret_connections{secret=\"%s\"} %lld\n",
922+
tcp_rpcs_get_ext_secret_label (_i), S(per_secret_connections[_i]));
923+
}
924+
sb_printf (sb,
925+
"# HELP mtproxy_secret_connections_created_total Total connections per configured secret.\n"
926+
"# TYPE mtproxy_secret_connections_created_total counter\n");
927+
for (_i = 0; _i < _sc; _i++) {
928+
sb_printf (sb, "mtproxy_secret_connections_created_total{secret=\"%s\"} %lld\n",
929+
tcp_rpcs_get_ext_secret_label (_i), S(per_secret_connections_created[_i]));
930+
}
931+
}
932+
}
933+
891934
#undef S
892935
#undef S1
893936
#undef SW
@@ -1255,13 +1298,22 @@ int mtproto_http_close (connection_job_t C, int who) {
12551298
int mtproto_ext_rpc_ready (connection_job_t C) {
12561299
assert ((unsigned) CONN_INFO(C)->fd < MAX_CONNECTIONS);
12571300
vkprintf (3, "ext_rpc connection ready (%d)\n", CONN_INFO(C)->fd);
1301+
int sid = TCP_RPC_DATA(C)->extra_int2;
1302+
if (sid > 0 && sid <= 16) {
1303+
per_secret_connections[sid - 1]++;
1304+
per_secret_connections_created[sid - 1]++;
1305+
}
12581306
lru_insert_conn (C);
12591307
return 0;
12601308
}
12611309

12621310
int mtproto_ext_rpc_close (connection_job_t C, int who) {
12631311
assert ((unsigned) CONN_INFO(C)->fd < MAX_CONNECTIONS);
12641312
vkprintf (3, "ext_rpc connection closing (%d) by %d\n", CONN_INFO(C)->fd, who);
1313+
int sid = TCP_RPC_DATA(C)->extra_int2;
1314+
if (sid > 0 && sid <= 16) {
1315+
per_secret_connections[sid - 1]--;
1316+
}
12651317
struct ext_connection *Ex = get_ext_connection_by_in_fd (CONN_INFO(C)->fd);
12661318
if (Ex) {
12671319
remove_ext_connection (Ex, 1);
@@ -2393,7 +2445,36 @@ int f_parse_option (int val) {
23932445
case 'S':
23942446
case 'P':
23952447
{
2396-
if (strlen (optarg) != 32) {
2448+
char *label = NULL;
2449+
int hex_len;
2450+
2451+
if (val == 'S') {
2452+
char *colon = strchr (optarg, ':');
2453+
if (colon) {
2454+
hex_len = colon - optarg;
2455+
label = colon + 1;
2456+
if (strlen (label) == 0) {
2457+
label = NULL;
2458+
} else if (strlen (label) > EXT_SECRET_LABEL_MAX) {
2459+
kprintf ("Secret label too long (max %d chars)\n", EXT_SECRET_LABEL_MAX);
2460+
usage ();
2461+
} else {
2462+
const char *p;
2463+
for (p = label; *p; p++) {
2464+
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_' || *p == '-')) {
2465+
kprintf ("Secret label contains invalid character '%c' (allowed: a-z, A-Z, 0-9, _, -)\n", *p);
2466+
usage ();
2467+
}
2468+
}
2469+
}
2470+
} else {
2471+
hex_len = strlen (optarg);
2472+
}
2473+
} else {
2474+
hex_len = strlen (optarg);
2475+
}
2476+
2477+
if (hex_len != 32) {
23972478
kprintf ("'%c' option requires exactly 32 hex digits\n", val);
23982479
usage ();
23992480
}
@@ -2418,7 +2499,7 @@ int f_parse_option (int val) {
24182499
}
24192500
}
24202501
if (val == 'S') {
2421-
tcp_rpcs_set_ext_secret (secret);
2502+
tcp_rpcs_set_ext_secret (secret, label);
24222503
secret_count++;
24232504
} else {
24242505
memcpy (proxy_tag, secret, sizeof (proxy_tag));
@@ -2446,7 +2527,7 @@ int f_parse_option (int val) {
24462527

24472528
void mtfront_prepare_parse_options (void) {
24482529
parse_option ("http-stats", no_argument, 0, 2000, "allow http server to answer on stats queries");
2449-
parse_option ("mtproto-secret", required_argument, 0, 'S', "16-byte secret in hex mode");
2530+
parse_option ("mtproto-secret", required_argument, 0, 'S', "16-byte secret in hex, optionally followed by :LABEL (e.g. -S abcdef01234567890abcdef012345678:myapp)");
24502531
parse_option ("proxy-tag", required_argument, 0, 'P', "16-byte proxy tag in hex mode to be passed along with all forwarded queries");
24512532
parse_option ("domain", required_argument, 0, 'D', "adds allowed domain or host:port for TLS-transport mode, disables other transports; can be specified more than once");
24522533
parse_option ("max-special-connections", required_argument, 0, 'C', "sets maximal number of accepted client connections per worker");

net/net-tcp-rpc-common.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ struct tcp_rpc_data {
134134
void *extra;
135135
};
136136
int extra_int;
137-
int extra_int2;
137+
int extra_int2; /* matched secret index + 1 (0 = unset) */
138138
int extra_int3;
139139
int extra_int4;
140140
double extra_double, extra_double2;

net/net-tcp-rpc-ext-server.c

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ int tcp_proxy_pass_write_packet (connection_job_t C, struct raw_message *raw) {
177177

178178
extern int direct_mode;
179179
extern long long direct_dc_connections_created, direct_dc_connections_active;
180+
extern long long per_secret_connections[16], per_secret_connections_created[16];
180181

181182
static int tcp_direct_client_parse_execute (connection_job_t C);
182183
static int tcp_direct_dc_parse_execute (connection_job_t C);
@@ -278,6 +279,10 @@ static int tcp_direct_close (connection_job_t C, int who) {
278279
vkprintf (1, "closing direct connection #%d %s:%d -> %s:%d\n", c->fd, show_our_ip (C), c->our_port, show_remote_ip (C), c->remote_port);
279280
if ((c->type == &ct_direct_client || c->type == &ct_direct_client_drs) && direct_dc_connections_active > 0) {
280281
direct_dc_connections_active--;
282+
int sid = TCP_RPC_DATA(C)->extra_int2;
283+
if (sid > 0 && sid <= 16) {
284+
per_secret_connections[sid - 1]--;
285+
}
281286
}
282287
if (c->extra) {
283288
job_t E = PTR_MOVE (c->extra);
@@ -430,6 +435,12 @@ static int direct_connect_to_dc (connection_job_t C, int target_dc) {
430435
direct_dc_connections_created++;
431436
direct_dc_connections_active++;
432437

438+
int sid = TCP_RPC_DATA(C)->extra_int2;
439+
if (sid > 0 && sid <= 16) {
440+
per_secret_connections[sid - 1]++;
441+
per_secret_connections_created[sid - 1]++;
442+
}
443+
433444
assert (CONN_INFO(EJ)->io_conn);
434445
unlock_job (JOB_REF_PASS (EJ));
435446

@@ -447,10 +458,27 @@ int tcp_rpcs_default_execute (connection_job_t c, int op, struct raw_message *ms
447458
static unsigned char ext_secret[16][16];
448459
static int ext_secret_cnt = 0;
449460
static int ext_rand_pad_only = 0;
461+
static char ext_secret_label[16][EXT_SECRET_LABEL_MAX + 1];
450462

451-
void tcp_rpcs_set_ext_secret (unsigned char secret[16]) {
463+
void tcp_rpcs_set_ext_secret (unsigned char secret[16], const char *label) {
452464
assert (ext_secret_cnt < 16);
453-
memcpy (ext_secret[ext_secret_cnt ++], secret, 16);
465+
int idx = ext_secret_cnt++;
466+
memcpy (ext_secret[idx], secret, 16);
467+
if (label && label[0]) {
468+
snprintf (ext_secret_label[idx], sizeof (ext_secret_label[idx]), "%s", label);
469+
} else {
470+
snprintf (ext_secret_label[idx], sizeof (ext_secret_label[idx]), "secret_%d", idx);
471+
}
472+
vkprintf (0, "Added secret #%d label=[%s]\n", idx, ext_secret_label[idx]);
473+
}
474+
475+
const char *tcp_rpcs_get_ext_secret_label (int index) {
476+
assert (index >= 0 && index < ext_secret_cnt);
477+
return ext_secret_label[index];
478+
}
479+
480+
int tcp_rpcs_get_ext_secret_count (void) {
481+
return ext_secret_cnt;
454482
}
455483

456484
void tcp_rpcs_set_ext_rand_pad_only(int set) {
@@ -1419,6 +1447,9 @@ int tcp_rpcs_compact_parse_execute (connection_job_t C) {
14191447
RETURN_TLS_ERROR(info);
14201448
}
14211449

1450+
D->extra_int2 = secret_id + 1;
1451+
vkprintf (1, "TLS handshake matched secret [%s] from %s:%d\n", ext_secret_label[secret_id], show_remote_ip (C), c->remote_port);
1452+
14221453
unsigned char cipher_suite_id;
14231454
if (tls_parse_client_hello_ciphers (client_hello, read_len, &cipher_suite_id) < 0) {
14241455
vkprintf (1, "Can't find supported cipher suite\n");
@@ -1576,7 +1607,8 @@ int tcp_rpcs_compact_parse_execute (connection_job_t C) {
15761607

15771608
int target = *(short *)(random_header + 60);
15781609
D->extra_int4 = target;
1579-
vkprintf (1, "tcp opportunistic encryption mode detected, tag = %08x, target=%d\n", tag, target);
1610+
D->extra_int2 = secret_id + 1;
1611+
vkprintf (1, "tcp opportunistic encryption mode detected, tag = %08x, target=%d, secret [%s]\n", tag, target, ext_secret_label[secret_id]);
15801612
ok = 1;
15811613
break;
15821614
} else {

net/net-tcp-rpc-ext-server.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,12 @@ extern conn_type_t ct_tcp_rpc_ext_server;
2929

3030
int tcp_rpcs_compact_parse_execute (connection_job_t c);
3131

32-
void tcp_rpcs_set_ext_secret(unsigned char secret[16]);
32+
#define EXT_SECRET_LABEL_MAX 32
33+
34+
void tcp_rpcs_set_ext_secret(unsigned char secret[16], const char *label);
3335
void tcp_rpcs_set_ext_rand_pad_only(int set);
36+
const char *tcp_rpcs_get_ext_secret_label(int index);
37+
int tcp_rpcs_get_ext_secret_count(void);
3438

3539
void tcp_rpc_add_proxy_domain (const char *domain);
3640

start.sh

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,15 @@ _i=1
6262
while [ "$_i" -le 16 ]; do
6363
eval "_val=\${SECRET_${_i}:-}"
6464
_val=$(printf '%s' "$_val" | tr -d '[:space:]')
65-
[ -n "$_val" ] && set -- "$@" "$_val"
65+
if [ -n "$_val" ]; then
66+
eval "_lbl=\${SECRET_LABEL_${_i}:-}"
67+
_lbl=$(printf '%s' "$_lbl" | tr -d '[:space:]')
68+
if [ -n "$_lbl" ]; then
69+
set -- "$@" "${_val}:${_lbl}"
70+
else
71+
set -- "$@" "$_val"
72+
fi
73+
fi
6674
_i=$((_i + 1))
6775
done
6876

@@ -156,16 +164,21 @@ echo ""
156164
echo "===== Connection Links ====="
157165
_host="${EXTERNAL_IP:-<YOUR_SERVER_IP>}"
158166
for _s in "$@"; do
167+
# Strip :LABEL suffix for URLs (labels are for the proxy, not for clients)
168+
_secret_hex=$(printf '%s' "$_s" | cut -d: -f1)
169+
_label=$(printf '%s' "$_s" | grep -o ':.*' | cut -c2- || true)
159170
if [ -n "$EE_DOMAIN" ]; then
160171
_domain_only=$(printf '%s' "$EE_DOMAIN" | cut -d: -f1)
161172
_domain_hex=$(printf '%s' "$_domain_only" | od -An -tx1 | tr -d ' \n')
162-
_full="ee${_s}${_domain_hex}"
173+
_full="ee${_secret_hex}${_domain_hex}"
163174
elif [ "$RANDOM_PADDING" = "true" ]; then
164-
_full="dd${_s}"
175+
_full="dd${_secret_hex}"
165176
else
166-
_full="$_s"
177+
_full="$_secret_hex"
167178
fi
168-
echo "https://t.me/proxy?server=${_host}&port=${PORT}&secret=${_full}"
179+
_label_display=""
180+
[ -n "$_label" ] && _label_display=" [$_label]"
181+
echo "https://t.me/proxy?server=${_host}&port=${PORT}&secret=${_full}${_label_display}"
169182
done
170183
if [ "$_host" = "<YOUR_SERVER_IP>" ]; then
171184
echo "(Set EXTERNAL_IP to show your server's IP)"

0 commit comments

Comments
 (0)