From 96e9598fccbc64014f70e74127d2abd2762b7e8a Mon Sep 17 00:00:00 2001 From: Brandon Hopkins Date: Wed, 12 Aug 2026 10:34:11 -0700 Subject: [PATCH] Fix compose project-normalization --- .../getting-started-enterprise.sh | 8 +++-- infrastructure_files/getting-started.sh | 35 +++++++++++++------ infrastructure_files/migrate.sh | 35 +++++++++++++------ 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/infrastructure_files/getting-started-enterprise.sh b/infrastructure_files/getting-started-enterprise.sh index 6ebbb4675..ceca98bef 100755 --- a/infrastructure_files/getting-started-enterprise.sh +++ b/infrastructure_files/getting-started-enterprise.sh @@ -74,9 +74,13 @@ valid_ipv4_slash24() { local octet='(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])' local re="^${octet}\.${octet}\.${octet}\.0/24$" [[ "$1" =~ $re ]] || return 1 - # Reject non-unicast/reserved ranges: 0/8, loopback, link-local, 224+ + # Reject non-unicast/reserved ranges: 0/8, loopback, link-local, 224+. + # 100.64/10 is rejected too: NetBird allocates overlay peer addresses from + # it by default, and a bridge there shadows the overlay without any Docker + # network overlapping, so the conflict check below would not catch it. case "$1" in 0.*|127.*|169.254.*|22[4-9].*|2[34][0-9].*|25[0-5].*) return 1 ;; + 100.6[4-9].*|100.[7-9][0-9].*|100.1[01][0-9].*|100.12[0-7].*) return 1 ;; esac return 0 } @@ -85,7 +89,7 @@ valid_ipv4_slash24() { apply_docker_subnet_override() { if [[ -n "${NETBIRD_DOCKER_SUBNET:-}" ]]; then if ! valid_ipv4_slash24 "$NETBIRD_DOCKER_SUBNET"; then - echo "NETBIRD_DOCKER_SUBNET must be a unicast IPv4 /24 network like 10.123.45.0/24 (0/8, 127/8, 169.254/16, and 224+ are not allowed), got: $NETBIRD_DOCKER_SUBNET" > /dev/stderr + echo "NETBIRD_DOCKER_SUBNET must be a unicast IPv4 /24 network like 10.123.45.0/24 (0/8, 127/8, 169.254/16, 100.64/10, and 224+ are not allowed), got: $NETBIRD_DOCKER_SUBNET" > /dev/stderr exit 1 fi DOCKER_SUBNET="$NETBIRD_DOCKER_SUBNET" diff --git a/infrastructure_files/getting-started.sh b/infrastructure_files/getting-started.sh index 7c494e44d..af58346bd 100755 --- a/infrastructure_files/getting-started.sh +++ b/infrastructure_files/getting-started.sh @@ -110,10 +110,16 @@ check_nb_domain() { fi # Letters, digits, dots, and hyphens only; the domain is embedded in - # generated YAML and env files + # generated YAML and env files. This is not FQDN validation: "use-ip" and + # bare IP addresses are valid inputs here and both satisfy the pattern. local re='^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$' - if [[ "$DOMAIN" != "use-ip" ]] && [[ ! "$DOMAIN" =~ $re ]]; then - echo "The NETBIRD_DOMAIN may only contain letters, digits, dots, and hyphens." > /dev/stderr + if [[ ! "$DOMAIN" =~ $re ]] || [[ "$DOMAIN" == *..* ]]; then + echo "The NETBIRD_DOMAIN may only contain letters, digits, dots, and hyphens, and cannot begin or end with a dot or hyphen." > /dev/stderr + return 1 + fi + + if [[ "${#DOMAIN}" -gt 253 ]]; then + echo "The NETBIRD_DOMAIN cannot be longer than 253 characters." > /dev/stderr return 1 fi return 0 @@ -371,9 +377,13 @@ valid_ipv4_slash24() { local octet='(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])' local re="^${octet}\.${octet}\.${octet}\.0/24$" [[ "$1" =~ $re ]] || return 1 - # Reject non-unicast/reserved ranges: 0/8, loopback, link-local, 224+ + # Reject non-unicast/reserved ranges: 0/8, loopback, link-local, 224+. + # 100.64/10 is rejected too: NetBird allocates overlay peer addresses from + # it by default, and a bridge there shadows the overlay without any Docker + # network overlapping, so the conflict check below would not catch it. case "$1" in 0.*|127.*|169.254.*|22[4-9].*|2[34][0-9].*|25[0-5].*) return 1 ;; + 100.6[4-9].*|100.[7-9][0-9].*|100.1[01][0-9].*|100.12[0-7].*) return 1 ;; esac return 0 } @@ -382,7 +392,7 @@ valid_ipv4_slash24() { apply_docker_subnet_override() { if [[ -n "${NETBIRD_DOCKER_SUBNET:-}" ]]; then if ! valid_ipv4_slash24 "$NETBIRD_DOCKER_SUBNET"; then - echo "NETBIRD_DOCKER_SUBNET must be a unicast IPv4 /24 network like 10.123.45.0/24 (0/8, 127/8, 169.254/16, and 224+ are not allowed), got: $NETBIRD_DOCKER_SUBNET" > /dev/stderr + echo "NETBIRD_DOCKER_SUBNET must be a unicast IPv4 /24 network like 10.123.45.0/24 (0/8, 127/8, 169.254/16, 100.64/10, and 224+ are not allowed), got: $NETBIRD_DOCKER_SUBNET" > /dev/stderr exit 1 fi DOCKER_SUBNET="$NETBIRD_DOCKER_SUBNET" @@ -466,11 +476,17 @@ configure_docker_subnet() { return 0 fi - # Skip our own network (_netbird) in the conflict check; - # normalization matches compose-go's NormalizeProjectName + # Skip our own network (_netbird) in the conflict check. Resolve the + # directory the way compose does (physical path, so a symlinked install dir + # still yields the real directory name) and normalize the basename the way + # compose-go NormalizeProjectName does: lowercase, drop leading and trailing + # invalid characters, collapse each inner run of invalid characters to a + # single "-", then trim leading "_" and "-". Deleting invalid characters + # instead would be compose v1 behaviour and would miss our own network. local project - project="${COMPOSE_PROJECT_NAME:-$(basename "$PWD")}" - project=$(echo "$project" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]//g; s/^[_-]*//') + project="${COMPOSE_PROJECT_NAME:-$(basename "$(pwd -P)")}" + project=$(echo "$project" | tr '[:upper:]' '[:lower:]' \ + | sed 's/^[^a-z0-9_-]*//; s/[^a-z0-9_-]*$//; s/[^a-z0-9_-][^a-z0-9_-]*/-/g; s/^[_-]*//') check_docker_subnet_conflicts "${project}_netbird" return 0 } @@ -845,7 +861,6 @@ render_docker_compose_traefik_builtin() { local crowdsec_volumes="" local traefik_file_provider="" local traefik_dynamic_volume="" - if [[ "$ENABLE_PROXY" == "true" ]]; then traefik_file_provider=' - "--providers.file.filename=/etc/traefik/dynamic.yaml"' traefik_dynamic_volume=" - ./traefik-dynamic.yaml:/etc/traefik/dynamic.yaml:ro" diff --git a/infrastructure_files/migrate.sh b/infrastructure_files/migrate.sh index 62f7591f2..9d9c4f2af 100755 --- a/infrastructure_files/migrate.sh +++ b/infrastructure_files/migrate.sh @@ -154,19 +154,30 @@ valid_ipv4_slash24() { local octet='(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])' local re="^${octet}\.${octet}\.${octet}\.0/24$" [[ "$1" =~ $re ]] || return 1 - # Reject non-unicast/reserved ranges: 0/8, loopback, link-local, 224+ + # Reject non-unicast/reserved ranges: 0/8, loopback, link-local, 224+. + # 100.64/10 is rejected too: NetBird allocates overlay peer addresses from + # it by default, and a bridge there shadows the overlay without any Docker + # network overlapping, so the conflict check below would not catch it. case "$1" in 0.*|127.*|169.254.*|22[4-9].*|2[34][0-9].*|25[0-5].*) return 1 ;; + 100.6[4-9].*|100.[7-9][0-9].*|100.1[01][0-9].*|100.12[0-7].*) return 1 ;; esac return 0 } # Apply NETBIRD_DOCKER_SUBNET and derive the gateway (.1) and Traefik IP (.10). # Runs during preflight so a bad value fails before anything is touched. +# +# Unlike the getting-started scripts, the subnet has a single consumer here: the +# generated docker-compose.yml. The reverseProxy trust pins in the generated +# config.yaml are carried over verbatim from the old management.json (see +# extract_config_values), so TRAEFIK_IP is deliberately not wired into them. If +# an old config already pinned an address inside the default 172.30.0.0/24, +# overriding the subnet will not update that pin. apply_docker_subnet_override() { if [[ -n "${NETBIRD_DOCKER_SUBNET:-}" ]]; then if ! valid_ipv4_slash24 "$NETBIRD_DOCKER_SUBNET"; then - log_error "NETBIRD_DOCKER_SUBNET must be a unicast IPv4 /24 network like 10.123.45.0/24 (0/8, 127/8, 169.254/16, and 224+ are not allowed), got: $NETBIRD_DOCKER_SUBNET" + log_error "NETBIRD_DOCKER_SUBNET must be a unicast IPv4 /24 network like 10.123.45.0/24 (0/8, 127/8, 169.254/16, 100.64/10, and 224+ are not allowed), got: $NETBIRD_DOCKER_SUBNET" exit 1 fi DOCKER_SUBNET="$NETBIRD_DOCKER_SUBNET" @@ -217,6 +228,8 @@ check_docker_subnet_conflicts() { log_error "Could not inspect the existing Docker networks (docker network inspect exited $inspect_status)." echo "Without it this script cannot verify that $DOCKER_SUBNET is free." echo "If a Docker network was removed while this script was running, run the script again." + echo "The old deployment is stopped at this point; restart it with:" + echo " bash $BACKUP_DIR/rollback.sh" exit 1 fi @@ -248,20 +261,22 @@ check_docker_subnet_conflicts() { return 0 } +# Only reached on the automatic (embedded Caddy) path, which is the only one +# that generates a compose file pinning a subnet; the exposed-ports compose for +# custom proxies lets Docker pick. configure_docker_subnet() { - # Only the generated Traefik compose pins a subnet; the exposed-ports compose - # for custom proxies lets Docker pick - if [[ "$PROXY_TYPE" != "$PROXY_TYPE_CADDY" ]]; then - return 0 - fi - # Skip our own network (_netbird) in the conflict check. The new # compose runs from INSTALL_DIR, so resolve it the way compose does (a # relative --install-dir still yields the real directory name) and normalize - # the basename the way compose-go NormalizeProjectName does. + # the basename the way compose-go NormalizeProjectName does: lowercase, drop + # leading and trailing invalid characters, collapse each inner run of invalid + # characters to a single "-", then trim leading "_" and "-". Deleting invalid + # characters instead would be compose v1 behaviour and would miss our own + # network. local project project="${COMPOSE_PROJECT_NAME:-$(basename "$(cd -- "$INSTALL_DIR" && pwd -P)")}" - project=$(echo "$project" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]//g; s/^[_-]*//') + project=$(echo "$project" | tr '[:upper:]' '[:lower:]' \ + | sed 's/^[^a-z0-9_-]*//; s/[^a-z0-9_-]*$//; s/[^a-z0-9_-][^a-z0-9_-]*/-/g; s/^[_-]*//') check_docker_subnet_conflicts "${project}_netbird" return 0 }