Subnet docker conflict check; enterprice domain alias.

This commit is contained in:
Brandon Hopkins
2026-08-05 15:38:25 -07:00
parent 40ac64bb4e
commit 2f18a1bd7f
2 changed files with 217 additions and 6 deletions

View File

@@ -108,6 +108,14 @@ check_nb_domain() {
echo "The NETBIRD_DOMAIN cannot be netbird.example.com" > /dev/stderr
return 1
fi
# Letters, digits, dots, and hyphens only; the domain is embedded in
# generated YAML and env files
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
return 1
fi
return 0
}
@@ -337,6 +345,103 @@ wait_management_direct() {
return 0
}
############################################
# Docker Network Subnet Override and Conflict Check
############################################
ip_to_int() {
local a b c d
IFS=. read -r a b c d <<< "$1"
echo $(( (10#$a << 24) + (10#$b << 16) + (10#$c << 8) + 10#$d ))
}
# cidrs_overlap <cidr> <cidr> — succeeds if the networks overlap
cidrs_overlap() {
local net1="${1%/*}" len1="${1#*/}" net2="${2%/*}" len2="${2#*/}"
local min_len=$(( len1 < len2 ? len1 : len2 ))
local mask=0
if [[ "$min_len" -gt 0 ]]; then
mask=$(( (0xFFFFFFFF << (32 - min_len)) & 0xFFFFFFFF ))
fi
[[ $(( $(ip_to_int "$net1") & mask )) -eq $(( $(ip_to_int "$net2") & mask )) ]]
}
# valid_ipv4_slash24 <cidr> — accepts a unicast IPv4 /24 like 10.123.45.0/24
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+
case "$1" in
0.*|127.*|169.254.*|22[4-9].*|2[34][0-9].*|25[0-5].*) return 1 ;;
esac
return 0
}
# Apply NETBIRD_DOCKER_SUBNET and derive the gateway (.1) and Traefik IP (.10)
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
exit 1
fi
DOCKER_SUBNET="$NETBIRD_DOCKER_SUBNET"
fi
local base="${DOCKER_SUBNET%.0/24}"
DOCKER_GATEWAY="${base}.1"
TRAEFIK_IP="${base}.10"
return 0
}
# check_docker_subnet_conflicts <compose network name>
# Fail early if an existing Docker network overlaps DOCKER_SUBNET, instead
# of letting "docker compose up" fail later. Host routes are not checked;
# NETBIRD_DOCKER_SUBNET covers those cases.
check_docker_subnet_conflicts() {
local expected_network="$1"
command -v docker &> /dev/null || return 0
local name subnets subnet
while IFS='|' read -r name subnets; do
for subnet in $subnets; do
[[ "$subnet" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]] || continue
if [[ "$name" == "$expected_network" ]]; then
# Our own leftover network: compose reuses it as-is, so its subnet
# must match the one we render
if [[ "$subnet" != "$DOCKER_SUBNET" ]]; then
echo "ERROR: the Docker network '$name', left over from a previous NetBird install, uses $subnet instead of $DOCKER_SUBNET." > /dev/stderr
echo "docker compose would reuse it as-is, and the generated configuration would not match it." > /dev/stderr
echo "Remove it and run this script again:" > /dev/stderr
echo " docker network rm $name" > /dev/stderr
exit 1
fi
elif cidrs_overlap "$DOCKER_SUBNET" "$subnet"; then
echo "ERROR: the existing Docker network '$name' ($subnet) overlaps $DOCKER_SUBNET, the subnet NetBird would use." > /dev/stderr
echo "That network is not managed by this script and is left untouched." > /dev/stderr
echo "Pick a free /24 for NetBird instead and run this script again:" > /dev/stderr
echo " NETBIRD_DOCKER_SUBNET=10.123.45.0/24 ./getting-started.sh" > /dev/stderr
exit 1
fi
done
done < <(docker network inspect --format '{{.Name}}|{{range .IPAM.Config}}{{.Subnet}} {{end}}' $(docker network ls -q 2>/dev/null) 2>/dev/null || true)
return 0
}
configure_docker_subnet() {
# Only the built-in Traefik mode pins a subnet; other modes let Docker pick
if [[ "$REVERSE_PROXY_TYPE" != "0" ]]; then
return 0
fi
# Skip our own network (<project>_netbird) in the conflict check;
# normalization matches compose-go's NormalizeProjectName
local project
project="${COMPOSE_PROJECT_NAME:-$(basename "$PWD")}"
project=$(echo "$project" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]//g; s/^[_-]*//')
check_docker_subnet_conflicts "${project}_netbird"
return 0
}
############################################
# Initialization and Configuration
############################################
@@ -368,7 +473,11 @@ initialize_default_values() {
BIND_LOCALHOST_ONLY="true"
EXTERNAL_PROXY_NETWORK=""
# Traefik static IP within the internal bridge network
# Internal bridge network. Management and proxy trust forwarded headers
# from TRAEFIK_IP only, so all three values derive from the same /24.
# Override with NETBIRD_DOCKER_SUBNET.
DOCKER_SUBNET="172.30.0.0/24"
DOCKER_GATEWAY="172.30.0.1"
TRAEFIK_IP="172.30.0.10"
# NetBird Proxy configuration
@@ -663,8 +772,10 @@ init_environment() {
check_docker_sock_perms
initialize_default_values
apply_docker_subnet_override
configure_domain
configure_reverse_proxy
configure_docker_subnet
check_jq
@@ -686,6 +797,7 @@ 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"
@@ -884,8 +996,8 @@ networks:
driver: bridge
ipam:
config:
- subnet: 172.30.0.0/24
gateway: 172.30.0.1
- subnet: $DOCKER_SUBNET
gateway: $DOCKER_GATEWAY
EOF
return 0
}