Skip to content

mtproxymax instance add crash-loops with AddrInUse: _next_free_metrics_port never reserves the metrics+1 stats port #135

Description

@rvalitov

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions