From 33d1b212fb602e35d2f360b9136d426be2c478c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20H=C3=A4rtter?= <108095150+janekhaertter@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:45:23 +0200 Subject: [PATCH] docs: describe how reverse proxy services share ports (#997) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: describe how reverse proxy services share ports The reverse proxy page said every L4 service needs a dedicated port and that each port can only be used by one service. That is not what the proxy and management do: the conflict check is per cluster, protocol and port, so a port takes any number of TLS services (told apart by SNI), at most one TCP service (the catch-all for unmatched connections), and, counted separately, at most one UDP service. Also correct the main-port description: a TCP service may listen on the main port too, and then receives the connections that match no SNI route, instead of the HTTP reverse proxy. Verified against checkPortConflict (management/internals/modules/ reverseproxy/service/manager/manager.go) and handleUnmatched (proxy/internal/tcp/router.go) in v0.79.0, and on a self-hosted cluster: TLS services on 443 are routed by SNI, unmatched and plain TCP connections reach the TCP service on 443, a second TCP service on 443 is rejected with 409, and a UDP service on 443 is accepted alongside them. Co-Authored-By: Claude Opus 5.5 * docs: TLS listen ports, custom-port fallback and one domain per service TLS services always need an explicit listen port, including on clusters without custom port selection, where the dashboard keeps the field editable; auto-assignment applies to TCP and UDP. On a custom port with no TCP service, unmatched and HTTP connections are dropped, and non-TLS connections on the main port take the same fallback as unmatched SNI. A domain belongs to one service, so the HTTP/TLS same-hostname warning now says creation fails. In Docker the main port is 8443, so listen port 443 is a custom port. Drop the ECH example from the no-SNI case. * docs: HTTP hostnames on a custom port fall back to its TCP service A request for an HTTP service's hostname on a custom port matches no route there, so it follows the same fallback as any unmatched connection: the port's TCP service if it has one, otherwise dropped. * docs: tighten the L4 port overview and the port-sharing warning The port-sharing warning read as if a second service of any protocol fails on a port; only TCP and UDP are limited to one per port, while TLS services share by SNI. Say that directly, shorten the L4 overview to a summary that links the two sections holding the rules, and name the listen port in the TCP and UDP mode descriptions. --------- Co-authored-by: Janek Härtter Co-authored-by: Claude Opus 5.5 Co-authored-by: Jack Carter <128555021+SunsetDrifter@users.noreply.github.com> --- src/pages/manage/reverse-proxy/index.mdx | 28 ++++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/pages/manage/reverse-proxy/index.mdx b/src/pages/manage/reverse-proxy/index.mdx index e6dc9f23..d43da645 100644 --- a/src/pages/manage/reverse-proxy/index.mdx +++ b/src/pages/manage/reverse-proxy/index.mdx @@ -53,11 +53,11 @@ The service mode determines how the proxy handles traffic between clients and yo | Mode | Layer | Description | |------|-------|-------------| | **HTTP** | L7 | TLS termination at the proxy, HTTP-level forwarding. Supports path-based routing, host header forwarding, redirect rewriting, and browser-based authentication (SSO, password, PIN). | -| **TCP** | L4 | Raw TCP relay. The proxy accepts TCP connections on a dedicated port and forwards them to your backend. | -| **UDP** | L4 | UDP relay with session tracking. The proxy accepts UDP packets on a dedicated port and forwards them to your backend. Sessions are reaped after an idle timeout. | +| **TCP** | L4 | Raw TCP relay. The proxy accepts TCP connections on the service's listen port and forwards them to your backend. | +| **UDP** | L4 | UDP relay with session tracking. The proxy accepts UDP packets on the service's listen port and forwards them to your backend. Sessions are reaped after an idle timeout. | | **TLS** | L4 | TLS passthrough with SNI-based routing. The proxy inspects the TLS ClientHello to read the SNI hostname and forwards the encrypted connection to your backend without terminating TLS. | -L4 services (TCP, UDP, TLS) listen on a dedicated port on the proxy cluster. Depending on the cluster, the port may be auto-assigned or you can specify one manually. The proxy cluster's `supports_custom_ports` capability determines whether manual port selection is available. +L4 services (TCP, UDP, TLS) listen on a port on the proxy cluster, and several services can share a port. TCP and UDP services can have their port auto-assigned or, on clusters that support custom ports (`supports_custom_ports`), chosen by you. TLS services always need a port you choose. See [Port allocation for L4 services](#port-allocation-for-l4-services) and [How services share ports](#how-services-share-ports). L4 services do not support browser-based authentication (SSO, password, PIN) or header authentication because there is no HTTP layer. You can use [access restrictions](/manage/reverse-proxy/authentication#access-restrictions) (IP CIDR, country, and CrowdSec rules) to protect L4 services. @@ -360,15 +360,17 @@ Within a service, you can: ## Port allocation for L4 services -L4 services (TCP, UDP, TLS) require a dedicated port on the proxy cluster. How ports are assigned depends on whether the proxy cluster supports custom port selection: +L4 services (TCP, UDP, TLS) listen on a port on the proxy cluster. How ports are assigned depends on whether the proxy cluster supports custom port selection: - **Custom port selection available**: you choose the exact port the proxy listens on. This is useful when clients expect a well-known port (e.g., 5432 for PostgreSQL, 3306 for MySQL). Self-hosted proxy clusters support this when configured to allow it. -- **Auto-assigned ports only**: the proxy cluster automatically assigns an available port. This is the case for NetBird's shared cloud proxy clusters, where port allocation is managed to avoid conflicts between accounts. The assigned port is shown in the service details after creation. +- **Auto-assigned ports only**: the proxy cluster automatically assigns an available port to TCP and UDP services. This is the case for NetBird's shared cloud proxy clusters, where port allocation is managed to avoid conflicts between accounts. The assigned port is shown in the service details after creation. -The dashboard indicates whether a proxy cluster supports custom ports when you select the domain. If custom ports are not supported, the listen port field is read-only and populated after creation. +TLS services are the exception: they always need a listen port that you choose, including on clusters without custom port selection, because TLS services share ports by SNI hostname. Creating a TLS service without a listen port fails. + +The dashboard indicates whether a proxy cluster supports custom ports when you select the domain. If custom ports are not supported, the listen port field of a TCP or UDP service is read-only and populated after creation. For a TLS service, the field stays editable. - Each port on a proxy cluster can only be used by one service at a time. If you specify a port that is already in use by another service, creation will fail. L4 listen ports also cannot conflict with the proxy's tunnel port. + A port on a proxy cluster can be shared by any number of TLS services, told apart by their SNI hostname, plus at most one TCP service and at most one UDP service. The TCP service receives every connection that matches no TLS service (or, on the main port, no HTTP service). Creating a TCP or UDP service fails if the port already has one of that protocol, and creating any service fails if another service already uses its domain. L4 listen ports also cannot conflict with the proxy's tunnel port. @@ -376,7 +378,7 @@ The dashboard indicates whether a proxy cluster supports custom ports when you s - **Self-hosted Docker deployments:** The default Docker Compose configuration only routes port 443 (via Traefik TLS passthrough) to the proxy container. L4 services that listen on additional TCP or UDP ports require you to manually expose those ports in your `docker-compose.yml`. See the [migration guide](/selfhosted/migration/enable-reverse-proxy#exposing-l4-ports) for instructions. + **Self-hosted Docker deployments:** The default Docker Compose configuration only routes port 443 (via Traefik TLS passthrough) to the proxy container, where the proxy's main port is `8443` (`NB_PROXY_ADDRESS`). A TLS service given listen port `443` in such a deployment therefore listens on a custom port inside the container, not on the main port. L4 services that listen on additional TCP or UDP ports require you to manually expose those ports in your `docker-compose.yml`. See the [migration guide](/selfhosted/migration/enable-reverse-proxy#exposing-l4-ports) for instructions. ### How services share ports @@ -387,17 +389,19 @@ The proxy's main port always runs an SNI router that peeks at the TLS ClientHell - Each incoming connection is matched against configured domains. - If the SNI hostname matches an **HTTP** service, the connection is forwarded to the HTTP reverse proxy for L7 handling. - If the SNI hostname matches a **TLS** service, the encrypted connection is passed through directly to the backend without TLS termination. -- If the SNI hostname does not match any service, the connection falls through to the HTTP reverse proxy, which returns an error since no matching service exists. -- Connections with no SNI (e.g., TLS 1.3 with Encrypted Client Hello) follow the same fallback path. +- If the SNI hostname does not match any service, the connection goes to the TCP service on that port if there is one, and otherwise falls through to the HTTP reverse proxy, which returns an error since no matching service exists. +- Connections with no SNI, and connections that are not TLS at all, follow the same fallback path. - Do not configure an HTTP and a TLS service with the same hostname. The HTTP route takes priority and the TLS service becomes unreachable. Use different domain names for each. + A domain can belong to only one service, so an HTTP and a TLS service cannot share a hostname: creating the second one fails with `domain already taken`. Use a different domain for each. -**On custom ports**, TLS and TCP services can coexist: +**On custom ports**, any number of TLS services and at most one TCP service can share the same port: - TLS connections are matched by SNI and passed through to the backend. - Connections that do not match any SNI route (or are not TLS at all) fall back to the TCP relay. - This is useful for running a TLS passthrough alongside a plain TCP catch-all on the same port. +- If the port has no TCP service, connections that match no TLS service are dropped. +- HTTP services are served only on the main port. On a custom port, a request for an HTTP service's hostname matches no route, so it goes to the TCP service if there is one and is dropped otherwise. ## Path-based routing