#!/bin/sh set -eu . /usr/libexec/haproxy-manager/common.sh umask 077 OUT="${1:-$TMP_CFG}" is_safe_temp_path "$OUT" || { echo "Refusing unsafe generated config path: $OUT" >&2; exit 1; } [ ! -d "$OUT" ] || { echo "Refusing generated config directory: $OUT" >&2; exit 1; } WORK_OUT="$OUT.new.$$" ENABLED="$(uci_get main enabled 0)" HTTP_PORT="$(uci_get main http_port 80)" HTTPS_PORT="$(uci_get main https_port 443)" WAN_IP="$(wan_ip || true)" if [ "$ENABLED" != 1 ]; then echo "Managed HAProxy configuration is disabled." >&2 exit 2 fi is_valid_port "$HTTP_PORT" || { echo "Invalid public HTTP port: $HTTP_PORT" >&2; exit 1; } is_valid_port "$HTTPS_PORT" || { echo "Invalid public HTTPS port: $HTTPS_PORT" >&2; exit 1; } if [ -z "$WAN_IP" ]; then echo "Cannot detect WAN IPv4 address. Set the WAN bind address manually." >&2 exit 1 fi is_valid_ip_or_host "$WAN_IP" || { echo "Invalid WAN bind address: $WAN_IP" >&2; exit 1; } TMP_HTTP_RULES=/tmp/haproxy-manager-http-rules.$$ TMP_HTTP_BACKENDS=/tmp/haproxy-manager-http-backends.$$ TMP_TLS_RULES=/tmp/haproxy-manager-tls-rules.$$ TMP_TLS_BACKENDS=/tmp/haproxy-manager-tls-backends.$$ TMP_TCP_FRONTENDS=/tmp/haproxy-manager-tcp-frontends.$$ TMP_PORTS=/tmp/haproxy-manager-listen-ports.$$ : > "$TMP_HTTP_RULES" : > "$TMP_HTTP_BACKENDS" : > "$TMP_TLS_RULES" : > "$TMP_TLS_BACKENDS" : > "$TMP_TCP_FRONTENDS" : > "$TMP_PORTS" trap 'rm -f "$WORK_OUT" "$TMP_HTTP_RULES" "$TMP_HTTP_BACKENDS" "$TMP_TLS_RULES" "$TMP_TLS_BACKENDS" "$TMP_TCP_FRONTENDS" "$TMP_PORTS"' EXIT HAS_HTTP=0 HAS_HTTPS=0 HAS_TCP=0 claim_port() { local port="$1" local owner="$2" local existing if grep -q "^${port}[[:space:]]" "$TMP_PORTS"; then existing="$(sed -n "s/^${port}[[:space:]]*//p" "$TMP_PORTS" | head -1)" echo "Public TCP port $port conflicts with ${existing:-another service}." >&2 exit 1 fi printf '%s\t%s\n' "$port" "$owner" >> "$TMP_PORTS" } add_http_route() { local name="$1" host="$2" backend_host="$3" backend_port="$4" local address address="$(backend_address "$backend_host" "$backend_port")" if [ "$HAS_HTTP" != 1 ]; then claim_port "$HTTP_PORT" "the shared Web HTTP listener" HAS_HTTP=1 fi { printf ' acl host_%s hdr(host),host_only -i %s\n' "$name" "$host" printf ' use_backend be_http_%s if host_%s\n' "$name" "$name" } >> "$TMP_HTTP_RULES" { printf 'backend be_http_%s\n' "$name" printf ' mode http\n' printf ' option httpchk GET /\n' printf ' server srv1 %s check\n\n' "$address" } >> "$TMP_HTTP_BACKENDS" } add_https_route() { local name="$1" host="$2" backend_host="$3" backend_port="$4" local address address="$(backend_address "$backend_host" "$backend_port")" if [ "$HAS_HTTPS" != 1 ]; then claim_port "$HTTPS_PORT" "the shared Web HTTPS listener" HAS_HTTPS=1 fi printf ' use_backend be_tls_%s if { req.ssl_sni -i %s }\n' "$name" "$host" >> "$TMP_TLS_RULES" { printf 'backend be_tls_%s\n' "$name" printf ' mode tcp\n' printf ' server srv1 %s check\n\n' "$address" } >> "$TMP_TLS_BACKENDS" } add_tcp_route() { local name="$1" label="$2" backend_host="$3" public_port="$4" backend_port="$5" local address bind_address address="$(backend_address "$backend_host" "$backend_port")" bind_address="$(backend_address "$WAN_IP" "$public_port")" claim_port "$public_port" "service $label" HAS_TCP=1 { printf 'frontend fe_tcp_%s\n' "$name" printf ' bind %s\n' "$bind_address" printf ' mode tcp\n' printf ' default_backend be_tcp_%s\n\n' "$name" printf 'backend be_tcp_%s\n' "$name" printf ' mode tcp\n' printf ' server srv1 %s check\n\n' "$address" } >> "$TMP_TCP_FRONTENDS" } for section in $(route_sections); do enabled="$(uci_get "$section" enabled 1)" [ "$enabled" = 1 ] || continue kind="$(route_kind "$section")" label="$(uci_get "$section" name)" host="$(uci_get "$section" host)" backend_host="$(uci_get "$section" backend_host)" [ -n "$label" ] || label="$host" [ -n "$label" ] || label="$kind" name="$(route_name "${label}_${section}")" is_valid_ip_or_host "$backend_host" || { echo "Invalid backend host for $label: $backend_host" >&2; exit 1; } case "$kind" in web) is_valid_host "$host" || { echo "Invalid or missing domain for Web service $label: $host" >&2; exit 1; } http_enabled="$(uci_get "$section" web_http 1)" https_enabled="$(uci_get "$section" web_https 1)" [ "$http_enabled" = 1 ] || [ "$https_enabled" = 1 ] || { echo "Web service $label has no enabled protocol." >&2; exit 1; } if [ "$http_enabled" = 1 ]; then backend_port="$(uci_get "$section" backend_http_port 80)" is_valid_port "$backend_port" || { echo "Invalid HTTP backend port for $label: $backend_port" >&2; exit 1; } add_http_route "$name" "$host" "$backend_host" "$backend_port" fi if [ "$https_enabled" = 1 ]; then backend_port="$(uci_get "$section" backend_https_port 443)" is_valid_port "$backend_port" || { echo "Invalid HTTPS backend port for $label: $backend_port" >&2; exit 1; } add_https_route "$name" "$host" "$backend_host" "$backend_port" fi ;; http|https|both) is_valid_host "$host" || { echo "Invalid or missing domain for legacy service $label: $host" >&2; exit 1; } backend_port="$(uci_get "$section" backend_port)" is_valid_port "$backend_port" || { echo "Invalid backend port for $label: $backend_port" >&2; exit 1; } case "$kind" in http|both) add_http_route "$name" "$host" "$backend_host" "$backend_port" ;; esac case "$kind" in https|both) add_https_route "$name" "$host" "$backend_host" "$backend_port" ;; esac ;; ssh|rdp|custom|tcp) map_index=0 maps="$(route_port_maps "$section" "$kind")" [ -n "$maps" ] || { echo "No TCP port mappings configured for $label." >&2; exit 1; } for map in $maps; do is_valid_port_map "$map" || { echo "Invalid TCP port mapping for $label: $map" >&2; exit 1; } public_port="${map%%:*}" backend_port="${map#*:}" map_index=$((map_index + 1)) add_tcp_route "${name}_${map_index}" "$label" "$backend_host" "$public_port" "$backend_port" done ;; *) echo "Invalid service type for $label: $kind" >&2 exit 1 ;; esac done if [ "$HAS_HTTP" != 1 ] && [ "$HAS_HTTPS" != 1 ] && [ "$HAS_TCP" != 1 ]; then echo "No enabled services define a HAProxy listener." >&2 exit 2 fi { printf '# Generated by luci-app-haproxy-manager. Manual changes may be overwritten.\n' printf 'global\n' printf ' log /dev/log local0\n' printf ' maxconn 2048\n' printf ' daemon\n\n' printf 'defaults\n' printf ' log global\n' printf ' timeout connect 5s\n' printf ' timeout client 60s\n' printf ' timeout server 60s\n' printf ' option dontlognull\n\n' if [ "$HAS_HTTP" = 1 ]; then http_bind="$(backend_address "$WAN_IP" "$HTTP_PORT")" printf 'frontend fe_web_http\n' printf ' bind %s\n' "$http_bind" printf ' mode http\n' printf ' option httplog\n' cat "$TMP_HTTP_RULES" printf ' default_backend be_not_found\n\n' cat "$TMP_HTTP_BACKENDS" fi if [ "$HAS_HTTPS" = 1 ]; then https_bind="$(backend_address "$WAN_IP" "$HTTPS_PORT")" printf 'frontend fe_web_https\n' printf ' bind %s\n' "$https_bind" printf ' mode tcp\n' printf ' tcp-request inspect-delay 5s\n' printf ' tcp-request content accept if { req.ssl_hello_type 1 }\n' cat "$TMP_TLS_RULES" printf ' default_backend be_tls_not_found\n\n' cat "$TMP_TLS_BACKENDS" fi cat "$TMP_TCP_FRONTENDS" printf 'backend be_not_found\n' printf ' mode http\n' printf ' http-request return status 404 content-type text/plain string "No HAProxy route matched this host.\\n"\n\n' printf 'backend be_tls_not_found\n' printf ' mode tcp\n' printf ' tcp-request content reject\n' } > "$WORK_OUT" # Replace the path itself so a stale symlink cannot redirect this root write. rm -f "$OUT" mv "$WORK_OUT" "$OUT" printf '%s\n' "$OUT"