# Port-range Rules (https://portunus.bybee.dev/en/docs/features/port-range)



Available since v0.2.0. A single rule binds many listeners atomically.

## How it works [#how-it-works]

A port-range rule maps a contiguous listen-port range onto the **same
offset** in a contiguous target-port range on a single upstream host.

```
Listen 30000-30050  →  upstream.local:30000-30050
                       ^ each port forwards to its same-offset target
                         (30005 → upstream.local:30005, etc.)
```

Range rules collapse to **one row** in `list-rules` and **one row per
collector** in `/metrics` regardless of range size. Per-port detail is
opt-in via `--per-port`.

## Push a range rule [#push-a-range-rule]

```sh
portunus-server push-rule edge-01 30000-30050 upstream.local:30000-30050
# binds 51 ports atomically
```

UDP works the same way:

```sh
portunus-server push-rule edge-01 30000-30050 upstream.local:30000-30050 \
  --protocol udp
```

## Limits [#limits]

The default cap is **1024 ports per range**, configurable in `server.toml`:

```toml
# Maximum ports a single port-range rule may span.
# Each port consumes one socket (one fd) on the client.
range_rule_max_ports = 1024
```

<Callout type="warn">
  Raising this above the default also requires raising `LimitNOFILE` on
  the client's systemd unit. The cap exists so an operator typo can't
  burst the client's RLIMIT\_NOFILE.
</Callout>

## Per-port stats [#per-port-stats]

```sh
# CLI
portunus-server rule-stats <rule_id> --per-port

# HTTP
curl -H "Authorization: Bearer $PORTUNUS_OPERATOR_TOKEN" \
     'http://127.0.0.1:7080/v1/rules/<id>/stats?per_port=true'
```

Per-port byte counters surface only through the API/CLI on demand; they
are **not** exported as separate Prometheus series. The Prometheus
cardinality budget stays one row per rule no matter how wide the range.

## Conflict handling [#conflict-handling]

Range conflicts reuse the v1 `port_in_use` error code with the offending
port named in the message:

```
error: activation_failed: port_in_use (port 30005 already bound)
```

The whole range is atomic — if any port in the requested range is
unavailable, the entire rule transitions to `Failed` and binds nothing.

## Verified [#verified]

* 100-port range push wall-clock: **\~18 ms** on a Linux x86\_64 host.
* Total fresh-deploy → traffic round-trip on 3 sample ports: **\~0.93 s**.
