diff --git a/VERSION b/VERSION index 17b2ccd..6f2743d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.3 +0.4.4 diff --git a/lib/cmd_install.sh b/lib/cmd_install.sh index 3e39a4e..0c23f9e 100644 --- a/lib/cmd_install.sh +++ b/lib/cmd_install.sh @@ -47,6 +47,7 @@ cmd_install() { # 5. Generate docker-compose.yml generate_compose_file + warn_custom_ports_under_host_net # 6. Build Docker image docker_build || die "Docker build failed. Check the output above for details." diff --git a/lib/cmd_security.sh b/lib/cmd_security.sh index 54957e6..def2b77 100644 --- a/lib/cmd_security.sh +++ b/lib/cmd_security.sh @@ -288,6 +288,32 @@ _check_permissions() { fi } +# Flag ufw-docker presence when the bridged monitoring stack is installed. The +# node itself uses host networking on Linux (so its QUIC port is unaffected by +# ufw-docker's DOCKER-USER UDP drops), but Grafana stays on the bridge — and +# ufw-docker will block LAN access to it until the operator allows the port +# explicitly. Warn-only; no auto-apply, since `ufw-docker allow` needs root +# and the exact command is operator-environment specific. +_check_ufw_docker() { + [[ "$LOGOS_OS" == "linux" ]] || return 0 + + # The ufw-docker shell helper edits /etc/ufw/after.rules; the binary itself + # may not stick around in PATH after install. Match either signal. + local has_rules=false has_bin=false + grep -qE '^-A DOCKER-USER' /etc/ufw/after.rules 2>/dev/null && has_rules=true + command -v ufw-docker &>/dev/null && has_bin=true + [[ "$has_rules" == "false" && "$has_bin" == "false" ]] && return 0 + + # Only warn when there's actually a bridged surface affected. Gate on the + # monitoring compose existing on disk, not on the stack running — the + # block applies regardless of whether Grafana is currently up. + local mon_compose="$LOGOS_NODE_DIR/docker-compose.monitoring.yml" + [[ -f "$mon_compose" ]] || return 0 + + _add_finding "warn" "ufw-docker" \ + "detected — may block LAN access to Grafana (${LOGOS_GRAFANA_PORT}/tcp). Allow with: sudo ufw-docker allow logos-grafana 3000/tcp" +} + # ── Scan (report only) ────────────────────────────────────────────── _security_scan() { @@ -316,6 +342,7 @@ _security_scan() { _check_auto_updates _check_fail2ban _check_permissions + _check_ufw_docker # Print findings for finding in "${FINDINGS[@]}"; do @@ -363,6 +390,18 @@ _security_apply() { _check_auto_updates _check_fail2ban _check_permissions + _check_ufw_docker + + # ufw-docker finding is informational — surface it up-front so the operator + # sees it before the interactive prompts. No corresponding _apply step; + # the suggested command needs root and is environment-specific. + for finding in "${FINDINGS[@]}"; do + if [[ "$finding" == *"ufw-docker"* ]]; then + echo -e " $finding" + echo "" + break + fi + done local changes_made=0 diff --git a/lib/cmd_start.sh b/lib/cmd_start.sh index 06c1646..367a76b 100644 --- a/lib/cmd_start.sh +++ b/lib/cmd_start.sh @@ -25,12 +25,34 @@ cmd_start() { return 0 fi - # Regenerate compose if port settings changed + # Regenerate compose if the on-disk file disagrees with current settings. + # Two flavors of drift: network mode (host vs bridge) and host port mapping + # (bridge mode only — host mode has no port maps to drift). if [[ -f "$compose_path" ]]; then - if ! grep -q "\"${LOGOS_API_PORT}:8080\"" "$compose_path" 2>/dev/null || \ - ! grep -q "\"${LOGOS_UDP_PORT}:3000/udp\"" "$compose_path" 2>/dev/null; then - log_info "Port settings changed — regenerating docker-compose.yml" + local needs_regen=false + local file_has_host_mode=false + grep -q '^[[:space:]]*network_mode:[[:space:]]*host' "$compose_path" 2>/dev/null && file_has_host_mode=true + + if [[ "$LOGOS_DOCKER_NETWORK_MODE" == "host" ]] && [[ "$file_has_host_mode" == "false" ]]; then + log_info "Switching to host networking — regenerating docker-compose.yml" + needs_regen=true + elif [[ "$LOGOS_DOCKER_NETWORK_MODE" != "host" ]] && [[ "$file_has_host_mode" == "true" ]]; then + log_info "Switching to bridge networking — regenerating docker-compose.yml" + needs_regen=true + elif [[ "$LOGOS_DOCKER_NETWORK_MODE" != "host" ]]; then + if ! grep -q "\"${LOGOS_API_PORT}:8080\"" "$compose_path" 2>/dev/null || \ + ! grep -q "\"${LOGOS_UDP_PORT}:3000/udp\"" "$compose_path" 2>/dev/null; then + log_info "Port settings changed — regenerating docker-compose.yml" + needs_regen=true + fi + fi + + if [[ "$needs_regen" == "true" ]]; then generate_compose_file + # OTLP endpoint baked into user_config.yaml depends on the network + # mode (logos-otel DNS under bridge, 127.0.0.1 under host). Rewrite + # if the mode just flipped. Idempotent / no-op otherwise. + migrate_user_config_otlp_endpoint "$(get_user_config_path)" fi fi diff --git a/lib/cmd_update.sh b/lib/cmd_update.sh index 0952788..850d03d 100644 --- a/lib/cmd_update.sh +++ b/lib/cmd_update.sh @@ -238,6 +238,7 @@ cmd_update() { save_setting "LOGOS_NODE_VERSION" "$LOGOS_NODE_VERSION" save_setting "LOGOS_CIRCUITS_VERSION" "$LOGOS_CIRCUITS_VERSION" generate_compose_file + warn_custom_ports_under_host_net source "$LOGOS_NODE_LIB/cmd_reset.sh" _perform_migration "update" "true" return 0 @@ -261,6 +262,8 @@ cmd_update() { save_setting "LOGOS_CIRCUITS_VERSION" "$LOGOS_CIRCUITS_VERSION" generate_compose_file + warn_custom_ports_under_host_net + migrate_user_config_otlp_endpoint "$(get_user_config_path)" docker_build || die "Failed to build updated Docker image" log_success "Node updated to ${LOGOS_NODE_VERSION}" @@ -292,6 +295,8 @@ cmd_update() { echo "" log_info "Node compose schema changed — regenerating docker-compose.yml" generate_compose_file + warn_custom_ports_under_host_net + migrate_user_config_otlp_endpoint "$(get_user_config_path)" if docker_is_running; then if confirm "Recreate the container now to apply the new compose?"; then log_step "Recreating node container..." diff --git a/lib/config.sh b/lib/config.sh index 86eeec6..355efb0 100644 --- a/lib/config.sh +++ b/lib/config.sh @@ -83,10 +83,26 @@ _set_defaults() { : "${LOGOS_GRAFANA_AUTH:=false}" : "${LOGOS_GRAFANA_PASSWORD:=logos}" + # Docker network mode for the node container. Default to host networking on + # Linux so ufw-docker's DOCKER-USER deny rules (which drop UDP dport ≤32767 + # to container IPs) can't kill the node's QUIC handshake replies — see + # https://github.com/logosnode/logosup/issues/18. Bridge stays the default + # everywhere else (Docker Desktop on macOS doesn't honor host mode). + # Operators with custom ports or other host-net problems can opt out by + # setting LOGOS_DOCKER_NETWORK_MODE=bridge in settings.env — that value is + # sourced before this function runs, so the override wins. + if [[ -z "${LOGOS_DOCKER_NETWORK_MODE:-}" ]]; then + if [[ "$(uname -s)" == "Linux" ]]; then + LOGOS_DOCKER_NETWORK_MODE="host" + else + LOGOS_DOCKER_NETWORK_MODE="bridge" + fi + fi + export LOGOS_NETWORK LOGOS_NODE_VERSION LOGOS_CIRCUITS_VERSION LOGOS_API_PORT LOGOS_UDP_PORT export LOGOS_FAUCET_URL LOGOS_DASHBOARD_URL LOGOS_DOCKER_IMAGE LOGOS_CONTAINER_NAME export LOGOS_NODE_REPO LOGOS_CLI_REPO LOGOS_BOOTSTRAP_PEERS LOGOS_GRAFANA_PORT - export LOGOS_GRAFANA_AUTH LOGOS_GRAFANA_PASSWORD + export LOGOS_GRAFANA_AUTH LOGOS_GRAFANA_PASSWORD LOGOS_DOCKER_NETWORK_MODE } # ── Init & Load ─────────────────────────────────────────────────────── diff --git a/lib/docker.sh b/lib/docker.sh index 8fb5730..a755b14 100644 --- a/lib/docker.sh +++ b/lib/docker.sh @@ -89,7 +89,14 @@ check_docker() { export DOCKER_COMPOSE DOCKER_CMD } -# Generate docker-compose.yml from settings +# Generate docker-compose.yml from settings. +# +# Two shapes depending on $LOGOS_DOCKER_NETWORK_MODE: +# host — node shares the host net namespace. No docker port mapping, no +# bridge. Linux default. Sidesteps ufw-docker's DOCKER-USER UDP +# drops that break QUIC on bridged containers (issue #18). +# bridge — original behavior. Mac/Docker Desktop default; Linux opt-out +# for operators who customize LOGOS_API_PORT / LOGOS_UDP_PORT. generate_compose_file() { local compose_path compose_path="$(get_compose_path)" @@ -101,7 +108,40 @@ generate_compose_file() { local host_gid host_gid="$(id -g)" - cat > "$compose_path" << YAML + if [[ "$LOGOS_DOCKER_NETWORK_MODE" == "host" ]]; then + cat > "$compose_path" << YAML +services: + logos-node: + build: + context: ${dockerfile_dir} + args: + NODE_VERSION: "${LOGOS_NODE_VERSION}" + CIRCUITS_VERSION: "${LOGOS_CIRCUITS_VERSION}" + image: ${LOGOS_DOCKER_IMAGE}:${LOGOS_NODE_VERSION} + container_name: ${LOGOS_CONTAINER_NAME} + restart: unless-stopped + user: "${host_uid}:${host_gid}" + network_mode: host + working_dir: /app/data + volumes: + - ${LOGOS_NODE_DIR}/user_config.yaml:/app/data/user_config.yaml:ro + - ${LOGOS_NODE_DIR}/data:/app/data + environment: + - LOGOS_BLOCKCHAIN_CIRCUITS=/app/circuits + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8080/cryptarchia/info"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 120s + logging: + driver: json-file + options: + max-size: "50m" + max-file: "5" +YAML + else + cat > "$compose_path" << YAML services: logos-node: build: @@ -140,8 +180,23 @@ networks: logosnode-net: name: logosnode-net YAML + fi log_success "Generated docker-compose.yml" + log_dim "Networking mode: ${LOGOS_DOCKER_NETWORK_MODE}" +} + +# Under host networking, the node binary listens on its own internal defaults +# (HTTP 8080, QUIC 3000) and Docker's host:container port mapping no longer +# applies — so LOGOS_API_PORT and LOGOS_UDP_PORT customizations silently lose +# effect. Surface this once after compose generation so the operator can +# either revert the custom port or opt back into bridge networking. +warn_custom_ports_under_host_net() { + [[ "$LOGOS_DOCKER_NETWORK_MODE" == "host" ]] || return 0 + [[ "$LOGOS_API_PORT" != "8080" || "$LOGOS_UDP_PORT" != "3000" ]] || return 0 + + log_warn "Custom API/UDP ports (${LOGOS_API_PORT}/${LOGOS_UDP_PORT}) are ignored under host networking." + log_info "To keep your custom ports, set ${BOLD}LOGOS_DOCKER_NETWORK_MODE=bridge${RESET} in ${LOGOS_SETTINGS_FILE}" } # Build the Docker image @@ -248,6 +303,19 @@ docker_init_config() { fi } +# Endpoint the node should push OTLP metrics to. Depends on the network mode: +# under bridge, the otel collector is reachable via compose's container DNS; +# under host networking, the node container shares the host net namespace and +# can't resolve the bridge-internal DNS name, so it pushes to the loopback port +# we publish on the otel service (see lib/monitoring.sh). +_otlp_endpoint_for_mode() { + if [[ "$LOGOS_DOCKER_NETWORK_MODE" == "host" ]]; then + echo "http://127.0.0.1:4317" + else + echo "http://logos-otel:4317" + fi +} + # Enable OTLP metrics push in user_config.yaml so logos-otel can collect # native node metrics. Idempotent: only rewrites `metrics: None`. If metrics # is already configured (e.g. operator customized it) we leave it alone. @@ -258,19 +326,57 @@ patch_user_config_for_otlp() { [[ -f "$config_path" ]] || return 0 grep -qE '^[[:space:]]+metrics: None$' "$config_path" || return 0 - awk ' + local otlp_endpoint + otlp_endpoint="$(_otlp_endpoint_for_mode)" + + awk -v endpoint="$otlp_endpoint" ' /^[[:space:]]+metrics: None$/ { match($0, /^[[:space:]]+/) indent = substr($0, 1, RLENGTH) print indent "metrics: !Otlp" - print indent " endpoint: \"http://logos-otel:4317\"" + print indent " endpoint: \"" endpoint "\"" print indent " host_identifier: \"logos-node\"" next } { print } ' "$config_path" > "${config_path}.tmp" && mv "${config_path}.tmp" "$config_path" chmod 600 "$config_path" - log_dim "Enabled OTLP metrics push to logos-otel (for monitoring stack)" + log_dim "Enabled OTLP metrics push to ${otlp_endpoint} (for monitoring stack)" +} + +# Migrate an existing user_config.yaml OTLP endpoint to match the current +# network mode. Used on `logosup update` so 0.4.3 installs (which baked in +# `http://logos-otel:4317`) get rewritten to `http://127.0.0.1:4317` after +# switching to host networking on Linux. Reversible: opting back into bridge +# mode rewrites the endpoint the other way. +# +# Idempotent no-op when: +# - $config_path does not exist +# - No `endpoint:` line is present (metrics block absent / operator opted out) +# - The endpoint already matches the desired mode +# +# Never touches custom endpoints — only the two values this CLI is known to +# write. An operator who pointed metrics at their own collector is unaffected. +migrate_user_config_otlp_endpoint() { + local config_path="$1" + [[ -f "$config_path" ]] || return 0 + + local desired old + desired="$(_otlp_endpoint_for_mode)" + if [[ "$desired" == "http://127.0.0.1:4317" ]]; then + old="http://logos-otel:4317" + else + old="http://127.0.0.1:4317" + fi + + grep -qE '^[[:space:]]+endpoint:[[:space:]]*"' "$config_path" || return 0 + grep -qE "endpoint:[[:space:]]*\"${desired}\"" "$config_path" && return 0 + # Regex-escape dots in the source pattern so they don't act as wildcards. + local old_escaped="${old//./\\.}" + grep -qE "endpoint:[[:space:]]*\"${old_escaped}\"" "$config_path" || return 0 + + sed_inplace "s|endpoint: \"${old_escaped}\"|endpoint: \"${desired}\"|" "$config_path" + log_dim "Migrated OTLP endpoint → ${desired}" } # Disable the tracing module's disk-based file output. The default config diff --git a/lib/monitoring.sh b/lib/monitoring.sh index b68fa9d..10c76b1 100644 --- a/lib/monitoring.sh +++ b/lib/monitoring.sh @@ -57,6 +57,22 @@ generate_monitoring_compose_file() { log_step "Generating monitoring compose file..." + # Under host networking the node container leaves the bridge, so the + # exporter can no longer reach it via compose DNS (logos-node:8080) and + # the node can no longer reach the otel collector via DNS either. Re-point + # both via host.docker.internal / loopback. Bridge mode is unchanged. + local node_api_url exporter_host_block otel_host_ports + if [[ "$LOGOS_DOCKER_NETWORK_MODE" == "host" ]]; then + node_api_url="http://host.docker.internal:${LOGOS_API_PORT}" + exporter_host_block=$' extra_hosts:\n - "host.docker.internal:host-gateway"' + # Bind 4317 to loopback only — never publish OTLP to the LAN. + otel_host_ports=$' ports:\n - "127.0.0.1:4317:4317"' + else + node_api_url="http://${LOGOS_CONTAINER_NAME}:8080" + exporter_host_block="" + otel_host_ports="" + fi + cat > "$compose_path" << COMPOSE services: logos-exporter: @@ -67,10 +83,11 @@ services: ports: - "9100:9100" environment: - - NODE_API_URL=http://${LOGOS_CONTAINER_NAME}:8080 + - NODE_API_URL=${node_api_url} - CONTAINER_NAME=${LOGOS_CONTAINER_NAME} - POLL_INTERVAL=15 pid: host +${exporter_host_block} volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - /proc:/host/proc:ro @@ -95,6 +112,7 @@ services: - "4317" # OTLP gRPC (node pushes here) - "4318" # OTLP HTTP - "8889" # Prometheus scrape +${otel_host_ports} logging: driver: json-file options: