feat: adding traefik + nb reverse proxy

This commit is contained in:
Diego Noguês
2026-02-12 18:43:35 +01:00
parent 0bd227196e
commit a1b048f2ad
+452 -7
View File
@@ -91,16 +91,17 @@ read_reverse_proxy_type() {
echo " [3] Nginx Proxy Manager (generates config + instructions)" > /dev/stderr echo " [3] Nginx Proxy Manager (generates config + instructions)" > /dev/stderr
echo " [4] External Caddy (generates Caddyfile snippet)" > /dev/stderr echo " [4] External Caddy (generates Caddyfile snippet)" > /dev/stderr
echo " [5] Other/Manual (displays setup documentation)" > /dev/stderr echo " [5] Other/Manual (displays setup documentation)" > /dev/stderr
echo " [6] Traefik TCP Proxy (single port 443 + STUN)" > /dev/stderr
echo "" > /dev/stderr echo "" > /dev/stderr
echo -n "Enter choice [0-5] (default: 0): " > /dev/stderr echo -n "Enter choice [0-6] (default: 0): " > /dev/stderr
read -r CHOICE < /dev/tty read -r CHOICE < /dev/tty
if [[ -z "$CHOICE" ]]; then if [[ -z "$CHOICE" ]]; then
CHOICE="0" CHOICE="0"
fi fi
if [[ ! "$CHOICE" =~ ^[0-5]$ ]]; then if [[ ! "$CHOICE" =~ ^[0-6]$ ]]; then
echo "Invalid choice. Please enter a number between 0 and 5." > /dev/stderr echo "Invalid choice. Please enter a number between 0 and 6." > /dev/stderr
read_reverse_proxy_type read_reverse_proxy_type
return return
fi fi
@@ -140,6 +141,35 @@ read_traefik_certresolver() {
return 0 return 0
} }
read_traefik_tcp_acme_email() {
echo "" > /dev/stderr
echo "Enter your email for Let's Encrypt certificate notifications." > /dev/stderr
echo -n "Email address: " > /dev/stderr
read -r EMAIL < /dev/tty
if [[ -z "$EMAIL" ]]; then
echo "Email is required for Let's Encrypt." > /dev/stderr
read_traefik_tcp_acme_email
return
fi
echo "$EMAIL"
return 0
}
read_enable_proxy() {
echo "" > /dev/stderr
echo "Do you want to enable the NetBird Proxy service?" > /dev/stderr
echo "The proxy exposes internal NetBird network resources to the internet." > /dev/stderr
echo -n "Enable proxy? [y/N]: " > /dev/stderr
read -r CHOICE < /dev/tty
if [[ "$CHOICE" =~ ^[Yy]$ ]]; then
echo "true"
else
echo "false"
fi
return 0
}
read_port_binding_preference() { read_port_binding_preference() {
echo "" > /dev/stderr echo "" > /dev/stderr
echo "Should container ports be bound to localhost only (127.0.0.1)?" > /dev/stderr echo "Should container ports be bound to localhost only (127.0.0.1)?" > /dev/stderr
@@ -206,6 +236,30 @@ wait_management() {
return 0 return 0
} }
wait_management_traefik() {
set +e
echo -n "Waiting for Management server to become ready"
counter=1
while true; do
# Check the embedded IdP endpoint through Traefik
if curl -sk -f -o /dev/null "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/oauth2/.well-known/openid-configuration" 2>/dev/null; then
break
fi
if [[ $counter -eq 60 ]]; then
echo ""
echo "Taking too long. Checking logs..."
$DOCKER_COMPOSE_COMMAND logs --tail=20 traefik
$DOCKER_COMPOSE_COMMAND logs --tail=20 management
fi
echo -n " ."
sleep 2
counter=$((counter + 1))
done
echo " done"
set -e
return 0
}
wait_management_direct() { wait_management_direct() {
set +e set +e
local upstream_host=$(get_upstream_host) local upstream_host=$(get_upstream_host)
@@ -246,10 +300,12 @@ initialize_default_values() {
# Docker images # Docker images
CADDY_IMAGE="caddy" CADDY_IMAGE="caddy"
DASHBOARD_IMAGE="netbirdio/dashboard:latest" #DASHBOARD_IMAGE="netbirdio/dashboard:latest"
DASHBOARD_IMAGE="netbirdio/dashboard:pr-552"
SIGNAL_IMAGE="netbirdio/signal:latest" SIGNAL_IMAGE="netbirdio/signal:latest"
RELAY_IMAGE="netbirdio/relay:latest" RELAY_IMAGE="netbirdio/relay:latest"
MANAGEMENT_IMAGE="netbirdio/management:latest" MANAGEMENT_IMAGE="netbirdio/management:latest"
PROXY_IMAGE=""
# Reverse proxy configuration # Reverse proxy configuration
REVERSE_PROXY_TYPE="0" REVERSE_PROXY_TYPE="0"
@@ -263,6 +319,14 @@ initialize_default_values() {
RELAY_HOST_PORT="8084" RELAY_HOST_PORT="8084"
BIND_LOCALHOST_ONLY="true" BIND_LOCALHOST_ONLY="true"
EXTERNAL_PROXY_NETWORK="" EXTERNAL_PROXY_NETWORK=""
# Traefik TCP proxy configuration
TRAEFIK_IMAGE="traefik:v3.6"
TRAEFIK_TCP_ACME_EMAIL=""
# NetBird Proxy configuration
ENABLE_PROXY="false"
PROXY_TOKEN=""
return 0 return 0
} }
@@ -293,8 +357,17 @@ configure_reverse_proxy() {
TRAEFIK_CERTRESOLVER=$(read_traefik_certresolver) TRAEFIK_CERTRESOLVER=$(read_traefik_certresolver)
fi fi
# Handle Traefik TCP proxy prompts
if [[ "$REVERSE_PROXY_TYPE" == "6" ]]; then
TRAEFIK_TCP_ACME_EMAIL=$(read_traefik_tcp_acme_email)
# Prompt for NetBird Proxy configuration
ENABLE_PROXY=$(read_enable_proxy)
# Note: PROXY_TOKEN will be auto-generated after Management starts
fi
# Handle port binding for external proxy options (2-5) # Handle port binding for external proxy options (2-5)
if [[ "$REVERSE_PROXY_TYPE" -ge 2 ]]; then if [[ "$REVERSE_PROXY_TYPE" -ge 2 && "$REVERSE_PROXY_TYPE" -le 5 ]]; then
BIND_LOCALHOST_ONLY=$(read_port_binding_preference) BIND_LOCALHOST_ONLY=$(read_port_binding_preference)
fi fi
@@ -313,7 +386,7 @@ check_existing_installation() {
echo "Generated files already exist, if you want to reinitialize the environment, please remove them first." echo "Generated files already exist, if you want to reinitialize the environment, please remove them first."
echo "You can use the following commands:" echo "You can use the following commands:"
echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes" echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes"
echo " rm -f docker-compose.yml Caddyfile dashboard.env management.json relay.env nginx-netbird.conf caddyfile-netbird.txt npm-advanced-config.txt" echo " rm -f docker-compose.yml Caddyfile dashboard.env management.json relay.env nginx-netbird.conf caddyfile-netbird.txt npm-advanced-config.txt proxy.env"
echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard." echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard."
exit 1 exit 1
fi fi
@@ -347,6 +420,15 @@ generate_configuration_files() {
5) 5)
render_docker_compose_exposed_ports > docker-compose.yml render_docker_compose_exposed_ports > docker-compose.yml
;; ;;
6)
render_docker_compose_traefik_tcp > docker-compose.yml
if [[ "$ENABLE_PROXY" == "true" ]]; then
# Create placeholder proxy.env so docker-compose can validate
# This will be overwritten with the actual token after Management starts
echo "# Placeholder - will be updated with token after Management starts" > proxy.env
echo "NB_PROXY_TOKEN=placeholder" >> proxy.env
fi
;;
*) *)
echo "Invalid reverse proxy type: $REVERSE_PROXY_TYPE" > /dev/stderr echo "Invalid reverse proxy type: $REVERSE_PROXY_TYPE" > /dev/stderr
exit 1 exit 1
@@ -402,6 +484,50 @@ start_services_and_show_instructions() {
echo "" echo ""
echo "NetBird containers are running. Configure NPM as shown above, then access:" echo "NetBird containers are running. Configure NPM as shown above, then access:"
echo " $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN" echo " $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN"
elif [[ "$REVERSE_PROXY_TYPE" == "6" ]]; then
# Traefik TCP Proxy - two-phase startup if proxy is enabled
echo -e "$MSG_STARTING_SERVICES"
if [[ "$ENABLE_PROXY" == "true" ]]; then
# Phase 1: Start core services (without proxy)
echo "Starting core services..."
$DOCKER_COMPOSE_COMMAND up -d traefik dashboard signal relay management
sleep 3
wait_management_traefik
# Phase 2: Create proxy token and start proxy
echo ""
echo "Creating proxy access token..."
# Use docker exec with bash to run the token command directly
# (bypassing the entrypoint which adds 'management' as first arg)
PROXY_TOKEN=$($DOCKER_COMPOSE_COMMAND exec -T management \
bash -c '/go/bin/netbird-mgmt token create --name "default-proxy" --config /etc/netbird/management.json' 2>/dev/null | grep "^Token:" | awk '{print $2}')
if [[ -z "$PROXY_TOKEN" ]]; then
echo "ERROR: Failed to create proxy token. Check management logs." > /dev/stderr
$DOCKER_COMPOSE_COMMAND logs --tail=20 management
exit 1
fi
echo "Proxy token created successfully."
# Generate proxy.env with the token
render_proxy_env > proxy.env
# Start proxy service
echo "Starting proxy service..."
$DOCKER_COMPOSE_COMMAND up -d proxy
else
# No proxy - start all services at once
$DOCKER_COMPOSE_COMMAND up -d
sleep 3
wait_management_traefik
fi
echo -e "$MSG_DONE"
print_post_setup_instructions
else else
# External proxies (nginx, external Caddy, other) - need manual config first # External proxies (nginx, external Caddy, other) - need manual config first
print_post_setup_instructions print_post_setup_instructions
@@ -547,6 +673,29 @@ EOF
return 0 return 0
} }
render_proxy_env() {
cat <<EOF
# NetBird Proxy Configuration
NB_PROXY_DEBUG_LOGS=false
# Use internal Docker network to connect to management (avoids hairpin NAT issues)
NB_PROXY_MANAGEMENT_ADDRESS=http://management:80
# Allow insecure gRPC connection to management (required for internal Docker network)
NB_PROXY_ALLOW_INSECURE=true
# Public URL where this proxy is reachable (used for cluster registration)
NB_PROXY_DOMAIN=$NETBIRD_DOMAIN
NB_PROXY_ADDRESS=:8443
NB_PROXY_TOKEN=$PROXY_TOKEN
NB_PROXY_CERTIFICATE_DIRECTORY=/certs
NB_PROXY_ACME_CERTIFICATES=true
NB_PROXY_ACME_CHALLENGE_TYPE=tls-alpn-01
NB_PROXY_OIDC_CLIENT_ID=netbird-proxy
NB_PROXY_OIDC_ENDPOINT=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/oauth2
NB_PROXY_OIDC_SCOPES=openid,profile,email
NB_PROXY_FORWARDED_PROTO=https
EOF
return 0
}
render_docker_compose() { render_docker_compose() {
cat <<EOF cat <<EOF
services: services:
@@ -736,7 +885,18 @@ $(if [[ -n "$tls_labels" ]]; then echo " - traefik.http.routers.netbird-rel
# Management (includes embedded IdP) # Management (includes embedded IdP)
management: management:
$(if [[ "$ENABLE_PROXY" == "true" ]]; then
cat <<MGMT_BUILD
build:
context: ..
dockerfile: management/Dockerfile.multistage
pull_policy: build
MGMT_BUILD
else
cat <<MGMT_IMAGE
image: $MANAGEMENT_IMAGE image: $MANAGEMENT_IMAGE
MGMT_IMAGE
fi)
container_name: netbird-management container_name: netbird-management
restart: unless-stopped restart: unless-stopped
networks: [$network_name] networks: [$network_name]
@@ -1115,6 +1275,258 @@ EOF
return 0 return 0
} }
render_docker_compose_traefik_tcp() {
# Generate proxy service section if enabled
local proxy_service=""
local proxy_volumes=""
local proxy_tcp_labels=""
if [[ "$ENABLE_PROXY" == "true" ]]; then
proxy_service="
# NetBird Proxy - exposes internal resources to the internet
proxy:
build:
context: ../
dockerfile: proxy/Dockerfile
# Always rebuild to pick up code changes during testing
pull_policy: build
#image: $PROXY_IMAGE
container_name: netbird-proxy
# Hairpin NAT fix: route domain back to traefik's static IP within Docker
extra_hosts:
- \"$NETBIRD_DOMAIN:172.30.0.10\"
restart: unless-stopped
networks: [netbird]
depends_on:
- signal
env_file:
- ./proxy.env
volumes:
- netbird_proxy_certs:/certs
labels:
# TCP passthrough for any unmatched domain (proxy handles its own TLS)
- traefik.enable=true
- traefik.tcp.routers.proxy-passthrough.entrypoints=websecure
- traefik.tcp.routers.proxy-passthrough.rule=HostSNI(\`*\`)
- traefik.tcp.routers.proxy-passthrough.tls.passthrough=true
- traefik.tcp.routers.proxy-passthrough.service=proxy-tls
- traefik.tcp.routers.proxy-passthrough.priority=1
- traefik.tcp.services.proxy-tls.loadbalancer.server.port=8443
logging:
driver: \"json-file\"
options:
max-size: \"500m\"
max-file: \"2\"
"
proxy_volumes="
netbird_proxy_certs:"
fi
cat <<EOF
services:
# Traefik - single port 443 entry point with TLS termination
traefik:
image: $TRAEFIK_IMAGE
container_name: netbird-traefik
restart: unless-stopped
networks:
netbird:
ipv4_address: 172.30.0.10
ports:
- '443:443'
volumes:
- netbird_traefik_data:/data
- /var/run/docker.sock:/var/run/docker.sock:ro
command:
# Logging
- --log.level=INFO
- --accesslog=true
# Docker provider
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=netbird
# Entrypoints
- --entrypoints.websecure.address=:443
- --entrypoints.websecure.allowACMEByPass=true
# Disable timeouts for long-lived gRPC streams
- --entrypoints.websecure.transport.respondingtimeouts.readtimeout=0s
- --entrypoints.websecure.transport.respondingtimeouts.writetimeout=0s
- --entrypoints.websecure.transport.respondingtimeouts.idletimeout=0s
# Let's Encrypt ACME
- --certificatesresolvers.letsencrypt.acme.email=$TRAEFIK_TCP_ACME_EMAIL
- --certificatesresolvers.letsencrypt.acme.storage=/data/acme.json
- --certificatesresolvers.letsencrypt.acme.tlschallenge=true
# gRPC transport settings (disable response timeout for long-lived streams)
- --serverstransport.forwardingtimeouts.responseheadertimeout=0s
- --serverstransport.forwardingtimeouts.idleconntimeout=0s
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
# UI dashboard
dashboard:
image: $DASHBOARD_IMAGE
container_name: netbird-dashboard
restart: unless-stopped
networks: [netbird]
env_file:
- ./dashboard.env
labels:
- traefik.enable=true
- traefik.http.routers.netbird-dashboard.entrypoints=websecure
- traefik.http.routers.netbird-dashboard.rule=Host(\`$NETBIRD_DOMAIN\`)
- traefik.http.routers.netbird-dashboard.tls=true
- traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-dashboard.service=dashboard
- traefik.http.routers.netbird-dashboard.priority=1
- traefik.http.services.dashboard.loadbalancer.server.port=80
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
# Signal
signal:
image: $SIGNAL_IMAGE
container_name: netbird-signal
restart: unless-stopped
networks: [netbird]
labels:
- traefik.enable=true
# Signal WebSocket
- traefik.http.routers.netbird-signal-ws.entrypoints=websecure
- traefik.http.routers.netbird-signal-ws.rule=Host(\`$NETBIRD_DOMAIN\`) && PathPrefix(\`/ws-proxy/signal\`)
- traefik.http.routers.netbird-signal-ws.tls=true
- traefik.http.routers.netbird-signal-ws.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-signal-ws.service=signal-ws
- traefik.http.routers.netbird-signal-ws.priority=100
- traefik.http.services.signal-ws.loadbalancer.server.port=80
# Signal gRPC
- traefik.http.routers.netbird-signal-grpc.entrypoints=websecure
- traefik.http.routers.netbird-signal-grpc.rule=Host(\`$NETBIRD_DOMAIN\`) && PathPrefix(\`/signalexchange.SignalExchange/\`)
- traefik.http.routers.netbird-signal-grpc.tls=true
- traefik.http.routers.netbird-signal-grpc.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-signal-grpc.service=signal-grpc
- traefik.http.routers.netbird-signal-grpc.priority=100
- traefik.http.services.signal-grpc.loadbalancer.server.port=10000
- traefik.http.services.signal-grpc.loadbalancer.server.scheme=h2c
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
# Relay (includes embedded STUN server)
relay:
image: $RELAY_IMAGE
container_name: netbird-relay
restart: unless-stopped
networks: [netbird]
ports:
- '$NETBIRD_STUN_PORT:$NETBIRD_STUN_PORT/udp'
env_file:
- ./relay.env
labels:
- traefik.enable=true
- traefik.http.routers.netbird-relay.entrypoints=websecure
- traefik.http.routers.netbird-relay.rule=Host(\`$NETBIRD_DOMAIN\`) && PathPrefix(\`/relay\`)
- traefik.http.routers.netbird-relay.tls=true
- traefik.http.routers.netbird-relay.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-relay.service=relay
- traefik.http.routers.netbird-relay.priority=100
- traefik.http.services.relay.loadbalancer.server.port=80
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
# Management (includes embedded IdP)
management:
$(if [[ "$ENABLE_PROXY" == "true" ]]; then
cat <<MGMT_BUILD
build:
context: ..
dockerfile: management/Dockerfile.multistage
pull_policy: build
MGMT_BUILD
else
cat <<MGMT_IMAGE
image: $MANAGEMENT_IMAGE
MGMT_IMAGE
fi)
container_name: netbird-management
restart: unless-stopped
networks: [netbird]
volumes:
- netbird_management:/var/lib/netbird
- ./management.json:/etc/netbird/management.json
command: [
"--port", "80",
"--log-file", "console",
"--log-level", "info",
"--disable-anonymous-metrics=false",
"--single-account-mode-domain=netbird.selfhosted",
"--dns-domain=netbird.selfhosted",
"--idp-sign-key-refresh-enabled",
]
labels:
- traefik.enable=true
# Management API
- traefik.http.routers.netbird-api.entrypoints=websecure
- traefik.http.routers.netbird-api.rule=Host(\`$NETBIRD_DOMAIN\`) && PathPrefix(\`/api\`)
- traefik.http.routers.netbird-api.tls=true
- traefik.http.routers.netbird-api.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-api.service=management
- traefik.http.routers.netbird-api.priority=100
# Management WebSocket
- traefik.http.routers.netbird-mgmt-ws.entrypoints=websecure
- traefik.http.routers.netbird-mgmt-ws.rule=Host(\`$NETBIRD_DOMAIN\`) && PathPrefix(\`/ws-proxy/management\`)
- traefik.http.routers.netbird-mgmt-ws.tls=true
- traefik.http.routers.netbird-mgmt-ws.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-mgmt-ws.service=management
- traefik.http.routers.netbird-mgmt-ws.priority=100
# Management gRPC
- traefik.http.routers.netbird-mgmt-grpc.entrypoints=websecure
- traefik.http.routers.netbird-mgmt-grpc.rule=Host(\`$NETBIRD_DOMAIN\`) && PathPrefix(\`/management.ManagementService/\`)
- traefik.http.routers.netbird-mgmt-grpc.tls=true
- traefik.http.routers.netbird-mgmt-grpc.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-mgmt-grpc.service=management-grpc
- traefik.http.routers.netbird-mgmt-grpc.priority=100
# OAuth2 (embedded IdP)
- traefik.http.routers.netbird-oauth2.entrypoints=websecure
- traefik.http.routers.netbird-oauth2.rule=Host(\`$NETBIRD_DOMAIN\`) && PathPrefix(\`/oauth2\`)
- traefik.http.routers.netbird-oauth2.tls=true
- traefik.http.routers.netbird-oauth2.tls.certresolver=letsencrypt
- traefik.http.routers.netbird-oauth2.service=management
- traefik.http.routers.netbird-oauth2.priority=100
# Services
- traefik.http.services.management.loadbalancer.server.port=80
- traefik.http.services.management-grpc.loadbalancer.server.port=80
- traefik.http.services.management-grpc.loadbalancer.server.scheme=h2c
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
${proxy_service}
volumes:
netbird_traefik_data:
netbird_management:${proxy_volumes}
networks:
netbird:
driver: bridge
ipam:
config:
- subnet: 172.30.0.0/24
gateway: 172.30.0.1
EOF
return 0
}
render_npm_advanced_config() { render_npm_advanced_config() {
local upstream_host=$(get_upstream_host) local upstream_host=$(get_upstream_host)
local relay_addr="${upstream_host}:${RELAY_HOST_PORT}" local relay_addr="${upstream_host}:${RELAY_HOST_PORT}"
@@ -1424,6 +1836,36 @@ print_manual_instructions() {
return 0 return 0
} }
print_traefik_tcp_instructions() {
echo ""
echo "$MSG_SEPARATOR"
echo " TRAEFIK TCP PROXY SETUP"
echo "$MSG_SEPARATOR"
echo ""
echo "This configuration uses Traefik as a single entry point on port 443."
echo "Traefik handles TLS termination with Let's Encrypt and routes to services."
echo ""
echo "Open ports:"
echo " - 443/tcp (HTTPS - all NetBird services)"
echo " - $NETBIRD_STUN_PORT/udp (STUN - required for NAT traversal)"
echo ""
echo "Generated files:"
echo " - docker-compose.yml (container definitions with Traefik labels)"
if [[ "$ENABLE_PROXY" == "true" ]]; then
echo " - proxy.env (NetBird Proxy configuration)"
echo ""
echo "NetBird Proxy:"
echo " The proxy service is enabled and will be built from source."
echo " Any domain NOT matching $NETBIRD_DOMAIN will be passed through to the proxy."
echo " The proxy handles its own TLS certificates via ACME TLS-ALPN-01 challenge."
echo " Point your proxy domains (CNAMEs) to this server's IP address."
fi
echo ""
echo "You can access the NetBird dashboard at $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN"
echo "Follow the onboarding steps to set up your NetBird instance."
return 0
}
print_post_setup_instructions() { print_post_setup_instructions() {
case "$REVERSE_PROXY_TYPE" in case "$REVERSE_PROXY_TYPE" in
0) 0)
@@ -1444,6 +1886,9 @@ print_post_setup_instructions() {
5) 5)
print_manual_instructions print_manual_instructions
;; ;;
6)
print_traefik_tcp_instructions
;;
*) *)
echo "Unknown reverse proxy type: $REVERSE_PROXY_TYPE" > /dev/stderr echo "Unknown reverse proxy type: $REVERSE_PROXY_TYPE" > /dev/stderr
;; ;;