#!/bin/sh # SecuBox mitmproxy manager - LXC container support with transparent mode # Copyright (C) 2024-2025 CyberMind.fr # Supports multiple instances: out (LAN->Internet), in (WAF/reverse) CONFIG="mitmproxy" OPKG_UPDATED=0 NFT_TABLE="mitmproxy" NFT_TABLE_WAN="mitmproxy_wan" # Current instance (set by load_instance_config) INSTANCE="" LXC_NAME="mitmproxy" LXC_PATH="/srv/lxc" LXC_ROOTFS="" LXC_CONFIG="" ADDON_PATH="/etc/mitmproxy/addons" usage() { cat <<'EOF' Usage: mitmproxyctl [instance] Commands: install [inst] Install prerequisites and create LXC container check Run prerequisite checks update [inst] Update mitmproxy in container status [inst] Show container status logs [inst] [-f] Show mitmproxy logs (use -f to follow) shell [inst] Open shell in container cert [inst] Show CA certificate info / export path firewall-setup Setup nftables rules for transparent mode firewall-clear Remove nftables transparent mode rules wan-setup Setup WAN protection mode (incoming traffic) wan-clear Remove WAN protection mode rules sync-routes Sync HAProxy/MetaBlogizer/Streamlit routes route list List all mitmproxy routes route add Add manual route route remove Remove route route check Check for missing routes haproxy-enable Enable HAProxy backend inspection mode haproxy-disable Disable HAProxy backend inspection mode process-autoban Process auto-ban requests from WAF (run via cron) reload-autoban Reload auto-ban config after UCI changes service-run [i] Internal: run container under procd service-stop [i] Stop container list-instances List configured instances Instances (configure in /etc/config/mitmproxy): out - LAN->Internet transparent proxy (outbound traffic) in - WAF/Reverse proxy (inbound traffic to services) Auto-ban Sensitivity Levels: aggressive - Ban immediately on first critical threat moderate - Ban after repeated attempts (default: 3 in 5 min) permissive - Ban after persistent attempts (default: 5 in 1 hour) Configure with: uci set mitmproxy.autoban.sensitivity='moderate' Modes (configure per-instance): regular - Standard HTTP/HTTPS proxy (default) transparent - Transparent proxy (auto-configures nftables) upstream - Forward to upstream proxy reverse - Reverse proxy mode Instance Ports (default): out: proxy=8888, web=8089 (LAN->Internet) in: proxy=8889, web=8090 (WAF/HAProxy backend) Examples: mitmproxyctl status out # Status of 'out' instance mitmproxyctl shell in # Shell into 'in' instance mitmproxyctl service-run out # Start 'out' instance (used by init.d) EOF } require_root() { [ "$(id -u)" -eq 0 ] || { echo "Root required" >&2; exit 1; }; } log_info() { echo "[INFO] $*"; } log_warn() { echo "[WARN] $*" >&2; } log_error() { echo "[ERROR] $*" >&2; } uci_get() { uci -q get ${CONFIG}.$1; } uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; } uci_get_list() { uci -q get ${CONFIG}.$1 2>/dev/null; } # Write autoban config to JSON for container to read write_autoban_config() { load_config local autoban_enabled=$(uci_get autoban.enabled || echo 0) local ban_duration=$(uci_get autoban.ban_duration || echo "4h") local min_severity=$(uci_get autoban.min_severity || echo "critical") local ban_cve=$(uci_get autoban.ban_cve_exploits || echo 1) local ban_sqli=$(uci_get autoban.ban_sqli || echo 1) local ban_cmdi=$(uci_get autoban.ban_cmdi || echo 1) local ban_traversal=$(uci_get autoban.ban_traversal || echo 1) local ban_scanners=$(uci_get autoban.ban_scanners || echo 1) local ban_rate_limit=$(uci_get autoban.ban_rate_limit || echo 0) local whitelist=$(uci_get autoban.whitelist || echo "") local sensitivity=$(uci_get autoban.sensitivity || echo "moderate") local moderate_threshold=$(uci_get autoban.moderate_threshold || echo 3) local moderate_window=$(uci_get autoban.moderate_window || echo 300) local permissive_threshold=$(uci_get autoban.permissive_threshold || echo 5) local permissive_window=$(uci_get autoban.permissive_window || echo 3600) # Convert 0/1 to true/false for JSON local enabled_json="false" [ "$autoban_enabled" = "1" ] && enabled_json="true" local cve_json="false" [ "$ban_cve" = "1" ] && cve_json="true" local sqli_json="false" [ "$ban_sqli" = "1" ] && sqli_json="true" local cmdi_json="false" [ "$ban_cmdi" = "1" ] && cmdi_json="true" local traversal_json="false" [ "$ban_traversal" = "1" ] && traversal_json="true" local scanners_json="false" [ "$ban_scanners" = "1" ] && scanners_json="true" local rate_json="false" [ "$ban_rate_limit" = "1" ] && rate_json="true" # Write JSON config cat > "$data_path/autoban.json" << EOF { "enabled": $enabled_json, "ban_duration": "$ban_duration", "min_severity": "$min_severity", "ban_cve_exploits": $cve_json, "ban_sqli": $sqli_json, "ban_cmdi": $cmdi_json, "ban_traversal": $traversal_json, "ban_scanners": $scanners_json, "ban_rate_limit": $rate_json, "whitelist": "$whitelist", "sensitivity": "$sensitivity", "moderate_threshold": $moderate_threshold, "moderate_window": $moderate_window, "permissive_threshold": $permissive_threshold, "permissive_window": $permissive_window } EOF chmod 644 "$data_path/autoban.json" } # Load instance-specific configuration # Usage: load_instance_config [instance_name] # If no instance provided, falls back to legacy 'main' config load_instance_config() { local inst="${1:-}" if [ -n "$inst" ]; then # Check if instance exists local inst_enabled="$(uci_get ${inst}.enabled 2>/dev/null)" if [ -z "$inst_enabled" ]; then log_error "Instance '$inst' not found in config" return 1 fi INSTANCE="$inst" LXC_NAME="$(uci_get ${inst}.container_name || echo mitmproxy-${inst})" proxy_port="$(uci_get ${inst}.proxy_port || echo 8888)" web_port="$(uci_get ${inst}.web_port || echo 8081)" web_host="$(uci_get ${inst}.web_host || echo 0.0.0.0)" data_path="$(uci_get ${inst}.data_path || echo /srv/mitmproxy-${inst})" memory_limit="$(uci_get ${inst}.memory_limit || echo 256M)" mode="$(uci_get ${inst}.mode || echo regular)" upstream_proxy="$(uci_get ${inst}.upstream_proxy || echo '')" reverse_target="$(uci_get ${inst}.reverse_target || echo '')" ssl_insecure="$(uci_get ${inst}.ssl_insecure || echo 0)" anticache="$(uci_get ${inst}.anticache || echo 0)" anticomp="$(uci_get ${inst}.anticomp || echo 0)" flow_detail="$(uci_get ${inst}.flow_detail || echo 1)" haproxy_backend="$(uci_get ${inst}.haproxy_backend || echo 0)" else # Legacy single-instance mode INSTANCE="" LXC_NAME="mitmproxy" proxy_port="$(uci_get main.proxy_port || echo 8888)" web_port="$(uci_get main.web_port || echo 8081)" web_host="$(uci_get main.web_host || echo 0.0.0.0)" data_path="$(uci_get main.data_path || echo /srv/mitmproxy)" memory_limit="$(uci_get main.memory_limit || echo 256M)" mode="$(uci_get main.mode || echo regular)" upstream_proxy="$(uci_get main.upstream_proxy || echo '')" reverse_target="$(uci_get main.reverse_target || echo '')" ssl_insecure="$(uci_get main.ssl_insecure || echo 0)" anticache="$(uci_get main.anticache || echo 0)" anticomp="$(uci_get main.anticomp || echo 0)" flow_detail="$(uci_get main.flow_detail || echo 1)" haproxy_backend="0" fi # Set derived paths LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" LXC_CONFIG="$LXC_PATH/$LXC_NAME/config" # Convert memory limit to bytes for cgroup2 case "$memory_limit" in *M) memory_limit_bytes="${memory_limit%M}000000" ;; *G) memory_limit_bytes="${memory_limit%G}000000000" ;; *) memory_limit_bytes="$memory_limit" ;; esac } # Load configuration with defaults (legacy, calls load_instance_config) load_config() { load_instance_config "$INSTANCE" # WAN protection mode settings wan_protection_enabled="$(uci_get wan_protection.enabled || echo 0)" wan_interface="$(uci_get wan_protection.wan_interface || echo wan)" wan_http_port="$(uci_get wan_protection.wan_http_port || echo 80)" wan_https_port="$(uci_get wan_protection.wan_https_port || echo 443)" crowdsec_feed="$(uci_get wan_protection.crowdsec_feed || echo 1)" block_bots="$(uci_get wan_protection.block_bots || echo 0)" rate_limit="$(uci_get wan_protection.rate_limit || echo 0)" # Transparent mode settings (LAN) transparent_enabled="$(uci_get transparent.enabled || echo 0)" transparent_iface="$(uci_get transparent.interface || echo br-lan)" redirect_http="$(uci_get transparent.redirect_http || echo 1)" redirect_https="$(uci_get transparent.redirect_https || echo 1)" http_port="$(uci_get transparent.http_port || echo 80)" https_port="$(uci_get transparent.https_port || echo 443)" # DPI mirror settings dpi_mirror_enabled="$(uci_get dpi_mirror.enabled || echo 0)" dpi_interface="$(uci_get dpi_mirror.dpi_interface || echo br-lan)" mirror_wan="$(uci_get dpi_mirror.mirror_wan || echo 0)" mirror_lan="$(uci_get dpi_mirror.mirror_lan || echo 0)" # Whitelist settings whitelist_enabled="$(uci_get whitelist.enabled || echo 1)" # Filtering settings filtering_enabled="$(uci_get filtering.enabled || echo 0)" log_requests="$(uci_get filtering.log_requests || echo 1)" filter_cdn="$(uci_get filtering.filter_cdn || echo 0)" filter_media="$(uci_get filtering.filter_media || echo 0)" block_ads="$(uci_get filtering.block_ads || echo 0)" addon_script="$(uci_get filtering.addon_script || echo /data/addons/secubox_analytics.py)" # HAProxy router settings # Check instance-specific override first, then global local inst_haproxy_enabled="$(uci_get ${INSTANCE}.haproxy_router_enabled 2>/dev/null)" if [ -n "$inst_haproxy_enabled" ]; then haproxy_router_enabled="$inst_haproxy_enabled" else haproxy_router_enabled="$(uci_get haproxy_router.enabled || echo 0)" fi haproxy_listen_port="$(uci_get haproxy_router.listen_port || echo 8889)" haproxy_threat_detection="$(uci_get haproxy_router.threat_detection || echo 1)" haproxy_routes_file="$(uci_get haproxy_router.routes_file || echo /srv/mitmproxy/haproxy-routes.json)" } ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } has_lxc() { command -v lxc-start >/dev/null 2>&1 && \ command -v lxc-stop >/dev/null 2>&1 } has_nft() { command -v nft >/dev/null 2>&1 } # Ensure required packages are installed ensure_packages() { require_root for pkg in "$@"; do if ! opkg list-installed | grep -q "^$pkg "; then if [ "$OPKG_UPDATED" -eq 0 ]; then opkg update || return 1 OPKG_UPDATED=1 fi opkg install "$pkg" || return 1 fi done } # ============================================================================= # NFTABLES TRANSPARENT MODE FUNCTIONS # ============================================================================= nft_setup() { load_config require_root if ! has_nft; then log_error "nftables not available" return 1 fi if [ "$mode" != "transparent" ]; then log_warn "Proxy mode is '$mode', not 'transparent'. Firewall rules not needed." return 0 fi # Check if LAN transparent mode is enabled if [ "$transparent_enabled" != "1" ]; then log_warn "LAN transparent mode is disabled. Enable with: uci set mitmproxy.transparent.enabled='1'" log_warn "Note: HTTPS interception requires clients to trust the mitmproxy CA certificate." return 0 fi log_info "Setting up nftables for LAN transparent proxy..." log_warn "Warning: HTTPS sites may show certificate errors until clients trust the CA." # Enable IP forwarding (required for transparent proxying) log_info "Enabling IP forwarding..." sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null 2>&1 # Create mitmproxy table nft add table inet $NFT_TABLE 2>/dev/null || true # Create chains nft add chain inet $NFT_TABLE prerouting { type nat hook prerouting priority -100 \; } 2>/dev/null || true nft add chain inet $NFT_TABLE output { type nat hook output priority -100 \; } 2>/dev/null || true # Create bypass set for whitelisted IPs nft add set inet $NFT_TABLE bypass_ipv4 { type ipv4_addr \; flags interval \; } 2>/dev/null || true nft add set inet $NFT_TABLE bypass_ipv6 { type ipv6_addr \; flags interval \; } 2>/dev/null || true # Load whitelist IPs into bypass set if [ "$whitelist_enabled" = "1" ]; then local bypass_ips=$(uci_get_list whitelist.bypass_ip 2>/dev/null) for ip in $bypass_ips; do case "$ip" in *:*) nft add element inet $NFT_TABLE bypass_ipv6 { $ip } 2>/dev/null || true ;; *) nft add element inet $NFT_TABLE bypass_ipv4 { $ip } 2>/dev/null || true ;; esac done log_info "Loaded whitelist bypass IPs" fi # Get interface index if specified local iif_match="" if [ -n "$transparent_iface" ]; then iif_match="iifname \"$transparent_iface\"" fi # Flush existing rules in our chains nft flush chain inet $NFT_TABLE prerouting 2>/dev/null || true nft flush chain inet $NFT_TABLE output 2>/dev/null || true # Add bypass rules first (before redirect) nft add rule inet $NFT_TABLE prerouting ip daddr @bypass_ipv4 return 2>/dev/null || true nft add rule inet $NFT_TABLE prerouting ip6 daddr @bypass_ipv6 return 2>/dev/null || true # Don't intercept traffic destined for the router itself (local services) local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1") nft add rule inet $NFT_TABLE prerouting ip daddr "$router_ip" return 2>/dev/null || true # Redirect HTTP traffic if [ "$redirect_http" = "1" ]; then if [ -n "$iif_match" ]; then nft add rule inet $NFT_TABLE prerouting $iif_match tcp dport $http_port redirect to :$proxy_port else nft add rule inet $NFT_TABLE prerouting tcp dport $http_port redirect to :$proxy_port fi log_info "HTTP redirect: port $http_port -> $proxy_port" fi # Redirect HTTPS traffic if [ "$redirect_https" = "1" ]; then if [ -n "$iif_match" ]; then nft add rule inet $NFT_TABLE prerouting $iif_match tcp dport $https_port redirect to :$proxy_port else nft add rule inet $NFT_TABLE prerouting tcp dport $https_port redirect to :$proxy_port fi log_info "HTTPS redirect: port $https_port -> $proxy_port" fi log_info "nftables transparent mode rules applied" log_info "Table: inet $NFT_TABLE" } nft_teardown() { require_root if ! has_nft; then return 0 fi log_info "Removing nftables transparent mode rules..." # Delete the entire table (removes all chains and rules) nft delete table inet $NFT_TABLE 2>/dev/null || true log_info "nftables rules removed" } nft_status() { if ! has_nft; then echo "nftables not available" return 1 fi echo "=== mitmproxy nftables rules ===" if nft list table inet $NFT_TABLE 2>/dev/null; then echo "" echo "Bypass IPv4 set:" nft list set inet $NFT_TABLE bypass_ipv4 2>/dev/null || echo " (empty or not created)" echo "" echo "Bypass IPv6 set:" nft list set inet $NFT_TABLE bypass_ipv6 2>/dev/null || echo " (empty or not created)" else echo "No mitmproxy LAN transparent rules configured" fi echo "" echo "=== WAN protection rules ===" if nft list table inet $NFT_TABLE_WAN 2>/dev/null; then echo "WAN protection mode active" else echo "No WAN protection rules configured" fi } # ============================================================================= # WAN PROTECTION MODE FUNCTIONS # ============================================================================= # Get the actual network interface name from UCI interface name get_wan_device() { local iface="$1" local device="" # Try to get device from network config device=$(uci -q get network.${iface}.device) [ -n "$device" ] && { echo "$device"; return 0; } # Try ifname (older OpenWrt) device=$(uci -q get network.${iface}.ifname) [ -n "$device" ] && { echo "$device"; return 0; } # Fallback: check if interface exists directly if ip link show "$iface" >/dev/null 2>&1; then echo "$iface" return 0 fi # Common WAN interface names for dev in eth1 eth0.2 wan pppoe-wan; do if ip link show "$dev" >/dev/null 2>&1; then echo "$dev" return 0 fi done return 1 } nft_wan_setup() { load_config require_root if ! has_nft; then log_error "nftables not available" return 1 fi if [ "$wan_protection_enabled" != "1" ]; then log_warn "WAN protection mode is disabled. Enable with: uci set mitmproxy.wan_protection.enabled='1'" return 0 fi log_info "Setting up nftables for WAN protection mode..." # Get actual WAN device name local wan_dev=$(get_wan_device "$wan_interface") if [ -z "$wan_dev" ]; then log_error "Could not determine WAN device for interface '$wan_interface'" return 1 fi log_info "WAN device: $wan_dev (from interface: $wan_interface)" # Create WAN protection table nft add table inet $NFT_TABLE_WAN 2>/dev/null || true # Create prerouting chain for incoming WAN traffic nft add chain inet $NFT_TABLE_WAN prerouting { type nat hook prerouting priority -100 \; } 2>/dev/null || true # Flush existing rules nft flush chain inet $NFT_TABLE_WAN prerouting 2>/dev/null || true # Don't redirect traffic destined to router itself (management) local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1") nft add rule inet $NFT_TABLE_WAN prerouting ip daddr "$router_ip" return 2>/dev/null || true # Skip private/local destinations (not exposed to WAN) nft add rule inet $NFT_TABLE_WAN prerouting ip daddr 10.0.0.0/8 return 2>/dev/null || true nft add rule inet $NFT_TABLE_WAN prerouting ip daddr 172.16.0.0/12 return 2>/dev/null || true nft add rule inet $NFT_TABLE_WAN prerouting ip daddr 192.168.0.0/16 return 2>/dev/null || true nft add rule inet $NFT_TABLE_WAN prerouting ip daddr 127.0.0.0/8 return 2>/dev/null || true # Redirect WAN incoming HTTP to mitmproxy if [ "$wan_http_port" != "0" ]; then nft add rule inet $NFT_TABLE_WAN prerouting iifname "$wan_dev" tcp dport "$wan_http_port" redirect to :$proxy_port log_info "WAN HTTP redirect: $wan_dev:$wan_http_port -> mitmproxy:$proxy_port" fi # Redirect WAN incoming HTTPS to mitmproxy if [ "$wan_https_port" != "0" ]; then nft add rule inet $NFT_TABLE_WAN prerouting iifname "$wan_dev" tcp dport "$wan_https_port" redirect to :$proxy_port log_info "WAN HTTPS redirect: $wan_dev:$wan_https_port -> mitmproxy:$proxy_port" fi # Optional: Add rate limiting if configured if [ "$rate_limit" -gt 0 ] 2>/dev/null; then log_info "Rate limiting: $rate_limit requests/minute per IP" # Note: Rate limiting in nftables requires meter/limit # This is a basic implementation - can be enhanced nft add chain inet $NFT_TABLE_WAN ratelimit 2>/dev/null || true fi log_info "WAN protection mode rules applied" log_info "Table: inet $NFT_TABLE_WAN" log_info "" log_info "Incoming WAN traffic on ports $wan_http_port/$wan_https_port will be" log_info "inspected by mitmproxy for threats before reaching backend services." } nft_wan_teardown() { require_root if ! has_nft; then return 0 fi log_info "Removing WAN protection mode rules..." # Delete the WAN protection table nft delete table inet $NFT_TABLE_WAN 2>/dev/null || true log_info "WAN protection rules removed" } # ============================================================================= # LXC CONTAINER FUNCTIONS # ============================================================================= lxc_check_prereqs() { log_info "Checking LXC prerequisites..." ensure_packages lxc lxc-common lxc-attach lxc-start lxc-stop lxc-destroy || return 1 if [ ! -d /sys/fs/cgroup ]; then log_error "cgroups not mounted at /sys/fs/cgroup" return 1 fi log_info "LXC ready" } lxc_create_rootfs() { load_config if [ -d "$LXC_ROOTFS" ] && [ -x "$LXC_ROOTFS/usr/bin/mitmproxy" ]; then log_info "LXC rootfs already exists with mitmproxy" return 0 fi log_info "Creating LXC rootfs for mitmproxy..." ensure_dir "$LXC_PATH/$LXC_NAME" lxc_create_docker_rootfs || return 1 lxc_create_config || return 1 log_info "LXC rootfs created successfully" } lxc_create_docker_rootfs() { local rootfs="$LXC_ROOTFS" local image="mitmproxy/mitmproxy" local tag="latest" local registry="registry-1.docker.io" local arch # Detect architecture for Docker manifest case "$(uname -m)" in x86_64) arch="amd64" ;; aarch64) arch="arm64" ;; armv7l) arch="arm" ;; *) arch="amd64" ;; esac log_info "Extracting mitmproxy Docker image ($arch)..." ensure_dir "$rootfs" # Get Docker Hub token local token=$(wget -q -O - "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" | jsonfilter -e '@.token') [ -z "$token" ] && { log_error "Failed to get Docker Hub token"; return 1; } # Get manifest list local manifest=$(wget -q -O - --header="Authorization: Bearer $token" \ --header="Accept: application/vnd.docker.distribution.manifest.list.v2+json" \ "https://$registry/v2/$image/manifests/$tag") # Find digest for our architecture local digest=$(echo "$manifest" | jsonfilter -e "@.manifests[@.platform.architecture='$arch'].digest") [ -z "$digest" ] && { log_error "No manifest found for $arch"; return 1; } # Get image manifest local img_manifest=$(wget -q -O - --header="Authorization: Bearer $token" \ --header="Accept: application/vnd.docker.distribution.manifest.v2+json" \ "https://$registry/v2/$image/manifests/$digest") # Extract layers and download them log_info "Downloading and extracting layers..." local layers=$(echo "$img_manifest" | jsonfilter -e '@.layers[*].digest') for layer_digest in $layers; do log_info " Layer: ${layer_digest:7:12}..." wget -q -O - --header="Authorization: Bearer $token" \ "https://$registry/v2/$image/blobs/$layer_digest" | \ tar xzf - -C "$rootfs" 2>&1 | grep -v "Cannot change ownership" || true done # Configure container echo "nameserver 8.8.8.8" > "$rootfs/etc/resolv.conf" mkdir -p "$rootfs/data" "$rootfs/var/log/mitmproxy" "$rootfs/etc/mitmproxy/addons" "$rootfs/tmp" # Ensure proper shell setup - Docker image is Python slim (Debian-based) # python:slim uses dash as /bin/sh but symlinks may not extract properly log_info "Checking shell availability..." ls -la "$rootfs/bin/" 2>/dev/null | head -20 || true # Ensure /bin/sh exists - critical for script execution if [ ! -x "$rootfs/bin/sh" ]; then log_warn "/bin/sh not found or not executable, attempting to fix..." # Check for available shells if [ -x "$rootfs/bin/dash" ]; then ln -sf dash "$rootfs/bin/sh" log_info "Created /bin/sh -> dash" elif [ -x "$rootfs/bin/bash" ]; then ln -sf bash "$rootfs/bin/sh" log_info "Created /bin/sh -> bash" elif [ -x "$rootfs/usr/bin/dash" ]; then mkdir -p "$rootfs/bin" ln -sf /usr/bin/dash "$rootfs/bin/sh" log_info "Created /bin/sh -> /usr/bin/dash" else # Last resort: copy busybox sh from host if available log_error "No shell found in container! Container may not start properly." fi fi # Verify shell is now available if [ -x "$rootfs/bin/sh" ]; then log_info "Shell ready: $(ls -la "$rootfs/bin/sh")" else log_error "Shell setup failed!" fi # Create startup script for mitmweb (POSIX-compliant for dash) cat > "$rootfs/opt/start-mitmproxy.sh" << 'START' #!/bin/sh export PATH="/usr/local/bin:/usr/bin:/bin:$PATH" export PYTHONUNBUFFERED=1 cd /data # Read environment variables MODE="${MITMPROXY_MODE:-regular}" PROXY_PORT="${MITMPROXY_PROXY_PORT:-8888}" WEB_PORT="${MITMPROXY_WEB_PORT:-8081}" WEB_HOST="${MITMPROXY_WEB_HOST:-0.0.0.0}" ADDON_SCRIPT="${MITMPROXY_ADDON_SCRIPT:-}" FILTERING_ENABLED="${MITMPROXY_FILTERING_ENABLED:-0}" # HAProxy router mode HAPROXY_ROUTER_ENABLED="${MITMPROXY_HAPROXY_ROUTER_ENABLED:-0}" HAPROXY_LISTEN_PORT="${MITMPROXY_HAPROXY_LISTEN_PORT:-8889}" HAPROXY_ROUTES_FILE="${MITMPROXY_HAPROXY_ROUTES_FILE:-/data/haproxy-routes.json}" # Build args ARGS="--listen-host 0.0.0.0 --listen-port $PROXY_PORT --set confdir=/data" ARGS="$ARGS --web-host $WEB_HOST --web-port $WEB_PORT --no-web-open-browser" # Configure mode case "$MODE" in transparent) ARGS="$ARGS --mode transparent" ;; upstream) [ -n "$UPSTREAM_PROXY" ] && ARGS="$ARGS --mode upstream:$UPSTREAM_PROXY" ;; reverse) [ -n "$REVERSE_TARGET" ] && ARGS="$ARGS --mode reverse:$REVERSE_TARGET" ;; esac # HAProxy router mode: add additional listening port for HAProxy traffic if [ "$HAPROXY_ROUTER_ENABLED" = "1" ]; then ARGS="$ARGS --mode regular@$HAPROXY_LISTEN_PORT" echo "HAProxy router mode: listening on port $HAPROXY_LISTEN_PORT" # Load HAProxy router addon if [ -f "/data/addons/haproxy_router.py" ]; then ARGS="$ARGS -s /data/addons/haproxy_router.py" echo "Loading HAProxy router addon" fi fi [ "$SSL_INSECURE" = "1" ] && ARGS="$ARGS --ssl-insecure" [ "$ANTICACHE" = "1" ] && ARGS="$ARGS --anticache" [ "$ANTICOMP" = "1" ] && ARGS="$ARGS --anticomp" # Load analytics addon if filtering is enabled if [ "$FILTERING_ENABLED" = "1" ] && [ -n "$ADDON_SCRIPT" ] && [ -f "$ADDON_SCRIPT" ]; then ARGS="$ARGS -s $ADDON_SCRIPT" echo "Loading addon: $ADDON_SCRIPT" fi rm -f /data/.mitmproxy_token /tmp/mitmweb.log echo "Starting mitmweb..." echo "Command: mitmweb $ARGS" # Start mitmweb in background, output to log file /usr/local/bin/mitmweb $ARGS 2>&1 | tee /tmp/mitmweb.log & MITMWEB_PID=$! # Wait for token to appear in log (with timeout) echo "Waiting for authentication token..." ATTEMPTS=0 MAX_ATTEMPTS=30 while [ $ATTEMPTS -lt $MAX_ATTEMPTS ]; do sleep 1 ATTEMPTS=$((ATTEMPTS + 1)) if [ -f /tmp/mitmweb.log ]; then # Extract token from log - mitmweb outputs: "Web server listening at http://x.x.x.x:8081/?token=XXXXX" # Token can be alphanumeric, not just hex TOKEN=$(grep -o 'token=[a-zA-Z0-9_-]*' /tmp/mitmweb.log 2>/dev/null | head -1 | cut -d= -f2) if [ -n "$TOKEN" ]; then echo "$TOKEN" > /data/.mitmproxy_token chmod 644 /data/.mitmproxy_token echo "Token captured: $(echo "$TOKEN" | cut -c1-8)..." echo "Web UI: http://$WEB_HOST:$WEB_PORT/?token=$TOKEN" break fi fi done if [ ! -f /data/.mitmproxy_token ]; then echo "Warning: Could not capture authentication token after ${MAX_ATTEMPTS}s" echo "Check /tmp/mitmweb.log for details" fi # Wait for mitmweb process to keep container running wait $MITMWEB_PID START chmod +x "$rootfs/opt/start-mitmproxy.sh" log_info "mitmproxy Docker image extracted successfully" # Install the SecuBox filter addon install_addon_script } install_addon_script() { load_config ensure_dir "$ADDON_PATH" ensure_dir "$LXC_ROOTFS/etc/mitmproxy/addons" # Create the SecuBox filter addon cat > "$ADDON_PATH/secubox_filter.py" << 'ADDON' """ SecuBox mitmproxy Filter Addon CDN/MediaFlow filtering and request logging """ import json import os import re from datetime import datetime from mitmproxy import http, ctx # CDN domains to track CDN_DOMAINS = [ r'\.cloudflare\.com$', r'\.cloudflareinsights\.com$', r'\.akamai\.net$', r'\.akamaized\.net$', r'\.fastly\.net$', r'\.cloudfront\.net$', r'\.azureedge\.net$', r'\.jsdelivr\.net$', r'\.unpkg\.com$', r'\.cdnjs\.cloudflare\.com$', ] # Media streaming domains MEDIA_DOMAINS = [ r'\.googlevideo\.com$', r'\.youtube\.com$', r'\.ytimg\.com$', r'\.netflix\.com$', r'\.nflxvideo\.net$', r'\.spotify\.com$', r'\.scdn\.co$', r'\.twitch\.tv$', r'\.ttvnw\.net$', ] # Ad/Tracker domains to block AD_DOMAINS = [ r'\.doubleclick\.net$', r'\.googlesyndication\.com$', r'\.googleadservices\.com$', r'\.facebook\.net$', r'\.analytics\.google\.com$', r'\.google-analytics\.com$', r'\.hotjar\.com$', r'\.segment\.io$', r'\.mixpanel\.com$', r'\.amplitude\.com$', ] class SecuBoxFilter: def __init__(self): self.log_file = os.environ.get('MITMPROXY_LOG_FILE', '/data/requests.log') self.filter_cdn = os.environ.get('MITMPROXY_FILTER_CDN', '0') == '1' self.filter_media = os.environ.get('MITMPROXY_FILTER_MEDIA', '0') == '1' self.block_ads = os.environ.get('MITMPROXY_BLOCK_ADS', '0') == '1' self.log_requests = os.environ.get('MITMPROXY_LOG_REQUESTS', '1') == '1' ctx.log.info(f"SecuBox Filter initialized") ctx.log.info(f" Log requests: {self.log_requests}") ctx.log.info(f" Filter CDN: {self.filter_cdn}") ctx.log.info(f" Filter Media: {self.filter_media}") ctx.log.info(f" Block Ads: {self.block_ads}") def _match_domain(self, host, patterns): """Check if host matches any pattern""" for pattern in patterns: if re.search(pattern, host, re.IGNORECASE): return True return False def _log_request(self, flow: http.HTTPFlow, category: str = "normal"): """Log request to JSON file""" if not self.log_requests: return try: entry = { "timestamp": datetime.now().isoformat(), "category": category, "request": { "method": flow.request.method, "host": flow.request.host, "port": flow.request.port, "path": flow.request.path, "scheme": flow.request.scheme, }, } if flow.response: entry["response"] = { "status_code": flow.response.status_code, "content_type": flow.response.headers.get("content-type", ""), "content_length": len(flow.response.content) if flow.response.content else 0, } with open(self.log_file, 'a') as f: f.write(json.dumps(entry) + '\n') except Exception as e: ctx.log.error(f"Failed to log request: {e}") def request(self, flow: http.HTTPFlow): """Process incoming request""" host = flow.request.host # Check for ad/tracker domains if self.block_ads and self._match_domain(host, AD_DOMAINS): ctx.log.info(f"Blocked ad/tracker: {host}") flow.response = http.Response.make( 403, b"Blocked by SecuBox", {"Content-Type": "text/plain"} ) self._log_request(flow, "blocked_ad") return # Track CDN requests if self._match_domain(host, CDN_DOMAINS): self._log_request(flow, "cdn") if self.filter_cdn: ctx.log.info(f"CDN request: {host}{flow.request.path[:50]}") return # Track media requests if self._match_domain(host, MEDIA_DOMAINS): self._log_request(flow, "media") if self.filter_media: ctx.log.info(f"Media request: {host}{flow.request.path[:50]}") return # Log normal request self._log_request(flow, "normal") def response(self, flow: http.HTTPFlow): """Process response - update log entry if needed""" pass addons = [SecuBoxFilter()] ADDON # Copy to container rootfs cp "$ADDON_PATH/secubox_filter.py" "$LXC_ROOTFS/etc/mitmproxy/addons/" 2>/dev/null || true log_info "Addon script installed: $ADDON_PATH/secubox_filter.py" } lxc_create_config() { load_config # Build addon path for container local container_addon="" if [ "$filtering_enabled" = "1" ] && [ -f "$LXC_ROOTFS$addon_script" ]; then container_addon="$addon_script" fi cat > "$LXC_CONFIG" << EOF # mitmproxy LXC Configuration lxc.uts.name = $LXC_NAME # Root filesystem lxc.rootfs.path = dir:$LXC_ROOTFS # Network - use host network for simplicity lxc.net.0.type = none # Mounts lxc.mount.auto = proc:mixed sys:ro cgroup:mixed lxc.mount.entry = $data_path data none bind,create=dir 0 0 lxc.mount.entry = $ADDON_PATH etc/mitmproxy/addons none bind,create=dir 0 0 # Environment variables for configuration lxc.environment = MITMPROXY_MODE=$mode lxc.environment = MITMPROXY_PROXY_PORT=$proxy_port lxc.environment = MITMPROXY_WEB_PORT=$web_port lxc.environment = MITMPROXY_WEB_HOST=$web_host lxc.environment = UPSTREAM_PROXY=$upstream_proxy lxc.environment = REVERSE_TARGET=$reverse_target lxc.environment = SSL_INSECURE=$ssl_insecure lxc.environment = ANTICACHE=$anticache lxc.environment = ANTICOMP=$anticomp lxc.environment = FLOW_DETAIL=$flow_detail lxc.environment = MITMPROXY_FILTERING_ENABLED=$filtering_enabled lxc.environment = MITMPROXY_ADDON_SCRIPT=$addon_script lxc.environment = MITMPROXY_LOG_REQUESTS=$log_requests lxc.environment = MITMPROXY_FILTER_CDN=$filter_cdn lxc.environment = MITMPROXY_FILTER_MEDIA=$filter_media lxc.environment = MITMPROXY_BLOCK_ADS=$block_ads lxc.environment = MITMPROXY_LOG_FILE=/data/requests.log # HAProxy router mode lxc.environment = MITMPROXY_HAPROXY_ROUTER_ENABLED=$haproxy_router_enabled lxc.environment = MITMPROXY_HAPROXY_LISTEN_PORT=$haproxy_listen_port lxc.environment = MITMPROXY_HAPROXY_ROUTES_FILE=/data/haproxy-routes.json # Capabilities - drop dangerous ones (sys_admin needed for some ops) lxc.cap.drop = sys_module mac_admin mac_override sys_time # Disable seccomp for cgroup v2 compatibility lxc.seccomp.profile = # Console/TTY (cgroup v2 compatible) lxc.tty.max = 0 lxc.pty.max = 256 # cgroup v2 limits (NOT v1 syntax) lxc.cgroup2.memory.max = $memory_limit_bytes # Init lxc.init.cmd = /opt/start-mitmproxy.sh EOF log_info "LXC config created at $LXC_CONFIG" } lxc_stop() { if lxc-info -n "$LXC_NAME" >/dev/null 2>&1; then lxc-stop -n "$LXC_NAME" -k >/dev/null 2>&1 || true fi } lxc_run() { load_config lxc_stop if [ ! -f "$LXC_CONFIG" ]; then log_error "LXC not configured. Run 'mitmproxyctl install' first." return 1 fi # Regenerate config to pick up any UCI changes lxc_create_config # Ensure mount points exist ensure_dir "$data_path" ensure_dir "$ADDON_PATH" # Write autoban config for container write_autoban_config # Setup LAN transparent firewall rules if both mode=transparent AND transparent.enabled=1 if [ "$mode" = "transparent" ] && [ "$transparent_enabled" = "1" ]; then nft_setup fi # Setup WAN protection rules if enabled if [ "$wan_protection_enabled" = "1" ]; then nft_wan_setup fi log_info "Starting mitmproxy LXC container..." log_info "Mode: $mode" log_info "Web interface: http://0.0.0.0:$web_port" log_info "Proxy port: $proxy_port" [ "$filtering_enabled" = "1" ] && log_info "Filtering: enabled" [ "$wan_protection_enabled" = "1" ] && log_info "WAN Protection: enabled (interface: $wan_interface)" exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG" } lxc_status() { local instance="${1:-}" if [ -n "$instance" ]; then load_instance_config "$instance" || return 1 echo "=== mitmproxy Instance: $instance ===" else load_config echo "=== mitmproxy Status ===" fi echo "" if lxc-info -n "$LXC_NAME" >/dev/null 2>&1; then lxc-info -n "$LXC_NAME" else echo "LXC container '$LXC_NAME' not found or not configured" fi echo "" echo "=== Configuration ===" echo "Container: $LXC_NAME" echo "Mode: $mode" echo "Proxy port: $proxy_port" echo "Web port: $web_port" echo "Data path: $data_path" echo "Filtering: $([ "$filtering_enabled" = "1" ] && echo "enabled" || echo "disabled")" [ "$haproxy_backend" = "1" ] && echo "HAProxy Backend: yes" # Only show WAN protection for non-instance mode if [ -z "$instance" ]; then echo "" echo "=== WAN Protection ===" echo "Enabled: $([ "$wan_protection_enabled" = "1" ] && echo "yes" || echo "no")" if [ "$wan_protection_enabled" = "1" ]; then echo "WAN Interface: $wan_interface" echo "HTTP Port: $wan_http_port" echo "HTTPS Port: $wan_https_port" echo "CrowdSec Feed: $([ "$crowdsec_feed" = "1" ] && echo "yes" || echo "no")" echo "Block Bots: $([ "$block_bots" = "1" ] && echo "yes" || echo "no")" fi if [ "$mode" = "transparent" ] || [ "$wan_protection_enabled" = "1" ]; then echo "" nft_status fi fi } cmd_status() { local instance="${1:-}" if [ -n "$instance" ]; then lxc_status "$instance" else # Show all instances echo "=== mitmproxy Instances ===" cmd_list_instances echo "" # Show legacy status load_config lxc_status fi } lxc_logs() { load_config local logfile="$LXC_ROOTFS/var/log/mitmproxy/mitmproxy.log" if lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; then # For mitmweb, logs go to stderr which procd captures if [ "$1" = "-f" ]; then logread -f -e mitmproxy else logread -e mitmproxy | tail -100 fi elif [ -f "$logfile" ]; then if [ "$1" = "-f" ]; then tail -f "$logfile" else tail -100 "$logfile" fi else log_warn "Container not running. Try: logread -e mitmproxy" fi } lxc_shell() { lxc-attach -n "$LXC_NAME" -- /bin/sh } lxc_destroy() { lxc_stop if [ -d "$LXC_PATH/$LXC_NAME" ]; then rm -rf "$LXC_PATH/$LXC_NAME" log_info "LXC container destroyed" fi } # ============================================================================= # COMMANDS # ============================================================================= cmd_install() { require_root load_config if ! has_lxc; then log_error "LXC not available. Install lxc packages first." exit 1 fi log_info "Installing mitmproxy..." # Create directories ensure_dir "$data_path" ensure_dir "$ADDON_PATH" lxc_check_prereqs || exit 1 lxc_create_rootfs || exit 1 uci_set main.enabled '1' /etc/init.d/mitmproxy enable log_info "mitmproxy installed." log_info "Start with: /etc/init.d/mitmproxy start" log_info "Web interface: http://:$web_port" log_info "Proxy port: $proxy_port" } cmd_check() { load_config log_info "Checking prerequisites..." if has_lxc; then log_info "LXC: available" lxc_check_prereqs else log_warn "LXC: not available" fi if has_nft; then log_info "nftables: available" else log_warn "nftables: not available (needed for transparent mode)" fi } cmd_update() { require_root load_config log_info "Updating mitmproxy..." lxc_destroy lxc_create_rootfs || exit 1 if /etc/init.d/mitmproxy enabled >/dev/null 2>&1; then /etc/init.d/mitmproxy restart else log_info "Update complete. Restart manually to apply." fi } cmd_status() { lxc_status } cmd_logs() { lxc_logs "$@" } cmd_shell() { lxc_shell } cmd_cert() { load_config local cert_path="$data_path/mitmproxy-ca-cert.pem" if [ -f "$cert_path" ]; then log_info "CA Certificate location: $cert_path" log_info "" log_info "To install on clients:" log_info " 1. Download from: http://:$web_port/cert/pem" log_info " 2. Or copy: $cert_path" log_info "" log_info "Certificate info:" openssl x509 -in "$cert_path" -noout -subject -dates 2>/dev/null || \ cat "$cert_path" else log_warn "CA certificate not yet generated." log_info "Start mitmproxy first: /etc/init.d/mitmproxy start" log_info "Then access: http://:$web_port/cert" fi } cmd_firewall_setup() { nft_setup } cmd_firewall_clear() { nft_teardown } cmd_service_run() { local instance="${1:-}" require_root if [ -n "$instance" ]; then load_instance_config "$instance" || exit 1 else load_config fi if ! has_lxc; then log_error "LXC not available" exit 1 fi log_info "Starting mitmproxy instance: ${INSTANCE:-default} (container: $LXC_NAME)" lxc_check_prereqs || exit 1 lxc_run } cmd_service_stop() { local instance="${1:-}" require_root if [ -n "$instance" ]; then load_instance_config "$instance" || exit 1 else load_config fi log_info "Stopping mitmproxy instance: ${INSTANCE:-default} (container: $LXC_NAME)" # Remove firewall rules if [ "$mode" = "transparent" ]; then nft_teardown fi # Remove WAN protection rules nft_wan_teardown 2>/dev/null || true lxc_stop } cmd_wan_setup() { nft_wan_setup } cmd_wan_clear() { nft_wan_teardown } # ============================================================================= # ROUTE MANAGEMENT # ============================================================================= cmd_route_list() { load_config local routes_file="$data_path/haproxy-routes.json" if [ ! -f "$routes_file" ]; then log_warn "Routes file not found: $routes_file" log_info "Run 'mitmproxyctl sync-routes' to generate" return 1 fi echo "Mitmproxy Routes ($routes_file):" echo "=================================" # Parse and display routes cat "$routes_file" | grep -E '^\s*"' | while read line; do # Extract domain, ip, port from JSON line local domain=$(echo "$line" | sed 's/.*"\([^"]*\)".*/\1/') local target=$(echo "$line" | sed 's/.*\[\([^]]*\)\].*/\1/' | tr -d '"' | tr ',' ':') echo " $domain -> $target" done local count=$(grep -c '"' "$routes_file" 2>/dev/null || echo 0) count=$((count / 2)) echo "" echo "Total: $count routes" } cmd_route_add() { require_root load_config local domain="$1" local ip="$2" local port="$3" if [ -z "$domain" ] || [ -z "$ip" ] || [ -z "$port" ]; then echo "Usage: mitmproxyctl route add " >&2 echo "Example: mitmproxyctl route add myapp.gk2.secubox.in 127.0.0.1 8080" >&2 return 1 fi local routes_file="$data_path/haproxy-routes.json" # Check if routes file exists if [ ! -f "$routes_file" ]; then echo "{}" > "$routes_file" fi # Check if route already exists if grep -q "\"$domain\"" "$routes_file"; then log_warn "Route for $domain already exists, updating..." # Remove existing entry local tmp=$(mktemp) grep -v "\"$domain\"" "$routes_file" | sed 's/,$//' > "$tmp" # Fix JSON (remove trailing comma before }) sed -i 's/,\s*}/}/' "$tmp" mv "$tmp" "$routes_file" fi # Add new route local tmp=$(mktemp) # Insert before closing brace head -n -1 "$routes_file" > "$tmp" # Add comma if not empty if grep -q '"' "$tmp"; then echo "," >> "$tmp" fi printf ' "%s": ["%s", %s]\n' "$domain" "$ip" "$port" >> "$tmp" echo "}" >> "$tmp" mv "$tmp" "$routes_file" chmod 644 "$routes_file" log_info "Added route: $domain -> $ip:$port" # Sync to all instances sync_routes_to_instances "$routes_file" } cmd_route_remove() { require_root load_config local domain="$1" if [ -z "$domain" ]; then echo "Usage: mitmproxyctl route remove " >&2 return 1 fi local routes_file="$data_path/haproxy-routes.json" if [ ! -f "$routes_file" ]; then log_error "Routes file not found" return 1 fi if ! grep -q "\"$domain\"" "$routes_file"; then log_warn "Route for $domain not found" return 1 fi # Remove route local tmp=$(mktemp) grep -v "\"$domain\"" "$routes_file" > "$tmp" # Fix JSON (remove double commas and trailing comma before }) sed -i 's/,,/,/g; s/,\s*}/\n}/g; s/{\s*,/{/g' "$tmp" mv "$tmp" "$routes_file" chmod 644 "$routes_file" log_info "Removed route: $domain" # Sync to all instances sync_routes_to_instances "$routes_file" } cmd_route_check() { load_config local routes_file="$data_path/haproxy-routes.json" local missing=0 log_info "Checking for missing mitmproxy routes..." # Check all mitmproxy_inspector vhosts uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d'=' -f1 | cut -d'.' -f2 | while read vhost; do local domain=$(uci -q get haproxy.$vhost.domain) local backend=$(uci -q get haproxy.$vhost.backend) local enabled=$(uci -q get haproxy.$vhost.enabled) [ "$enabled" = "0" ] && continue [ "$backend" != "mitmproxy_inspector" ] && continue [ -z "$domain" ] && continue # Check if route exists if ! grep -q "\"$domain\"" "$routes_file" 2>/dev/null; then echo "MISSING: $domain" missing=$((missing + 1)) fi done if [ $missing -eq 0 ]; then log_info "All mitmproxy_inspector vhosts have routes configured" else log_warn "$missing vhosts are missing mitmproxy routes" log_info "Run 'mitmproxyctl sync-routes' to auto-generate, or add manually with 'mitmproxyctl route add'" fi } # Helper: Sync routes to all instance data paths sync_routes_to_instances() { local routes_file="$1" local instances=$(uci show mitmproxy 2>/dev/null | grep '=instance$' | sed 's/mitmproxy\.\([^=]*\)=instance/\1/') for inst in $instances; do local inst_data_path=$(uci -q get mitmproxy.${inst}.data_path) [ -z "$inst_data_path" ] && inst_data_path="/srv/mitmproxy-${inst}" if [ -d "$inst_data_path" ]; then cp "$routes_file" "$inst_data_path/haproxy-routes.json" log_info "Routes synced to $inst_data_path" fi done } # ============================================================================= # HAPROXY BACKEND INSPECTION # ============================================================================= # Helper: Add route to temp file (used by sync functions) add_route_entry() { local tmp_file="$1" local domain="$2" local ip="$3" local port="$4" local source="$5" local count_file="$6" local count=$(cat "$count_file" 2>/dev/null || echo 0) if [ $count -gt 0 ]; then echo "," >> "$tmp_file" fi count=$((count + 1)) echo "$count" > "$count_file" printf ' "%s": ["%s", %s]' "$domain" "$ip" "$port" >> "$tmp_file" log_info " $domain -> $ip:$port ($source)" } # Scan MetaBlogizer sites and add routes (writes to stdout as JSON fragments) sync_metablogizer_routes() { local added_file="$1" # Check if metablogizer is installed [ -x /usr/sbin/metablogizerctl ] || return 0 log_info "Scanning MetaBlogizer sites..." # Get all metablogizer sites local sites_file="/tmp/mb_sites.tmp" uci show metablogizer 2>/dev/null | grep "=site$" | cut -d'=' -f1 | cut -d'.' -f2 > "$sites_file" while read site; do [ -z "$site" ] && continue local domain=$(uci -q get metablogizer.$site.domain) local port=$(uci -q get metablogizer.$site.port) local enabled=$(uci -q get metablogizer.$site.enabled) [ "$enabled" != "1" ] && continue [ -z "$domain" ] && continue [ -z "$port" ] && continue # Check if already added if grep -q "^$domain$" "$added_file" 2>/dev/null; then continue fi echo "$domain" >> "$added_file" # Output JSON fragment echo "\"$domain\": [\"127.0.0.1\", $port]" log_info " $domain -> 127.0.0.1:$port (metablogizer)" done < "$sites_file" rm -f "$sites_file" } # Scan Streamlit instances and add routes (writes to stdout as JSON fragments) sync_streamlit_routes() { local added_file="$1" # Check if streamlit is installed [ -f /etc/config/streamlit ] || return 0 log_info "Scanning Streamlit instances..." # Get all streamlit instances local inst_file="/tmp/st_inst.tmp" uci show streamlit 2>/dev/null | grep "=instance$" | cut -d'=' -f1 | cut -d'.' -f2 > "$inst_file" while read inst; do [ -z "$inst" ] && continue local port=$(uci -q get streamlit.$inst.port) local enabled=$(uci -q get streamlit.$inst.enabled) [ "$enabled" != "1" ] && continue [ -z "$port" ] && continue # Check for direct domain mapping (inst.gk2.secubox.in pattern) local default_domain="${inst}.gk2.secubox.in" if uci show haproxy 2>/dev/null | grep -q "domain='$default_domain'"; then if ! grep -q "^$default_domain$" "$added_file" 2>/dev/null; then echo "$default_domain" >> "$added_file" echo "\"$default_domain\": [\"192.168.255.1\", $port]" log_info " $default_domain -> 192.168.255.1:$port (streamlit:$inst)" fi fi done < "$inst_file" rm -f "$inst_file" } # Check for missing routes in mitmproxy_inspector vhosts check_missing_routes() { local routes_file="$1" local missing=0 log_info "Checking for missing routes in mitmproxy_inspector vhosts..." uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d'=' -f1 | cut -d'.' -f2 | while read vhost; do local domain=$(uci -q get haproxy.$vhost.domain) local backend=$(uci -q get haproxy.$vhost.backend) [ "$backend" != "mitmproxy_inspector" ] && continue [ -z "$domain" ] && continue # Check if route exists if ! grep -q "\"$domain\"" "$routes_file" 2>/dev/null; then log_warn " MISSING ROUTE: $domain (vhost uses mitmproxy_inspector but no route defined)" missing=$((missing + 1)) fi done return $missing } cmd_sync_routes() { load_config log_info "Syncing HAProxy backends to mitmproxy routes..." local routes_file="$data_path/haproxy-routes.json" local fragments_file="/tmp/haproxy-routes-fragments.tmp" local added_file="/tmp/haproxy-routes.added" # Initialize > "$fragments_file" > "$added_file" # Get all vhosts - avoid subshell by using temp file local vhosts_file="/tmp/haproxy-vhosts.tmp" uci show haproxy 2>/dev/null | grep "=vhost" | cut -d'=' -f1 | cut -d'.' -f2 > "$vhosts_file" while read vhost; do [ -z "$vhost" ] && continue local domain=$(uci -q get haproxy.$vhost.domain) local backend=$(uci -q get haproxy.$vhost.backend) # If currently using mitmproxy_inspector, use the stored original backend if [ "$backend" = "mitmproxy_inspector" ]; then backend=$(uci -q get haproxy.$vhost.original_backend) fi # Skip fallback, luci, and mitmproxy backends case "$backend" in fallback|luci|luci_default|mitmproxy_inspector|"") continue ;; esac if [ -n "$domain" ] && [ -n "$backend" ]; then local ip="" local port="" # Method 1: Check for inline server field (old style) local server=$(uci -q get haproxy.$backend.server) if [ -n "$server" ]; then # Parse server spec: "name ip:port check [options]" local addr=$(echo "$server" | awk '{print $2}') ip=$(echo "$addr" | cut -d':' -f1) port=$(echo "$addr" | cut -d':' -f2) # Handle backends without explicit port [ "$ip" = "$port" ] && port="80" fi # Method 2: Check for separate server section (new style) if [ -z "$ip" ]; then # Find server section that references this backend local server_section=$(uci show haproxy 2>/dev/null | grep "\.backend='$backend'" | grep "=server" | head -1 | cut -d'=' -f1 | cut -d'.' -f2) if [ -z "$server_section" ]; then # Try pattern: backend_name_servername=server server_section=$(uci show haproxy 2>/dev/null | grep "^haproxy\.${backend}_.*=server" | head -1 | cut -d'=' -f1 | cut -d'.' -f2) fi if [ -n "$server_section" ]; then ip=$(uci -q get haproxy.$server_section.address) port=$(uci -q get haproxy.$server_section.port) fi fi # Only add route if we found valid ip:port if [ -n "$ip" ] && [ -n "$port" ]; then echo "$domain" >> "$added_file" echo "\"$domain\": [\"$ip\", $port]" >> "$fragments_file" log_info " $domain -> $ip:$port (haproxy:$backend)" else log_warn " $domain: could not resolve backend '$backend'" fi fi done < "$vhosts_file" rm -f "$vhosts_file" # Sync MetaBlogizer routes (appends to fragments_file) sync_metablogizer_routes "$added_file" >> "$fragments_file" # Sync Streamlit routes (appends to fragments_file) sync_streamlit_routes "$added_file" >> "$fragments_file" # Build final JSON from fragments local count=$(wc -l < "$fragments_file" | tr -d ' ') { echo "{" local first=1 while read line; do [ -z "$line" ] && continue if [ $first -eq 1 ]; then echo " $line" first=0 else echo ", $line" fi done < "$fragments_file" echo "}" } > "$routes_file" rm -f "$fragments_file" "$added_file" chmod 644 "$routes_file" log_info "Generated $routes_file with $count routes" # Copy routes to all instance data paths sync_routes_to_instances "$routes_file" # Check for missing routes and warn check_missing_routes "$routes_file" } cmd_haproxy_enable() { require_root load_config log_info "Enabling HAProxy backend inspection..." # 1. Enable HAProxy router in config uci set mitmproxy.haproxy_router.enabled='1' uci commit mitmproxy # 2. Sync routes from HAProxy cmd_sync_routes # 3. Create HAProxy backend for mitmproxy log_info "Configuring HAProxy backend 'mitmproxy_inspector'..." # Check if backend already exists if ! uci -q get haproxy.mitmproxy_inspector >/dev/null 2>&1; then uci set haproxy.mitmproxy_inspector=backend uci set haproxy.mitmproxy_inspector.server="mitmproxy 127.0.0.1:$haproxy_listen_port check" fi # 4. Store original backends and update vhosts to use mitmproxy log_info "Updating HAProxy vhosts to route through mitmproxy..." local updated=0 for vhost in $(uci show haproxy 2>/dev/null | grep "=vhost" | cut -d'=' -f1 | cut -d'.' -f2); do local current_backend=$(uci -q get haproxy.$vhost.backend) # Skip if already using mitmproxy or if it's the fallback if [ "$current_backend" = "mitmproxy_inspector" ] || [ "$current_backend" = "fallback" ]; then continue fi # Store original backend uci set haproxy.$vhost.original_backend="$current_backend" # Set to mitmproxy uci set haproxy.$vhost.backend="mitmproxy_inspector" updated=$((updated + 1)) local domain=$(uci -q get haproxy.$vhost.domain) log_info " $domain: $current_backend -> mitmproxy_inspector" done uci commit haproxy log_info "Updated $updated vhosts" # 5. Restart services log_info "Restarting services..." /etc/init.d/mitmproxy restart /etc/init.d/haproxy reload 2>/dev/null || /etc/init.d/haproxy restart 2>/dev/null log_info "" log_info "HAProxy backend inspection ENABLED" log_info "All vhost traffic now flows through mitmproxy for threat detection" log_info "View threats at: http:///cgi-bin/luci/admin/services/mitmproxy" } # ============================================================================= # AUTOBAN PROCESSOR # ============================================================================= cmd_reload_autoban_config() { log_info "Reloading auto-ban configuration..." # Write to all instance data paths local instances=$(uci show mitmproxy 2>/dev/null | grep '=instance$' | sed 's/mitmproxy\.\([^=]*\)=instance/\1/') if [ -n "$instances" ]; then for inst in $instances; do load_instance_config "$inst" load_config write_autoban_config log_info "Auto-ban config updated at $data_path/autoban.json" done else # Legacy single-instance mode load_config write_autoban_config log_info "Auto-ban config updated at $data_path/autoban.json" fi } cmd_process_autoban() { load_config # Refresh config before processing write_autoban_config 2>/dev/null || true local autoban_enabled=$(uci_get autoban.enabled || echo 0) if [ "$autoban_enabled" != "1" ]; then return 0 fi local autoban_log="$data_path/autoban-requests.log" local processed_log="$data_path/autoban-processed.log" local ban_duration=$(uci_get autoban.ban_duration || echo "4h") local whitelist=$(uci_get autoban.whitelist || echo "") # Check if log exists and has content [ ! -f "$autoban_log" ] && return 0 [ ! -s "$autoban_log" ] && return 0 # Check if CrowdSec CLI is available if ! command -v cscli >/dev/null 2>&1; then log_warn "cscli not found - cannot process auto-bans" return 1 fi # Process each line in the log local processed=0 local skipped=0 while IFS= read -r line; do [ -z "$line" ] && continue # Parse JSON line: {"ip": "x.x.x.x", "reason": "...", "duration": "4h", ...} local ip=$(echo "$line" | jsonfilter -e '@.ip' 2>/dev/null) local reason=$(echo "$line" | jsonfilter -e '@.reason' 2>/dev/null) local req_duration=$(echo "$line" | jsonfilter -e '@.duration' 2>/dev/null) [ -z "$ip" ] && continue # Use request duration or default local duration="${req_duration:-$ban_duration}" # Check whitelist local skip=0 if [ -n "$whitelist" ]; then for wl_ip in $(echo "$whitelist" | tr ',' ' '); do if [ "$ip" = "$wl_ip" ]; then log_info "Skipping whitelisted IP: $ip" skip=1 break fi done fi if [ "$skip" = "1" ]; then skipped=$((skipped + 1)) continue fi # Check if IP is already banned if cscli decisions list -i "$ip" 2>/dev/null | grep -q "$ip"; then log_info "IP already banned: $ip" skipped=$((skipped + 1)) continue fi # Add ban via CrowdSec log_info "Auto-banning IP: $ip for $duration (reason: $reason)" if cscli decisions add -i "$ip" -d "$duration" -R "mitmproxy-waf: $reason" -t ban >/dev/null 2>&1; then processed=$((processed + 1)) # Log to processed file echo "$(date -Iseconds) BANNED $ip $duration $reason" >> "$processed_log" else log_error "Failed to ban IP: $ip" fi done < "$autoban_log" # Clear the processed log if [ $processed -gt 0 ] || [ $skipped -gt 0 ]; then log_info "Processed $processed bans, skipped $skipped" : > "$autoban_log" # Truncate the file fi } cmd_haproxy_disable() { require_root load_config log_info "Disabling HAProxy backend inspection..." # 1. Disable HAProxy router in config uci set mitmproxy.haproxy_router.enabled='0' uci commit mitmproxy # 2. Restore original backends log_info "Restoring original HAProxy backends..." local restored=0 for vhost in $(uci show haproxy 2>/dev/null | grep "=vhost" | cut -d'=' -f1 | cut -d'.' -f2); do local original_backend=$(uci -q get haproxy.$vhost.original_backend) if [ -n "$original_backend" ]; then uci set haproxy.$vhost.backend="$original_backend" uci delete haproxy.$vhost.original_backend 2>/dev/null restored=$((restored + 1)) local domain=$(uci -q get haproxy.$vhost.domain) log_info " $domain: mitmproxy_inspector -> $original_backend" fi done uci commit haproxy log_info "Restored $restored vhosts" # 3. Restart services log_info "Restarting services..." /etc/init.d/haproxy reload 2>/dev/null || /etc/init.d/haproxy restart 2>/dev/null log_info "" log_info "HAProxy backend inspection DISABLED" log_info "Vhosts now route directly to their original backends" } cmd_list_instances() { echo "Configured mitmproxy instances:" echo "" for inst in $(uci show mitmproxy 2>/dev/null | grep "=instance" | cut -d'.' -f2 | cut -d'=' -f1); do local enabled=$(uci -q get mitmproxy.$inst.enabled || echo 0) local container=$(uci -q get mitmproxy.$inst.container_name || echo "mitmproxy-$inst") local desc=$(uci -q get mitmproxy.$inst.description || echo "") local port=$(uci -q get mitmproxy.$inst.proxy_port || echo "?") local web=$(uci -q get mitmproxy.$inst.web_port || echo "?") local mode=$(uci -q get mitmproxy.$inst.mode || echo "regular") local status_icon="[OFF]" [ "$enabled" = "1" ] && status_icon="[ON]" # Check if running if lxc-info -n "$container" -s 2>/dev/null | grep -q "RUNNING"; then status_icon="[RUN]" fi printf " %-8s %s %-20s proxy=%-5s web=%-5s mode=%s\n" \ "$inst" "$status_icon" "$container" "$port" "$web" "$mode" [ -n "$desc" ] && echo " $desc" done } # Main Entry Point case "${1:-}" in install) shift; cmd_install "$@" ;; check) shift; cmd_check "$@" ;; update) shift; cmd_update "$@" ;; status) shift; cmd_status "$@" ;; logs) shift; cmd_logs "$@" ;; shell) shift; cmd_shell "$@" ;; cert) shift; cmd_cert "$@" ;; firewall-setup) shift; cmd_firewall_setup "$@" ;; firewall-clear) shift; cmd_firewall_clear "$@" ;; wan-setup) shift; cmd_wan_setup "$@" ;; wan-clear) shift; cmd_wan_clear "$@" ;; sync-routes) shift; cmd_sync_routes "$@" ;; route) shift case "$1" in list) shift; cmd_route_list "$@" ;; add) shift; cmd_route_add "$@" ;; remove|rm|del) shift; cmd_route_remove "$@" ;; check) shift; cmd_route_check "$@" ;; *) echo "Usage: mitmproxyctl route {list|add|remove|check}" >&2; exit 1 ;; esac ;; haproxy-enable) shift; cmd_haproxy_enable "$@" ;; haproxy-disable) shift; cmd_haproxy_disable "$@" ;; process-autoban) shift; cmd_process_autoban "$@" ;; reload-autoban) shift; cmd_reload_autoban_config "$@" ;; service-run) shift; cmd_service_run "$@" ;; service-stop) shift; cmd_service_stop "$@" ;; list-instances|list) cmd_list_instances ;; help|--help|-h|'') usage ;; *) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;; esac