- Add route management commands: - `mitmproxyctl route list` - List all routes - `mitmproxyctl route add <domain> <ip> <port>` - Add manual route - `mitmproxyctl route remove <domain>` - Remove route - `mitmproxyctl route check` - Check for missing routes - Improve sync-routes to scan MetaBlogizer and Streamlit services: - Auto-detect enabled MetaBlogizer sites and add routes - Auto-detect enabled Streamlit instances with matching vhosts - Warn about mitmproxy_inspector vhosts with missing routes - Update routes config with 188 routes This fixes the issue where services using mitmproxy_inspector backend would fall back to default because their routes were not configured. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1952 lines
58 KiB
Bash
Executable File
1952 lines
58 KiB
Bash
Executable File
#!/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 <command> [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 <domain> <ip> <port> Add manual route
|
|
route remove <domain> 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://<router-ip>:$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://<router-ip>:$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://<router-ip>:$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 <domain> <ip> <port>" >&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 <domain>" >&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://<router-ip>/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
|