Summary
On a default install, adding a secondary instance produces a container that
crash-loops immediately:
mtproxymax-9443 | Restarting (1) ExitCode: 1 Restarts: 6
Error: Os { code: 98, kind: AddrInUse, message: "Address already in use" }
The instance is handed a metrics port that the primary is already listening on,
so the multi-port instance feature cannot be used out of the box.
Environment
- MTProxyMax v1.4.0-LTS
- telemt 3.5.6 (
3693d1e)
- Alpine / OpenRC LXC, Docker
- Default primary ports (
PROXY_PORT=443, PROXY_METRICS_PORT=9090)
Reproduction
mtproxymax instance add 9443 test
sleep 5
docker ps -a --filter name=mtproxymax-9443
grep metrics_listen /opt/mtproxymax/mtproxy/config-9443.toml
The generated instance config contains:
metrics_listen = "127.0.0.1:9091"
and 9091 is already held:
netstat -tlnp | grep 9091 # 0.0.0.0:9091 telemt
Root cause
Two pieces that don't know about each other.
1. Every telemt instance listens on two ports, not one. The generated config
derives a stats port one above the metrics port (generate_telemt_config, lines
1248 and 1304-1305 on main):
local stats_port=$((metrics_port + 1)) # 1248
...
internal_stats_listen = "127.0.0.1:${stats_port}" # 1304
stats_listen = "127.0.0.1:${stats_port}" # 1305
So the primary with metrics 9090 also binds 9091.
2. The allocator only knows about metrics ports. _next_free_metrics_port
(line 13430 on main) starts at 9091 and skips only the primary's metrics
port and the recorded instance metrics ports:
_next_free_metrics_port() {
local p=9091
while [ "${p:-0}" -lt 9200 ]; do
local used=false
# Check against primary metrics port
[ "$p" = "${PROXY_METRICS_PORT:-9090}" ] && used=true
# Check against existing instance metrics ports
if [ "$used" = "false" ]; then
for mp in "${INSTANCE_METRICS_PORTS[@]}"; do
[ "$mp" = "$p" ] && used=true && break
done
fi
[ "$used" = "false" ] && { echo "$p"; return; }
((p++))
done
echo "9091"
}
With the default primary metrics port, the first candidate is 9091: it differs
from PROXY_METRICS_PORT (9090) and INSTANCE_METRICS_PORTS is empty, so it is
returned. The instance is then told to listen on the primary's stats port.
Second-order case
The allocator never reserves metrics + 1 for the instances it hands out
either, so fixing only the primary collision is not enough. Instance 1 is given
metrics 9091 → its own stats listener takes 9092. INSTANCE_METRICS_PORTS then
contains only 9091, so instance 2 is offered 9092 and collides with instance
1's stats port.
Suggested fix
Treat each listener as a pair of ports. A candidate p is only usable if
neither p nor p + 1 is taken by the primary (PROXY_METRICS_PORT and
PROXY_METRICS_PORT + 1) or by any recorded instance (mp and mp + 1 — the
latter is what INSTANCE_METRICS_PORTS currently omits). Allocating from
PROXY_METRICS_PORT + 2 in steps of two is equivalent for fresh installs, but a
set-based check also behaves correctly for installs that already have
odd-looking recorded ports.
While in there: the echo "9091" fallback after the loop returns a port that is
known to collide, so exhausting 9091-9199 silently reintroduces the bug instead
of reporting that no port is free.
Notes
- Any install whose primary metrics port is not the default is unaffected,
because 9091 then happens to be free — which is probably why this survived.
Summary
On a default install, adding a secondary instance produces a container that
crash-loops immediately:
The instance is handed a metrics port that the primary is already listening on,
so the multi-port instance feature cannot be used out of the box.
Environment
3693d1e)PROXY_PORT=443,PROXY_METRICS_PORT=9090)Reproduction
The generated instance config contains:
and 9091 is already held:
Root cause
Two pieces that don't know about each other.
1. Every telemt instance listens on two ports, not one. The generated config
derives a stats port one above the metrics port (
generate_telemt_config, lines1248 and 1304-1305 on
main):So the primary with metrics 9090 also binds 9091.
2. The allocator only knows about metrics ports.
_next_free_metrics_port(line 13430 on
main) starts at 9091 and skips only the primary's metricsport and the recorded instance metrics ports:
With the default primary metrics port, the first candidate is 9091: it differs
from
PROXY_METRICS_PORT(9090) andINSTANCE_METRICS_PORTSis empty, so it isreturned. The instance is then told to listen on the primary's stats port.
Second-order case
The allocator never reserves
metrics + 1for the instances it hands outeither, so fixing only the primary collision is not enough. Instance 1 is given
metrics 9091 → its own stats listener takes 9092.
INSTANCE_METRICS_PORTSthencontains only
9091, so instance 2 is offered 9092 and collides with instance1's stats port.
Suggested fix
Treat each listener as a pair of ports. A candidate
pis only usable ifneither
pnorp + 1is taken by the primary (PROXY_METRICS_PORTandPROXY_METRICS_PORT + 1) or by any recorded instance (mpandmp + 1— thelatter is what
INSTANCE_METRICS_PORTScurrently omits). Allocating fromPROXY_METRICS_PORT + 2in steps of two is equivalent for fresh installs, but aset-based check also behaves correctly for installs that already have
odd-looking recorded ports.
While in there: the
echo "9091"fallback after the loop returns a port that isknown to collide, so exhausting 9091-9199 silently reintroduces the bug instead
of reporting that no port is free.
Notes
because 9091 then happens to be free — which is probably why this survived.