[GH-ISSUE #5351] Combined server: server.relays disables built-in relay and STUN — no way to add additional relays #11072

Open
opened 2026-08-05 01:28:23 -04:00 by saavagebueno · 7 comments
Owner

Originally created by @alex-s-interexy on GitHub (Feb 16, 2026).
Original GitHub issue: https://github.com/netbirdio/netbird/issues/5351

Originally assigned to: @jnfrati on GitHub.

Bug / Feature Request

Problem

When using the combined netbird-server container, configuring server.relays in config.yaml to add additional external relay servers disables the built-in local relay AND STUN server entirely. There is no way to have both the built-in relay and additional external relays.

This forces users who need multi-relay support (e.g., geo-distributed relay servers) to run a separate netbirdio/relay container alongside netbird-server, defeating part of the purpose of the combined image.

Root Cause

In combined/cmd/config.go, applyRelayDefaults() returns early when external relays are configured:

func (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {
    if hasExternalRelay {
        return  // <-- Built-in relay AND STUN are never enabled
    }
    
    c.Relay.Enabled = true
    // ... relay setup ...
    
    // STUN setup is also inside this function, so it is skipped too
    if !hasExternalStuns && len(c.Server.StunPorts) > 0 {
        c.Relay.Stun.Enabled = true
        // ...
    }
}

The three modes are mutually exclusive:

  1. No server.relays → built-in relay + STUN enabled (auto-configured)
  2. server.relays set → built-in relay disabled, built-in STUN disabled
  3. server.stuns set → built-in STUN disabled

There is no mode where: built-in relay is enabled AND additional external relay addresses are advertised to clients.

Expected Behavior

Users should be able to configure additional relay servers while keeping the built-in relay running. For example:

server:
  exposedAddress: "https://example.com:443"
  additionalRelays:    # new field — appended to the auto-configured local relay
    - "rels://relay-eu.example.com:443"
    - "rels://relay-us.example.com:443"

Or alternatively, server.relays should have an option to keep the built-in relay enabled:

server:
  relays:
    keepBuiltIn: true  # don't disable the local relay
    addresses:
      - "rels://example.com:443"       # local (auto-configured if keepBuiltIn)
      - "rels://relay-eu.example.com:443"  # additional

Current Workaround

Run a separate netbirdio/relay container alongside netbird-server:

  1. Set server.relays.addresses with all relay URIs (including local)
  2. Set server.stuns with external STUN URI pointing to the relay container
  3. The relay container handles relay WebSocket + STUN
  4. Traefik routes /relay to the relay container instead of netbird-server

This works but adds an extra container and complexity.

Use Case

We run a production deployment with 113+ peers across multiple geographic regions. We need:

  • A primary relay at rels://neb.domain.com:443 (co-located with the management server)
  • A secondary relay at rels://relay-eu.domain.com:443 (closer to peers in xxx GEO)

Both should be advertised to all peers so they can select the lowest-latency one.

  • #4739 — Feature request for relay grouping/priority by region (broader feature, but the inability to add relays without losing the built-in one is the first blocker)
  • #4090 — Relay URI regression with reverse proxy

Environment

  • NetBird server version: v0.65.1 (netbirdio/netbird-server:latest)
  • Deployment: Combined container behind Traefik reverse proxy
Originally created by @alex-s-interexy on GitHub (Feb 16, 2026). Original GitHub issue: https://github.com/netbirdio/netbird/issues/5351 Originally assigned to: @jnfrati on GitHub. ## Bug / Feature Request ### Problem When using the combined `netbird-server` container, configuring `server.relays` in `config.yaml` to add additional external relay servers **disables the built-in local relay AND STUN server entirely**. There is no way to have both the built-in relay and additional external relays. This forces users who need multi-relay support (e.g., geo-distributed relay servers) to run a separate `netbirdio/relay` container alongside `netbird-server`, defeating part of the purpose of the combined image. ### Root Cause In [`combined/cmd/config.go`](https://github.com/netbirdio/netbird/blob/main/combined/cmd/config.go), `applyRelayDefaults()` returns early when external relays are configured: ```go func (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) { if hasExternalRelay { return // <-- Built-in relay AND STUN are never enabled } c.Relay.Enabled = true // ... relay setup ... // STUN setup is also inside this function, so it is skipped too if !hasExternalStuns && len(c.Server.StunPorts) > 0 { c.Relay.Stun.Enabled = true // ... } } ``` The three modes are mutually exclusive: 1. **No `server.relays`** → built-in relay + STUN enabled (auto-configured) ✅ 2. **`server.relays` set** → built-in relay disabled, built-in STUN disabled ❌ 3. **`server.stuns` set** → built-in STUN disabled ❌ There is no mode where: built-in relay is enabled **AND** additional external relay addresses are advertised to clients. ### Expected Behavior Users should be able to configure additional relay servers while keeping the built-in relay running. For example: ```yaml server: exposedAddress: "https://example.com:443" additionalRelays: # new field — appended to the auto-configured local relay - "rels://relay-eu.example.com:443" - "rels://relay-us.example.com:443" ``` Or alternatively, `server.relays` should have an option to keep the built-in relay enabled: ```yaml server: relays: keepBuiltIn: true # don't disable the local relay addresses: - "rels://example.com:443" # local (auto-configured if keepBuiltIn) - "rels://relay-eu.example.com:443" # additional ``` ### Current Workaround Run a separate `netbirdio/relay` container alongside `netbird-server`: 1. Set `server.relays.addresses` with all relay URIs (including local) 2. Set `server.stuns` with external STUN URI pointing to the relay container 3. The relay container handles relay WebSocket + STUN 4. Traefik routes `/relay` to the relay container instead of netbird-server This works but adds an extra container and complexity. ### Use Case We run a production deployment with 113+ peers across multiple geographic regions. We need: - A primary relay at `rels://neb.domain.com:443` (co-located with the management server) - A secondary relay at `rels://relay-eu.domain.com:443` (closer to peers in xxx GEO) Both should be advertised to all peers so they can select the lowest-latency one. ### Related Issues - #4739 — Feature request for relay grouping/priority by region (broader feature, but the inability to add relays without losing the built-in one is the first blocker) - #4090 — Relay URI regression with reverse proxy ### Environment - NetBird server version: v0.65.1 (`netbirdio/netbird-server:latest`) - Deployment: Combined container behind Traefik reverse proxy
saavagebueno added the feature-request label 2026-08-05 01:28:23 -04:00
Author
Owner

@braginini commented on GitHub (Feb 17, 2026):

This is intended. If you split the services, then embedded relay won't run. Usually, you have a cluster of relays managed separately from the management server.

<!-- gh-comment-id:3913290091 --> @braginini commented on GitHub (Feb 17, 2026): This is intended. If you split the services, then embedded relay won't run. Usually, you have a cluster of relays managed separately from the management server.
Author
Owner

@alex-s-interexy commented on GitHub (Feb 19, 2026):

Any decision regarding this feature?

<!-- gh-comment-id:3931104873 --> @alex-s-interexy commented on GitHub (Feb 19, 2026): Any decision regarding this feature?
Author
Owner

@JCBird1012 commented on GitHub (Mar 16, 2026):

I'm running into a similar issue around stuns vs stunPorts. There are deployment scenarios where the public hostname for NetBird's HTTP/gRPC services is different from the hostname that can actually receive STUN traffic. In my case, NetBird runs on AWS ECS Fargate, with an ALB handling HTTP/gRPC traffic and a separate public NLB handling STUN/UDP traffic.

Using stuns lets us advertise the STUN hostname, but it disables the embedded STUN server. Using stunPorts keeps the embedded STUN server enabled, but it advertises the same public hostname as the main server. What would be nice is a way to keep embedded STUN enabled while advertising a different STUN hostname than the main exposedAddress.

A workaround is to place a public NLB in front of everything, send 443 from the NLB to the ALB, and send 3478 from the NLB directly to NetBird. That allows one hostname to serve both HTTPS and STUN. But that's a topology workaround; it would still be useful if NetBird supported advertising a separate STUN hostname while keeping embedded STUN enabled.

<!-- gh-comment-id:4071200948 --> @JCBird1012 commented on GitHub (Mar 16, 2026): I'm running into a similar issue around `stuns` vs `stunPorts`. There are deployment scenarios where the public hostname for NetBird's HTTP/gRPC services is different from the hostname that can actually receive STUN traffic. In my case, NetBird runs on AWS ECS Fargate, with an ALB handling HTTP/gRPC traffic and a separate public NLB handling STUN/UDP traffic. Using `stuns` lets us advertise the STUN hostname, but it disables the embedded STUN server. Using `stunPorts` keeps the embedded STUN server enabled, but it advertises the same public hostname as the main server. What would be nice is a way to keep embedded STUN enabled while advertising a different STUN hostname than the main `exposedAddress`. A workaround is to place a public NLB in front of everything, send `443` from the NLB to the ALB, and send `3478` from the NLB directly to NetBird. That allows one hostname to serve both HTTPS and STUN. But that's a topology workaround; it would still be useful if NetBird supported advertising a separate STUN hostname while keeping embedded STUN enabled.
Author
Owner

@rehcnt commented on GitHub (Apr 8, 2026):

Geography of my peers is widely spread, for thousands of miles. So I need relay on main server as much as others since it is placed in own region. It is make sense to keep embedded relay on.

<!-- gh-comment-id:4208685619 --> @rehcnt commented on GitHub (Apr 8, 2026): Geography of my peers is widely spread, for thousands of miles. So I need relay on main server as much as others since it is placed in own region. It is make sense to keep embedded relay on.
Author
Owner

@andyrue commented on GitHub (May 7, 2026):

Is there a workaround for this besides having a load balancer in front of both services? My dashboard and server run on separate IPs.

<!-- gh-comment-id:4400676784 --> @andyrue commented on GitHub (May 7, 2026): Is there a workaround for this besides having a load balancer in front of both services? My dashboard and server run on separate IPs.
Author
Owner

@braginini commented on GitHub (May 8, 2026):

Given the interest in the feature, I think that it is worth for us to consider checking this out @TechHutTV @jnfrati

<!-- gh-comment-id:4405485894 --> @braginini commented on GitHub (May 8, 2026): Given the interest in the feature, I think that it is worth for us to consider checking this out @TechHutTV @jnfrati
Author
Owner

@lrnd1 commented on GitHub (Jul 7, 2026):

Even a simple env or parameter to stop auto disabling the built in services when using stuns:/relays:/signalUri: options in config.yaml would be appreciated!

I'm routing traffic to Netbird from two different subdomains. If I try to specify the subdomain using the mentioned options, the services are auto disabled.

<!-- gh-comment-id:4904301805 --> @lrnd1 commented on GitHub (Jul 7, 2026): Even a simple env or parameter to stop auto disabling the built in services when using stuns:/relays:/signalUri: options in config.yaml would be appreciated! I'm routing traffic to Netbird from two different subdomains. If I try to specify the subdomain using the mentioned options, the services are auto disabled.
Sign in to join this conversation.
No Label feature-request
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DYNR/netbird#11072