Fix NPM forwarding: use HOST_IP env var instead of socket detection

Socket detection inside Docker returns the container IP (172.18.0.x),
not the host IP. Now:
- install.sh detects host IP via hostname -I and stores in .env
- docker-compose.yml passes HOST_IP to the container
- npm_service.py reads HOST_IP from environment
- Increased SSL cert timeout to 120s (LE validation is slow)
- Added better logging for SSL cert creation/assignment
- README updated with HOST_IP in .env example

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 21:00:29 +01:00
parent b56f0eb8a4
commit 6d42e583d6
4 changed files with 38 additions and 22 deletions

View File

@@ -257,6 +257,7 @@ DATA_DIR=/opt/netbird-instances
DOCKER_NETWORK=npm-network DOCKER_NETWORK=npm-network
LOG_LEVEL=INFO LOG_LEVEL=INFO
WEB_UI_PORT=8000 WEB_UI_PORT=8000
HOST_IP=<your-server-ip>
``` ```
> **All application settings** (domain, NPM credentials, Docker images, branding, etc.) are stored in the SQLite database and editable via the Web UI under **Settings**. > **All application settings** (domain, NPM credentials, Docker images, branding, etc.) are stored in the SQLite database and editable via the Web UI under **Settings**.

View File

@@ -13,7 +13,7 @@ Also manages NPM streams for STUN/TURN relay UDP ports.
""" """
import logging import logging
import socket import os
from typing import Any from typing import Any
import httpx import httpx
@@ -25,27 +25,24 @@ NPM_TIMEOUT = 30
def _get_forward_host() -> str: def _get_forward_host() -> str:
"""Detect the host machine's real IP address. """Get the host machine's real IP address for NPM forwarding.
NPM proxy hosts must forward to the actual host IP where Docker NPM proxy hosts must forward to the actual host IP where Docker
port mappings are exposed — NOT a container name or Docker gateway. port mappings are exposed — NOT a container name or Docker gateway.
Uses a UDP socket to determine the primary outbound IP address Reads the HOST_IP environment variable set during installation
of the host (works inside Docker containers). (detected via ``hostname -I`` on the host and stored in .env).
Returns: Returns:
The host's primary IP address (e.g. ``192.168.26.191``). The host's IP address (e.g. ``192.168.26.191``).
""" """
try: host_ip = os.environ.get("HOST_IP", "").strip()
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) if host_ip:
s.connect(("8.8.8.8", 80)) logger.info("Using HOST_IP from environment: %s", host_ip)
host_ip = s.getsockname()[0]
s.close()
logger.info("Detected host IP: %s", host_ip)
return host_ip return host_ip
except Exception:
logger.warning("Could not detect host IP, falling back to 172.17.0.1") logger.warning("HOST_IP not set in environment — please add HOST_IP=<your-server-ip> to .env")
return "172.17.0.1" return "127.0.0.1"
async def _npm_login(client: httpx.AsyncClient, api_url: str, email: str, password: str) -> str: async def _npm_login(client: httpx.AsyncClient, api_url: str, email: str, password: str) -> str:
@@ -164,7 +161,7 @@ async def create_proxy_host(
} }
try: try:
async with httpx.AsyncClient(timeout=NPM_TIMEOUT) as client: async with httpx.AsyncClient(timeout=180) as client: # Long timeout for LE cert
# Step 1: Login to NPM # Step 1: Login to NPM
token = await _npm_login(client, api_url, npm_email, npm_password) token = await _npm_login(client, api_url, npm_email, npm_password)
headers = { headers = {
@@ -208,6 +205,9 @@ async def _request_ssl(
) -> None: ) -> None:
"""Request a Let's Encrypt SSL certificate for a proxy host. """Request a Let's Encrypt SSL certificate for a proxy host.
Let's Encrypt validation can take up to 120 seconds, so we use
a longer timeout for certificate requests.
Args: Args:
client: httpx client (already authenticated). client: httpx client (already authenticated).
api_url: NPM API base URL. api_url: NPM API base URL.
@@ -227,20 +227,28 @@ async def _request_ssl(
}, },
} }
try: try:
logger.info("Requesting Let's Encrypt certificate for %s ...", domain)
resp = await client.post( resp = await client.post(
f"{api_url}/nginx/certificates", json=ssl_payload, headers=headers f"{api_url}/nginx/certificates",
json=ssl_payload,
headers=headers,
timeout=120, # LE validation can be slow
) )
if resp.status_code in (200, 201): if resp.status_code in (200, 201):
cert_id = resp.json().get("id") cert_id = resp.json().get("id")
# Assign certificate to proxy host logger.info("Certificate created (id=%s), assigning to proxy host %s", cert_id, proxy_id)
await client.put( assign_resp = await client.put(
f"{api_url}/nginx/proxy-hosts/{proxy_id}", f"{api_url}/nginx/proxy-hosts/{proxy_id}",
json={"certificate_id": cert_id}, json={"certificate_id": cert_id},
headers=headers, headers=headers,
) )
logger.info("SSL certificate %s assigned to proxy host %s", cert_id, proxy_id) if assign_resp.status_code in (200, 201):
logger.info("SSL certificate %s assigned to proxy host %s", cert_id, proxy_id)
else:
logger.warning("Failed to assign cert to proxy host: %s %s",
assign_resp.status_code, assign_resp.text[:200])
else: else:
logger.warning("SSL request returned %s: %s", resp.status_code, resp.text[:200]) logger.warning("SSL cert request returned %s: %s", resp.status_code, resp.text[:500])
except Exception as exc: except Exception as exc:
logger.warning("SSL certificate request failed: %s", exc) logger.warning("SSL certificate request failed: %s", exc)

View File

@@ -17,6 +17,7 @@ services:
- LOG_LEVEL=${LOG_LEVEL:-INFO} - LOG_LEVEL=${LOG_LEVEL:-INFO}
- DATA_DIR=${DATA_DIR:-/opt/netbird-instances} - DATA_DIR=${DATA_DIR:-/opt/netbird-instances}
- DOCKER_NETWORK=${DOCKER_NETWORK:-npm-network} - DOCKER_NETWORK=${DOCKER_NETWORK:-npm-network}
- HOST_IP=${HOST_IP:-}
networks: networks:
- npm-network - npm-network
healthcheck: healthcheck:

View File

@@ -338,6 +338,10 @@ echo "Generating encryption keys..."
SECRET_KEY=$(openssl rand -base64 32) SECRET_KEY=$(openssl rand -base64 32)
echo -e "${GREEN}✓ Encryption keys generated${NC}" echo -e "${GREEN}✓ Encryption keys generated${NC}"
# Detect host IP for NPM forwarding
HOST_IP=$(hostname -I | awk '{print $1}')
echo -e "Host IP: ${CYAN}${HOST_IP}${NC}"
# Create MINIMAL .env — only container-level vars needed by docker-compose.yml # Create MINIMAL .env — only container-level vars needed by docker-compose.yml
# All application config goes into the DATABASE, not here! # All application config goes into the DATABASE, not here!
echo "Creating minimal container environment..." echo "Creating minimal container environment..."
@@ -350,6 +354,7 @@ DATA_DIR=$DATA_DIR
DOCKER_NETWORK=$DOCKER_NETWORK DOCKER_NETWORK=$DOCKER_NETWORK
LOG_LEVEL=INFO LOG_LEVEL=INFO
WEB_UI_PORT=8000 WEB_UI_PORT=8000
HOST_IP=$HOST_IP
ENVEOF ENVEOF
chmod 600 "$INSTALL_DIR/.env" chmod 600 "$INSTALL_DIR/.env"
@@ -482,8 +487,9 @@ if [ -n "$MSP_DOMAIN" ]; then
if [ -n "$PROXY_ID" ] && [ "$PROXY_ID" != "None" ] && [ "$PROXY_ID" != "" ]; then if [ -n "$PROXY_ID" ] && [ "$PROXY_ID" != "None" ] && [ "$PROXY_ID" != "" ]; then
echo -e "${GREEN}✓ NPM proxy host created (ID: ${PROXY_ID})${NC}" echo -e "${GREEN}✓ NPM proxy host created (ID: ${PROXY_ID})${NC}"
# Step 3: Request Let's Encrypt certificate # Step 3: Request Let's Encrypt certificate (can take up to 120s)
CERT_RESULT=$(curl -s -X POST "${NPM_API_URL}/nginx/certificates" \ echo -e "${CYAN}Requesting Let's Encrypt certificate (this may take a minute)...${NC}"
CERT_RESULT=$(curl -s --max-time 120 -X POST "${NPM_API_URL}/nginx/certificates" \
-H "Authorization: Bearer ${NPM_TOKEN}" \ -H "Authorization: Bearer ${NPM_TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{ -d "{