From ee987726603d5f58577cd8eaf27c3ccd8d03f7c6 Mon Sep 17 00:00:00 2001 From: Cursor Date: Thu, 8 Jan 2026 12:47:07 +0100 Subject: [PATCH] fixes --- limit-bandwidth.sh | 846 ++++++++++++++++++++++++++++++---- op/xlayer/testnet/rollup.json | 86 ++-- 2 files changed, 813 insertions(+), 119 deletions(-) diff --git a/limit-bandwidth.sh b/limit-bandwidth.sh index 7e30b291..1c290c14 100755 --- a/limit-bandwidth.sh +++ b/limit-bandwidth.sh @@ -9,13 +9,25 @@ # - P2P protocols have backpressure - if you're slow to respond, peers back off # - Limiting outgoing is usually sufficient to control your bandwidth usage # +# IP-based limiting: When using --limit-by-ip, a cronjob is automatically created +# to update iptables rules if the container IP changes (e.g., after restart) +# # Usage: -# ./limit-bandwidth.sh [start|stop|status] [--limit BANDWIDTH] +# ./limit-bandwidth.sh [start|stop|status] [OPTIONS] # ./limit-bandwidth.sh rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace start # ./limit-bandwidth.sh rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace start --limit 20mbit +# ./limit-bandwidth.sh rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace start --limit 5mbit --total-limit +# ./limit-bandwidth.sh rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace start --limit 5mbit --per-port-limit # -# Environment variable: +# Options: +# --limit BANDWIDTH Set bandwidth limit (e.g., 20mbit, 100mbit) +# --total-limit All ports share the limit (default) +# --per-port-limit Each port gets its own limit +# +# Environment variables: # BANDWIDTH_LIMIT=20mbit ./limit-bandwidth.sh start +# TOTAL_LIMIT=false BANDWIDTH_LIMIT=5mbit ./limit-bandwidth.sh start +# (TOTAL_LIMIT=false for per-port limits, true is default) # # For cronjob: # */5 * * * * /path/to/rpc/limit-bandwidth.sh /path/to/compose.yml start @@ -28,6 +40,14 @@ BASEPATH="$(cd "$(dirname "$0")" && pwd)" # Bandwidth limit (can be overridden via BANDWIDTH_LIMIT env var or --limit parameter) # Default: 100mbit BANDWIDTH_LIMIT="${BANDWIDTH_LIMIT:-100mbit}" +# TOTAL_LIMIT: If "true", limit is shared across all ports (total bandwidth) +# If "false", each port gets its own limit (per-port bandwidth) +# Default: true (total limit is now the default behavior) +TOTAL_LIMIT="${TOTAL_LIMIT:-true}" +# LIMIT_BY_IP: If "true", limit all traffic from container IP (not just specific ports) +# This catches ephemeral ports but requires detecting container IP dynamically +# Default: false (use port-based limiting) +LIMIT_BY_IP="${LIMIT_BY_IP:-false}" BURST_MULTIPLIER="${BURST_MULTIPLIER:-0.1}" # Burst is 10% of limit by default LATENCY="50ms" # Latency for shaping @@ -61,16 +81,24 @@ fi # Parse arguments if [ $# -lt 1 ]; then - echo "Usage: $0 [start|stop|status] [--limit BANDWIDTH]" + echo "Usage: $0 [start|stop|status] [OPTIONS]" echo "" echo "Options:" - echo " --limit BANDWIDTH Set bandwidth limit (e.g., 20mbit, 100mbit)" - echo " Can also use BANDWIDTH_LIMIT environment variable" + echo " --limit BANDWIDTH Set bandwidth limit (e.g., 20mbit, 100mbit)" + echo " Can also use BANDWIDTH_LIMIT environment variable" + echo " --total-limit Use total limit mode: all ports share the limit (default)" + echo " --per-port-limit Use per-port limit mode: each port gets its own limit" + echo " --limit-by-ip Limit by container IP (catches all ports including ephemeral)" + echo " Traffic between containers on same network is NOT limited" + echo " --limit-by-port Limit by source port only (default, misses ephemeral ports)" + echo " Can also use LIMIT_BY_IP=true/false environment variable" echo "" echo "Examples:" echo " $0 rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace start" echo " $0 rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace.yml start" echo " $0 rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace start --limit 20mbit" + echo " $0 rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace start --limit 5mbit --total-limit" + echo " $0 rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace start --limit 5mbit --per-port-limit" echo " BANDWIDTH_LIMIT=20mbit $0 rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace start" echo " $0 rpc/ethereum/geth/ethereum-mainnet-geth-pruned-pebble-path status" exit 1 @@ -79,19 +107,54 @@ fi COMPOSE_FILE="$1" ACTION=${2:-start} -# Parse optional --limit parameter -if [ "$ACTION" = "--limit" ] && [ $# -ge 3 ]; then - BANDWIDTH_LIMIT="$3" - ACTION=${4:-start} -elif [ $# -ge 3 ] && [ "$2" = "--limit" ]; then - BANDWIDTH_LIMIT="$3" - ACTION=${4:-start} -elif [ $# -ge 4 ] && [ "$3" = "--limit" ]; then - BANDWIDTH_LIMIT="$4" -elif [ "$ACTION" = "--limit" ]; then - echo -e "${RED}Error: --limit requires a bandwidth value${NC}" - echo "Example: $0 $COMPOSE_FILE start --limit 20mbit" - exit 1 +# Parse optional parameters +# Special handling for update-ip command (it has its own parameters) +if [ "$ACTION" = "update-ip" ]; then + # For update-ip, skip normal argument parsing and let the case statement handle it + shift 2 # Remove compose file and action +else + # Normal argument parsing for start/stop/status + shift # Remove compose file + shift # Remove action (or use default) + while [ $# -gt 0 ]; do + case "$1" in + --limit) + if [ -z "$2" ]; then + echo -e "${RED}Error: --limit requires a bandwidth value${NC}" + echo "Example: $0 $COMPOSE_FILE start --limit 20mbit" + exit 1 + fi + BANDWIDTH_LIMIT="$2" + shift 2 + ;; + --total-limit) + TOTAL_LIMIT="true" + shift + ;; + --per-port-limit) + TOTAL_LIMIT="false" + shift + ;; + --limit-by-ip) + LIMIT_BY_IP="true" + shift + ;; + --limit-by-port) + LIMIT_BY_IP="false" + shift + ;; + *) + # Unknown option, might be old-style --limit usage + if [ "$1" = "--limit" ] && [ -n "$2" ]; then + BANDWIDTH_LIMIT="$2" + shift 2 + else + echo -e "${YELLOW}Warning: Unknown option '$1', ignoring${NC}" + shift + fi + ;; + esac + done fi # Handle .yml extension (like latest.sh does) @@ -216,6 +279,76 @@ find_bridge() { echo "eth0" # Final fallback } +# Function to find the physical egress interface (where traffic actually leaves the system) +find_egress_interface() { + # Method 1: Find interface from default route (most reliable) + local default_route=$(ip route show default 2>/dev/null | head -1) + if [ -n "$default_route" ]; then + local egress=$(echo "$default_route" | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1) + if [ -n "$egress" ] && ip link show "$egress" >/dev/null 2>&1; then + # Verify it's not a virtual interface + if ! echo "$egress" | grep -qE "^(lo|docker|br-|veth)"; then + echo "$egress" + return 0 + fi + fi + fi + + # Method 2: Find interface used for a test route (works even without default route) + # Try multiple common IPs to find egress interface + for test_ip in 8.8.8.8 1.1.1.1 208.67.222.222; do + local route_output=$(ip route get "$test_ip" 2>/dev/null | head -1) + if [ -n "$route_output" ]; then + local egress=$(echo "$route_output" | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1) + if [ -n "$egress" ] && ip link show "$egress" >/dev/null 2>&1; then + # Verify it's not a virtual interface + if ! echo "$egress" | grep -qE "^(lo|docker|br-|veth)"; then + echo "$egress" + return 0 + fi + fi + fi + done + + # Method 3: Find first physical interface (non-virtual, non-loopback) + # Look for interfaces that are UP and not virtual + local iface=$(ip link show | grep -E "^[0-9]+:" | grep -vE "lo:|docker|br-|veth" | \ + while read -r line; do + ifname=$(echo "$line" | cut -d: -f2 | tr -d ' ') + # Check if interface is UP and not a virtual type + if ip link show "$ifname" 2>/dev/null | grep -q "state UP" && \ + ! ip link show "$ifname" 2>/dev/null | grep -qE "link/loopback|link/none"; then + echo "$ifname" + break + fi + done | head -1) + + if [ -n "$iface" ]; then + echo "$iface" + return 0 + fi + + # Method 4: Simple fallback - find first non-virtual interface name + local iface=$(ip link show | grep -E "^[0-9]+:" | grep -vE "lo:|docker|br-|veth" | \ + head -1 | cut -d: -f2 | tr -d ' ') + + if [ -n "$iface" ]; then + echo "$iface" + return 0 + fi + + # Final fallback: common interface names (in order of likelihood) + for fallback in eth0 enp0s3 enp0s8 ens33 ens3; do + if ip link show "$fallback" >/dev/null 2>&1; then + echo "$fallback" + return 0 + fi + done + + # Last resort + echo "eth0" +} + # Extract ports from compose file PORTS_RAW=$(extract_ports "$COMPOSE_FILE") @@ -237,24 +370,160 @@ fi echo -e "${GREEN}Found ${#PORTS[@]} public port(s): ${PORTS[*]}${NC}" -# Find the bridge interface +# Find the bridge interface (for iptables marking) BRIDGE=$(find_bridge "$COMPOSE_DIR") -echo -e "${BLUE}Using network interface: ${BRIDGE}${NC}" +echo -e "${BLUE}Using bridge interface: ${BRIDGE}${NC}" -# Verify interface exists +# Find the physical egress interface (where traffic actually leaves) +EGRESS_IFACE=$(find_egress_interface) +echo -e "${BLUE}Using egress interface: ${EGRESS_IFACE}${NC}" + +# Function to get network subnet from .env file or environment +get_network_subnet() { + # Try to get from .env file in the same directory as compose file first + local compose_dir=$(dirname "$COMPOSE_FILE") + local env_file="$compose_dir/.env" + + # Also check root .env file (where CHAINS_SUBNET is typically defined) + if [ ! -f "$env_file" ] || ! grep -q "^[[:space:]]*CHAINS_SUBNET[[:space:]]*=" "$env_file" 2>/dev/null; then + env_file="$BASEPATH/.env" + fi + + # Try to read CHAINS_SUBNET from .env file + if [ -f "$env_file" ]; then + # Handle various .env formats: CHAINS_SUBNET=value, CHAINS_SUBNET="value", CHAINS_SUBNET='value', CHAINS_SUBNET = value + local subnet=$(grep -E "^[[:space:]]*CHAINS_SUBNET[[:space:]]*=" "$env_file" 2>/dev/null | head -1 | sed 's/^[^=]*=[[:space:]]*//' | sed 's/^["'\'']//' | sed 's/["'\'']$//' | tr -d ' ') + if [ -n "$subnet" ]; then + echo "$subnet" + return 0 + fi + fi + + # Fallback: try environment variable (allows override) + if [ -n "$CHAINS_SUBNET" ]; then + echo "$CHAINS_SUBNET" + return 0 + fi + + # Last resort: try to detect from Docker network (dynamic detection) + local network="rpc_chains" + local subnet=$(docker network inspect "$network" --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}' 2>/dev/null | head -1) + if [ -n "$subnet" ]; then + echo "$subnet" + return 0 + fi + + # Final fallback: default from base.yml (matches base.yml default) + echo "192.168.0.0/26" +} + +# Function to get container IP and network subnet (for IP-based limiting) +get_container_network_info() { + # Try docker compose first + local container_name=$(docker compose -f "$COMPOSE_FILE" ps --format json 2>/dev/null | \ + jq -r '.[0].Name' 2>/dev/null | head -1) + + # Fallback: try to find container by compose file path + if [ -z "$container_name" ] || [ "$container_name" = "null" ]; then + # Extract service name from compose file path (e.g., gnosis-mainnet-erigon3 from path) + local service_name=$(basename "$COMPOSE_FILE" .yml | sed 's/-pruned-trace$//' | sed 's/-archive-trace$//' | sed 's/-minimal-trace$//') + container_name=$(docker ps --filter "name=$service_name" --format "{{.Names}}" 2>/dev/null | head -1) + fi + + if [ -z "$container_name" ] || [ "$container_name" = "null" ]; then + echo "" + return 1 + fi + + # Get container IP + local container_ip=$(docker inspect "$container_name" --format '{{range $net, $conf := .NetworkSettings.Networks}}{{$conf.IPAddress}}{{end}}' 2>/dev/null | head -1) + + if [ -z "$container_ip" ]; then + echo "" + return 1 + fi + + # Get network subnet from .env or environment (not hardcoded) + local subnet=$(get_network_subnet) + + echo "$container_ip|$subnet" + return 0 +} + +# Get container IP and network info if IP-based limiting is enabled +CONTAINER_IP="" +NETWORK_SUBNET="" +if [ "$LIMIT_BY_IP" = "true" ] || [ "$LIMIT_BY_IP" = "1" ] || [ "$LIMIT_BY_IP" = "yes" ]; then + NETWORK_INFO=$(get_container_network_info) + if [ -n "$NETWORK_INFO" ]; then + CONTAINER_IP=$(echo "$NETWORK_INFO" | cut -d'|' -f1) + NETWORK_SUBNET=$(echo "$NETWORK_INFO" | cut -d'|' -f2) + if [ -n "$CONTAINER_IP" ]; then + echo -e "${BLUE}Container IP: ${CONTAINER_IP}${NC}" + if [ -n "$NETWORK_SUBNET" ]; then + echo -e "${BLUE}Network subnet: ${NETWORK_SUBNET} (inter-container traffic will NOT be limited)${NC}" + fi + else + echo -e "${YELLOW}Warning: Could not detect container IP, falling back to port-based limiting${NC}" + LIMIT_BY_IP="false" + fi + else + echo -e "${YELLOW}Warning: Could not detect container IP, falling back to port-based limiting${NC}" + LIMIT_BY_IP="false" + fi +fi + +# Verify interfaces exist if ! ip link show "$BRIDGE" >/dev/null 2>&1; then echo -e "${RED}Error: Network interface '$BRIDGE' not found${NC}" exit 1 fi +if ! ip link show "$EGRESS_IFACE" >/dev/null 2>&1; then + echo -e "${RED}Error: Egress interface '$EGRESS_IFACE' not found${NC}" + exit 1 +fi + case "$ACTION" in start) echo -e "${YELLOW}Setting up bandwidth limiting...${NC}" - # Remove existing qdisc if any + # Clean up existing iptables rules first (to avoid duplicates/conflicts) + echo -e "${BLUE}Cleaning up existing iptables rules...${NC}" + PORT_ID=10 + for port in "${PORTS[@]}"; do + # Remove OUTPUT rules (outgoing from host) + iptables -t mangle -D OUTPUT -p tcp --sport ${port} -j MARK 2>/dev/null || true + iptables -t mangle -D OUTPUT -p udp --sport ${port} -j MARK 2>/dev/null || true + + # Remove FORWARD rules (outgoing from containers) + iptables -t mangle -D FORWARD -p tcp --sport ${port} -j MARK 2>/dev/null || true + iptables -t mangle -D FORWARD -p udp --sport ${port} -j MARK 2>/dev/null || true + + # Remove POSTROUTING rules + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -p tcp --sport ${port} -j MARK 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -p udp --sport ${port} -j MARK 2>/dev/null || true + + PORT_ID=$((PORT_ID + 1)) + done + + # Also clean up shared class mark (20) if it exists + SHARED_MARK=20 + for port in "${PORTS[@]}"; do + iptables -t mangle -D OUTPUT -p tcp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D OUTPUT -p udp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D FORWARD -p tcp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D FORWARD -p udp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -p tcp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -p udp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + done + + # Remove existing qdisc if any (on both bridge and egress interface) tc qdisc del dev "$BRIDGE" root 2>/dev/null || true + tc qdisc del dev "$EGRESS_IFACE" root 2>/dev/null || true # Create HTB (Hierarchical Token Bucket) qdisc for egress (outgoing traffic only) + # Apply to the physical egress interface where traffic actually leaves the system # We only limit outgoing because: # - You control what you send (outgoing) # - You can't control what others send (incoming) @@ -265,99 +534,519 @@ case "$ACTION" in # Higher r2q = smaller quantum = better for low bandwidths # Quantum = rate / r2q, so higher r2q means smaller quantums # Adaptive r2q based on bandwidth limit to avoid quantum warnings - tc qdisc add dev "$BRIDGE" root handle 1: htb r2q ${R2Q_VALUE} default 30 + tc qdisc add dev "$EGRESS_IFACE" root handle 1: htb r2q ${R2Q_VALUE} default 30 # Create root class with high bandwidth # Set explicit quantum to avoid warnings # Quantum should be between 1500-60000 bytes for optimal performance # For 1000mbit (125MB/s), use quantum of 50000 bytes (within recommended range) - tc class add dev "$BRIDGE" parent 1: classid 1:1 htb rate 1000mbit quantum 50000 + tc class add dev "$EGRESS_IFACE" parent 1: classid 1:1 htb rate 1000mbit quantum 50000 # Create unlimited class for non-limited traffic - tc class add dev "$BRIDGE" parent 1:1 classid 1:30 htb rate 1000mbit quantum 50000 + tc class add dev "$EGRESS_IFACE" parent 1:1 classid 1:30 htb rate 1000mbit quantum 50000 - # Process each port - PORT_ID=10 - for port in "${PORTS[@]}"; do - echo -e "${BLUE} Limiting port ${port} to ${BANDWIDTH_LIMIT}...${NC}" + # Check if we should use total limit (shared across all ports) or per-port limit + if [ "$TOTAL_LIMIT" = "true" ] || [ "$TOTAL_LIMIT" = "1" ] || [ "$TOTAL_LIMIT" = "yes" ]; then + # TOTAL LIMIT MODE: All ports share a single class with the specified limit + echo -e "${BLUE}Using TOTAL limit mode: All ${#PORTS[@]} ports share ${BANDWIDTH_LIMIT} total${NC}" - # Create limited class for this port (outgoing traffic only) - # Calculate quantum based on rate to avoid warnings - # Quantum should be between 1500-60000 bytes for optimal performance - # Formula: quantum should be roughly rate_in_bytes / 2000 to stay in range + # Calculate quantum for the shared class if [[ "$BANDWIDTH_LIMIT" =~ ^([0-9]+)mbit$ ]]; then RATE_NUM="${BASH_REMATCH[1]}" - RATE_BYTES=$((RATE_NUM * 125000)) # Convert mbit to bytes/sec (1mbit = 125KB/s) - QUANTUM=$((RATE_BYTES / 2000)) # Divide by 2000 to get reasonable quantum - # Ensure minimum quantum of 1500 (MTU size) + RATE_BYTES=$((RATE_NUM * 125000)) + QUANTUM=$((RATE_BYTES / 2000)) if [ "$QUANTUM" -lt 1500 ]; then QUANTUM=1500 fi - # Cap maximum quantum at 60000 (HTB recommended max) if [ "$QUANTUM" -gt 60000 ]; then QUANTUM=60000 fi - tc class add dev "$BRIDGE" parent 1:1 classid 1:${PORT_ID} htb rate ${BANDWIDTH_LIMIT} burst ${BURST} ceil ${BANDWIDTH_LIMIT} quantum ${QUANTUM} else - # Fallback: use safe default quantum - tc class add dev "$BRIDGE" parent 1:1 classid 1:${PORT_ID} htb rate ${BANDWIDTH_LIMIT} burst ${BURST} ceil ${BANDWIDTH_LIMIT} quantum 1500 + QUANTUM=1500 fi - # Add filter to route marked packets to this class (outgoing only) - tc filter add dev "$BRIDGE" parent 1: protocol ip prio ${PORT_ID} handle ${PORT_ID} fw flowid 1:${PORT_ID} + # Create a single shared class for all ports (class ID 20) + SHARED_CLASS_ID=20 + MARK_VALUE=${SHARED_CLASS_ID} + tc class add dev "$EGRESS_IFACE" parent 1:1 classid 1:${SHARED_CLASS_ID} htb rate ${BANDWIDTH_LIMIT} burst ${BURST} ceil ${BANDWIDTH_LIMIT} quantum ${QUANTUM} - # Mark outgoing traffic in OUTPUT chain (traffic from host) - iptables -t mangle -C OUTPUT -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || \ - iptables -t mangle -A OUTPUT -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} - iptables -t mangle -C OUTPUT -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || \ - iptables -t mangle -A OUTPUT -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} + # Add filter to route marked packets to shared class (only once, not per port) + tc filter add dev "$EGRESS_IFACE" parent 1: protocol ip prio ${MARK_VALUE} handle ${MARK_VALUE} fw flowid 1:${SHARED_CLASS_ID} 2>/dev/null || true - # Mark outgoing traffic in FORWARD chain (traffic from containers) - # This catches traffic from containers going out through the bridge - iptables -t mangle -C FORWARD -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || \ - iptables -t mangle -A FORWARD -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} - iptables -t mangle -C FORWARD -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || \ - iptables -t mangle -A FORWARD -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} + # IP-based limiting: Mark all traffic from container IP (excluding inter-container traffic) + if [ "$LIMIT_BY_IP" = "true" ] && [ -n "$CONTAINER_IP" ]; then + echo -e "${BLUE} Adding IP-based limiting for ${CONTAINER_IP}...${NC}" + + # Mark outgoing traffic in FORWARD chain (traffic from container going to internet) + # Exclude traffic going to other containers in the same network + if [ -n "$NETWORK_SUBNET" ]; then + echo -e "${BLUE} Excluding traffic to ${NETWORK_SUBNET} (inter-container traffic)${NC}" + iptables -t mangle -C FORWARD -s "$CONTAINER_IP" ! -d "$NETWORK_SUBNET" -j MARK --set-mark ${MARK_VALUE} 2>/dev/null || \ + iptables -t mangle -A FORWARD -s "$CONTAINER_IP" ! -d "$NETWORK_SUBNET" -j MARK --set-mark ${MARK_VALUE} + else + iptables -t mangle -C FORWARD -s "$CONTAINER_IP" -j MARK --set-mark ${MARK_VALUE} 2>/dev/null || \ + iptables -t mangle -A FORWARD -s "$CONTAINER_IP" -j MARK --set-mark ${MARK_VALUE} + fi + + # Mark outgoing traffic in POSTROUTING chain (critical for Docker NAT) + if [ -n "$NETWORK_SUBNET" ]; then + iptables -t mangle -C POSTROUTING -o "$EGRESS_IFACE" -s "$CONTAINER_IP" ! -d "$NETWORK_SUBNET" -j MARK --set-mark ${MARK_VALUE} 2>/dev/null || \ + iptables -t mangle -A POSTROUTING -o "$EGRESS_IFACE" -s "$CONTAINER_IP" ! -d "$NETWORK_SUBNET" -j MARK --set-mark ${MARK_VALUE} + else + iptables -t mangle -C POSTROUTING -o "$EGRESS_IFACE" -s "$CONTAINER_IP" -j MARK --set-mark ${MARK_VALUE} 2>/dev/null || \ + iptables -t mangle -A POSTROUTING -o "$EGRESS_IFACE" -s "$CONTAINER_IP" -j MARK --set-mark ${MARK_VALUE} + fi + fi - PORT_ID=$((PORT_ID + 1)) + # Port-based limiting: Mark traffic from specific ports (if not using IP-only mode) + if [ "$LIMIT_BY_IP" != "true" ] || [ -z "$CONTAINER_IP" ]; then + # Process each port - all route to the same shared class + for port in "${PORTS[@]}"; do + echo -e "${BLUE} Adding port ${port} to shared limit of ${BANDWIDTH_LIMIT}...${NC}" + + # Mark outgoing traffic in OUTPUT chain (traffic from host) + iptables -t mangle -C OUTPUT -p tcp --sport ${port} -j MARK --set-mark ${MARK_VALUE} 2>/dev/null || \ + iptables -t mangle -A OUTPUT -p tcp --sport ${port} -j MARK --set-mark ${MARK_VALUE} + iptables -t mangle -C OUTPUT -p udp --sport ${port} -j MARK --set-mark ${MARK_VALUE} 2>/dev/null || \ + iptables -t mangle -A OUTPUT -p udp --sport ${port} -j MARK --set-mark ${MARK_VALUE} + + # Mark outgoing traffic in FORWARD chain (traffic from containers) + iptables -t mangle -C FORWARD -p tcp --sport ${port} -j MARK --set-mark ${MARK_VALUE} 2>/dev/null || \ + iptables -t mangle -A FORWARD -p tcp --sport ${port} -j MARK --set-mark ${MARK_VALUE} + iptables -t mangle -C FORWARD -p udp --sport ${port} -j MARK --set-mark ${MARK_VALUE} 2>/dev/null || \ + iptables -t mangle -A FORWARD -p udp --sport ${port} -j MARK --set-mark ${MARK_VALUE} + + # Mark outgoing traffic in POSTROUTING chain (critical for Docker NAT) + iptables -t mangle -C POSTROUTING -o "$EGRESS_IFACE" -p tcp --sport ${port} -j MARK --set-mark ${MARK_VALUE} 2>/dev/null || \ + iptables -t mangle -A POSTROUTING -o "$EGRESS_IFACE" -p tcp --sport ${port} -j MARK --set-mark ${MARK_VALUE} + iptables -t mangle -C POSTROUTING -o "$EGRESS_IFACE" -p udp --sport ${port} -j MARK --set-mark ${MARK_VALUE} 2>/dev/null || \ + iptables -t mangle -A POSTROUTING -o "$EGRESS_IFACE" -p udp --sport ${port} -j MARK --set-mark ${MARK_VALUE} + done + fi + + echo -e "${GREEN}✓ Outgoing bandwidth limiting configured!${NC}" + echo -e "${GREEN}All ${#PORTS[@]} ports share a TOTAL limit of ${BANDWIDTH_LIMIT} outgoing traffic${NC}" + else + # PER-PORT LIMIT MODE: Each port gets its own class (original behavior) + echo -e "${BLUE}Using PER-PORT limit mode: Each port limited to ${BANDWIDTH_LIMIT}${NC}" + + PORT_ID=10 + for port in "${PORTS[@]}"; do + echo -e "${BLUE} Limiting port ${port} to ${BANDWIDTH_LIMIT}...${NC}" + + # Create limited class for this port (outgoing traffic only) + # Calculate quantum based on rate to avoid warnings + # Quantum should be between 1500-60000 bytes for optimal performance + # Formula: quantum should be roughly rate_in_bytes / 2000 to stay in range + if [[ "$BANDWIDTH_LIMIT" =~ ^([0-9]+)mbit$ ]]; then + RATE_NUM="${BASH_REMATCH[1]}" + RATE_BYTES=$((RATE_NUM * 125000)) # Convert mbit to bytes/sec (1mbit = 125KB/s) + QUANTUM=$((RATE_BYTES / 2000)) # Divide by 2000 to get reasonable quantum + # Ensure minimum quantum of 1500 (MTU size) + if [ "$QUANTUM" -lt 1500 ]; then + QUANTUM=1500 + fi + # Cap maximum quantum at 60000 (HTB recommended max) + if [ "$QUANTUM" -gt 60000 ]; then + QUANTUM=60000 + fi + tc class add dev "$EGRESS_IFACE" parent 1:1 classid 1:${PORT_ID} htb rate ${BANDWIDTH_LIMIT} burst ${BURST} ceil ${BANDWIDTH_LIMIT} quantum ${QUANTUM} + else + # Fallback: use safe default quantum + tc class add dev "$EGRESS_IFACE" parent 1:1 classid 1:${PORT_ID} htb rate ${BANDWIDTH_LIMIT} burst ${BURST} ceil ${BANDWIDTH_LIMIT} quantum 1500 + fi + + # Add filter to route marked packets to this class (outgoing only) + tc filter add dev "$EGRESS_IFACE" parent 1: protocol ip prio ${PORT_ID} handle ${PORT_ID} fw flowid 1:${PORT_ID} + + # Mark outgoing traffic in OUTPUT chain (traffic from host) + iptables -t mangle -C OUTPUT -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || \ + iptables -t mangle -A OUTPUT -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} + iptables -t mangle -C OUTPUT -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || \ + iptables -t mangle -A OUTPUT -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} + + # Mark outgoing traffic in FORWARD chain (traffic from containers) + # This catches traffic from containers going out through the bridge + iptables -t mangle -C FORWARD -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || \ + iptables -t mangle -A FORWARD -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} + iptables -t mangle -C FORWARD -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || \ + iptables -t mangle -A FORWARD -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} + + # Mark outgoing traffic in POSTROUTING chain (critical for Docker NAT) + # This ensures marks are preserved through NAT and applied on the egress interface + iptables -t mangle -C POSTROUTING -o "$EGRESS_IFACE" -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || \ + iptables -t mangle -A POSTROUTING -o "$EGRESS_IFACE" -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} + iptables -t mangle -C POSTROUTING -o "$EGRESS_IFACE" -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || \ + iptables -t mangle -A POSTROUTING -o "$EGRESS_IFACE" -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} + + PORT_ID=$((PORT_ID + 1)) + done + + echo -e "${GREEN}✓ Outgoing bandwidth limiting configured!${NC}" + echo -e "${GREEN}All ports are now limited to ${BANDWIDTH_LIMIT} outgoing traffic each${NC}" + fi + + echo -e "${YELLOW}Note: Only OUTGOING traffic is limited. Incoming traffic is not limited.${NC}" + + # If using IP-based limiting, set up cronjob to update IP if container restarts + if [ "$LIMIT_BY_IP" = "true" ] && [ -n "$CONTAINER_IP" ]; then + echo -e "${BLUE}Setting up cronjob to update IP-based rules if container IP changes...${NC}" + + # Create a unique identifier for this cronjob based on compose file + CRON_ID=$(echo "$COMPOSE_FILE" | md5sum | cut -d' ' -f1 | head -c 8) + CRON_TAG="limit-bandwidth-${CRON_ID}" + + # Remove existing cronjob if any + (crontab -l 2>/dev/null | grep -v "$CRON_TAG" || true) | crontab - + + # Add new cronjob (every 5 minutes) + CRON_CMD="*/5 * * * * $BASEPATH/limit-bandwidth.sh \"$COMPOSE_FILE\" update-ip --cron-id $CRON_ID >> /var/log/limit-bandwidth-${CRON_ID}.log 2>&1" + (crontab -l 2>/dev/null | grep -v "$CRON_TAG"; echo "$CRON_CMD # $CRON_TAG") | crontab - + + echo -e "${GREEN}✓ Cronjob registered (runs every 5 minutes to update IP if changed)${NC}" + echo -e "${BLUE} To view logs: tail -f /var/log/limit-bandwidth-${CRON_ID}.log${NC}" + fi + ;; + update-ip) + # Internal command to update IP-based rules (called by cronjob) + # Parse --cron-id parameter from remaining arguments + CRON_ID="" + while [ $# -gt 0 ]; do + case "$1" in + --cron-id) + if [ -n "$2" ]; then + CRON_ID="$2" + shift 2 + else + echo "$(date): Error: --cron-id requires a value" + exit 1 + fi + ;; + --cron-id=*) + CRON_ID="${1#*=}" + shift + ;; + *) + shift + ;; + esac done - echo -e "${GREEN}✓ Outgoing bandwidth limiting configured!${NC}" - echo -e "${GREEN}All ports are now limited to ${BANDWIDTH_LIMIT} outgoing traffic each${NC}" - echo -e "${YELLOW}Note: Only OUTGOING traffic is limited. Incoming traffic is not limited.${NC}" + if [ -z "$CRON_ID" ]; then + echo "$(date): Error: update-ip requires --cron-id parameter" + exit 1 + fi + + # Need to re-initialize variables for update-ip command + EGRESS_IFACE=$(find_egress_interface) + + # Get expected service name from compose file to verify container matches + EXPECTED_SERVICE=$(grep -E "^[[:space:]]*[a-zA-Z0-9_-]+:" "$COMPOSE_FILE" | grep -v "^[[:space:]]*x-" | head -1 | cut -d: -f1 | tr -d ' ') + + # Get ports from compose file (needed for cleanup) + PORTS_RAW=$(extract_ports "$COMPOSE_FILE") + PORTS=() + while IFS= read -r line; do + [ -n "$line" ] && PORTS+=("$line") + done <<< "$PORTS_RAW" + + # Get current container IP and verify container exists + NETWORK_INFO=$(get_container_network_info) + if [ -z "$NETWORK_INFO" ]; then + echo "$(date): WARNING: Container not found for compose file $COMPOSE_FILE" + echo "$(date): Container may have been removed. Removing all bandwidth limiting rules (IP and port-based)." + + # Find and remove any existing IP-based rules + EXISTING_IP=$(iptables -t mangle -L FORWARD -n 2>/dev/null | grep "MARK set 0x14" | grep -oE "192\.168\.[0-9]+\.[0-9]+" | head -1) + if [ -z "$EXISTING_IP" ]; then + EXISTING_IP=$(iptables -t mangle -L POSTROUTING -n 2>/dev/null | grep "MARK set 0x14" | grep -oE "192\.168\.[0-9]+\.[0-9]+" | head -1) + fi + + if [ -n "$EXISTING_IP" ]; then + SHARED_MARK=20 + CURRENT_SUBNET=$(get_network_subnet) + if [ -n "$CURRENT_SUBNET" ]; then + iptables -t mangle -D FORWARD -s "$EXISTING_IP" ! -d "$CURRENT_SUBNET" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -s "$EXISTING_IP" ! -d "$CURRENT_SUBNET" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + else + iptables -t mangle -D FORWARD -s "$EXISTING_IP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -s "$EXISTING_IP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + fi + echo "$(date): Removed IP-based rules for $EXISTING_IP" + fi + + # Remove port-based rules (they add overhead to iptables if left orphaned) + if [ ${#PORTS[@]} -gt 0 ]; then + echo "$(date): Removing port-based rules for ports: ${PORTS[*]}" + SHARED_MARK=20 + PORT_ID=10 + for port in "${PORTS[@]}"; do + # Remove OUTPUT rules + iptables -t mangle -D OUTPUT -p tcp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D OUTPUT -p udp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + # Remove FORWARD rules + iptables -t mangle -D FORWARD -p tcp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D FORWARD -p udp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + # Remove POSTROUTING rules + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -p tcp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -p udp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + PORT_ID=$((PORT_ID + 1)) + done + echo "$(date): Removed port-based rules for ${#PORTS[@]} ports" + fi + + # Optionally remove cronjob if container is permanently gone + # (User can manually restart if container comes back) + echo "$(date): NOTE: If container is permanently removed, run 'stop' command to remove cronjob" + exit 0 + fi + + CURRENT_IP=$(echo "$NETWORK_INFO" | cut -d'|' -f1) + CURRENT_SUBNET=$(echo "$NETWORK_INFO" | cut -d'|' -f2) + + # Verify the container matches expected service (safety check) + # The IP we got came from get_container_network_info, which already verified the container exists + # But we should double-check that the container is still running and matches + + # Get container name that was used to find the IP (re-use the same logic) + CONTAINER_NAME=$(docker compose -f "$COMPOSE_FILE" ps --format json 2>/dev/null | \ + jq -r '.[0].Name' 2>/dev/null | head -1) + + if [ -z "$CONTAINER_NAME" ] || [ "$CONTAINER_NAME" = "null" ]; then + # Fallback: try to find container by service name pattern + if [ -n "$EXPECTED_SERVICE" ]; then + CONTAINER_NAME=$(docker ps --filter "name=$EXPECTED_SERVICE" --format "{{.Names}}" 2>/dev/null | head -1) + fi + fi + + # If we can't find the container name, but we got an IP, verify by checking which container has that IP + if [ -z "$CONTAINER_NAME" ]; then + # Find container by IP address + CONTAINER_NAME=$(docker ps --format "{{.Names}}" 2>/dev/null | while read name; do + container_ip=$(docker inspect "$name" --format '{{range $net, $conf := .NetworkSettings.Networks}}{{$conf.IPAddress}}{{end}}' 2>/dev/null | head -1) + if [ "$container_ip" = "$CURRENT_IP" ]; then + echo "$name" + break + fi + done | head -1) + fi + + # Verify container exists and is running + if [ -z "$CONTAINER_NAME" ]; then + echo "$(date): WARNING: Could not find container with IP $CURRENT_IP" + echo "$(date): Container may have been removed. Removing IP-based rules to prevent affecting other containers." + + # Remove rules for this IP to prevent affecting wrong container + SHARED_MARK=20 + if [ -n "$CURRENT_SUBNET" ]; then + iptables -t mangle -D FORWARD -s "$CURRENT_IP" ! -d "$CURRENT_SUBNET" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -s "$CURRENT_IP" ! -d "$CURRENT_SUBNET" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + else + iptables -t mangle -D FORWARD -s "$CURRENT_IP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -s "$CURRENT_IP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + fi + echo "$(date): Removed rules for $CURRENT_IP. Run 'start' command again when correct container is running." + exit 0 + fi + + # Verify IP actually belongs to this container + CONTAINER_IP_CHECK=$(docker inspect "$CONTAINER_NAME" --format '{{range $net, $conf := .NetworkSettings.Networks}}{{$conf.IPAddress}}{{end}}' 2>/dev/null | head -1) + if [ "$CONTAINER_IP_CHECK" != "$CURRENT_IP" ]; then + echo "$(date): WARNING: IP mismatch! Container $CONTAINER_NAME has IP $CONTAINER_IP_CHECK but found $CURRENT_IP" + echo "$(date): This IP may have been reassigned to a different container. Removing rules to prevent affecting wrong container." + + # Remove old rules for the IP we found + SHARED_MARK=20 + if [ -n "$CURRENT_SUBNET" ]; then + iptables -t mangle -D FORWARD -s "$CURRENT_IP" ! -d "$CURRENT_SUBNET" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -s "$CURRENT_IP" ! -d "$CURRENT_SUBNET" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + else + iptables -t mangle -D FORWARD -s "$CURRENT_IP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -s "$CURRENT_IP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + fi + echo "$(date): Removed rules for $CURRENT_IP. Run 'start' command again when correct container is running." + exit 0 + fi + + # Additional verification: check if container is actually running + CONTAINER_STATE=$(docker inspect "$CONTAINER_NAME" --format '{{.State.Status}}' 2>/dev/null) + if [ "$CONTAINER_STATE" != "running" ]; then + echo "$(date): WARNING: Container $CONTAINER_NAME is not running (state: $CONTAINER_STATE)" + echo "$(date): Removing IP-based rules to prevent affecting other containers if IP is reassigned." + + SHARED_MARK=20 + if [ -n "$CURRENT_SUBNET" ]; then + iptables -t mangle -D FORWARD -s "$CURRENT_IP" ! -d "$CURRENT_SUBNET" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -s "$CURRENT_IP" ! -d "$CURRENT_SUBNET" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + else + iptables -t mangle -D FORWARD -s "$CURRENT_IP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -s "$CURRENT_IP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + fi + echo "$(date): Removed rules for $CURRENT_IP. Rules will be re-added when container is running again." + exit 0 + fi + + # Verify container name matches expected service (additional safety check) + if [ -n "$EXPECTED_SERVICE" ] && ! echo "$CONTAINER_NAME" | grep -qi "$EXPECTED_SERVICE"; then + echo "$(date): WARNING: Container name '$CONTAINER_NAME' doesn't match expected service '$EXPECTED_SERVICE'" + echo "$(date): However, IP $CURRENT_IP belongs to this container. Proceeding with update." + fi + + # Check if IP-based rules exist and get the IP they're using + # Look for rules with MARK set 0x14 (20 in decimal) that have source IP + EXISTING_IP=$(iptables -t mangle -L FORWARD -n 2>/dev/null | grep "MARK set 0x14" | grep -oE "192\.168\.[0-9]+\.[0-9]+" | head -1) + + # If no existing IP found, try POSTROUTING chain + if [ -z "$EXISTING_IP" ]; then + EXISTING_IP=$(iptables -t mangle -L POSTROUTING -n 2>/dev/null | grep "MARK set 0x14" | grep -oE "192\.168\.[0-9]+\.[0-9]+" | head -1) + fi + + # If IP hasn't changed, no update needed + if [ -n "$EXISTING_IP" ] && [ "$CURRENT_IP" = "$EXISTING_IP" ]; then + echo "$(date): IP unchanged ($CURRENT_IP), no update needed" + exit 0 + fi + + if [ -n "$EXISTING_IP" ]; then + echo "$(date): IP changed from $EXISTING_IP to $CURRENT_IP, updating rules..." + else + echo "$(date): No existing IP rules found, adding rules for $CURRENT_IP..." + fi + + # Remove old rules + if [ -n "$EXISTING_IP" ]; then + SHARED_MARK=20 + # Try to find subnet from existing rules + EXISTING_SUBNET=$(iptables -t mangle -L FORWARD -n 2>/dev/null | grep "$EXISTING_IP" | grep -oE "192\.168\.[0-9]+\.[0-9]+/[0-9]+" | head -1) + if [ -z "$EXISTING_SUBNET" ]; then + EXISTING_SUBNET="$CURRENT_SUBNET" + fi + + if [ -n "$EXISTING_SUBNET" ]; then + iptables -t mangle -D FORWARD -s "$EXISTING_IP" ! -d "$EXISTING_SUBNET" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -s "$EXISTING_IP" ! -d "$EXISTING_SUBNET" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + else + iptables -t mangle -D FORWARD -s "$EXISTING_IP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -s "$EXISTING_IP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + fi + fi + + # Add new rules with current IP + SHARED_MARK=20 + if [ -n "$CURRENT_SUBNET" ]; then + iptables -t mangle -C FORWARD -s "$CURRENT_IP" ! -d "$CURRENT_SUBNET" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || \ + iptables -t mangle -A FORWARD -s "$CURRENT_IP" ! -d "$CURRENT_SUBNET" -j MARK --set-mark ${SHARED_MARK} + iptables -t mangle -C POSTROUTING -o "$EGRESS_IFACE" -s "$CURRENT_IP" ! -d "$CURRENT_SUBNET" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || \ + iptables -t mangle -A POSTROUTING -o "$EGRESS_IFACE" -s "$CURRENT_IP" ! -d "$CURRENT_SUBNET" -j MARK --set-mark ${SHARED_MARK} + else + iptables -t mangle -C FORWARD -s "$CURRENT_IP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || \ + iptables -t mangle -A FORWARD -s "$CURRENT_IP" -j MARK --set-mark ${SHARED_MARK} + iptables -t mangle -C POSTROUTING -o "$EGRESS_IFACE" -s "$CURRENT_IP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || \ + iptables -t mangle -A POSTROUTING -o "$EGRESS_IFACE" -s "$CURRENT_IP" -j MARK --set-mark ${SHARED_MARK} + fi + + echo "$(date): Successfully updated IP-based rules to $CURRENT_IP" ;; stop) echo -e "${YELLOW}Removing bandwidth limiting...${NC}" - # Remove iptables rules for each port - PORT_ID=10 - for port in "${PORTS[@]}"; do - echo -e "${BLUE} Removing limits for port ${port}...${NC}" - - # Remove OUTPUT rules (outgoing from host) - iptables -t mangle -D OUTPUT -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || true - iptables -t mangle -D OUTPUT -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || true - - # Remove FORWARD rules (outgoing from containers) - iptables -t mangle -D FORWARD -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || true - iptables -t mangle -D FORWARD -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || true - - PORT_ID=$((PORT_ID + 1)) - done + # Remove cronjob if it exists + CRON_ID=$(echo "$COMPOSE_FILE" | md5sum | cut -d' ' -f1 | head -c 8) + CRON_TAG="limit-bandwidth-${CRON_ID}" + if crontab -l 2>/dev/null | grep -q "$CRON_TAG"; then + echo -e "${BLUE} Removing cronjob...${NC}" + (crontab -l 2>/dev/null | grep -v "$CRON_TAG" || true) | crontab - + echo -e "${GREEN}✓ Cronjob removed${NC}" + fi - # Remove tc qdisc + # Get container IP for cleanup (if IP-based limiting was used) + # Try to find IP from existing iptables rules first + EXISTING_IP=$(iptables -t mangle -L FORWARD -n 2>/dev/null | grep "MARK set 0x14" | grep -oE "192\.168\.[0-9]+\.[0-9]+" | head -1) + + CONTAINER_IP_CLEANUP="" + NETWORK_SUBNET_CLEANUP="" + if [ -z "$EXISTING_IP" ]; then + # Fallback: try to get from container + NETWORK_INFO=$(get_container_network_info 2>/dev/null) + if [ -n "$NETWORK_INFO" ]; then + CONTAINER_IP_CLEANUP=$(echo "$NETWORK_INFO" | cut -d'|' -f1) + NETWORK_SUBNET_CLEANUP=$(echo "$NETWORK_INFO" | cut -d'|' -f2) + fi + else + CONTAINER_IP_CLEANUP="$EXISTING_IP" + # Try to get subnet from existing rules + EXISTING_SUBNET=$(iptables -t mangle -L FORWARD -n 2>/dev/null | grep "MARK set 0x14" | grep -oE "192\.168\.[0-9]+\.[0-9]+/[0-9]+" | head -1) + if [ -n "$EXISTING_SUBNET" ]; then + NETWORK_SUBNET_CLEANUP="$EXISTING_SUBNET" + fi + fi + + # Remove IP-based iptables rules if they exist + if [ -n "$CONTAINER_IP_CLEANUP" ]; then + echo -e "${BLUE} Removing IP-based limits for ${CONTAINER_IP_CLEANUP}...${NC}" + SHARED_MARK=20 + if [ -n "$NETWORK_SUBNET_CLEANUP" ]; then + iptables -t mangle -D FORWARD -s "$CONTAINER_IP_CLEANUP" ! -d "$NETWORK_SUBNET_CLEANUP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -s "$CONTAINER_IP_CLEANUP" ! -d "$NETWORK_SUBNET_CLEANUP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + else + iptables -t mangle -D FORWARD -s "$CONTAINER_IP_CLEANUP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -s "$CONTAINER_IP_CLEANUP" -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + fi + fi + + # Remove iptables rules for each port + # Check if we need to remove shared class (20) or individual classes (10+) + if tc class show dev "$EGRESS_IFACE" 2>/dev/null | grep -q "classid 1:20"; then + # Shared class mode - all ports use mark 20 + SHARED_MARK=20 + for port in "${PORTS[@]}"; do + echo -e "${BLUE} Removing limits for port ${port}...${NC}" + iptables -t mangle -D OUTPUT -p tcp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D OUTPUT -p udp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D FORWARD -p tcp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D FORWARD -p udp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -p tcp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -p udp --sport ${port} -j MARK --set-mark ${SHARED_MARK} 2>/dev/null || true + done + else + # Per-port mode - each port has its own mark + PORT_ID=10 + for port in "${PORTS[@]}"; do + echo -e "${BLUE} Removing limits for port ${port}...${NC}" + + # Remove OUTPUT rules (outgoing from host) + iptables -t mangle -D OUTPUT -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || true + iptables -t mangle -D OUTPUT -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || true + + # Remove FORWARD rules (outgoing from containers) + iptables -t mangle -D FORWARD -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || true + iptables -t mangle -D FORWARD -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || true + + # Remove POSTROUTING rules + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -p tcp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || true + iptables -t mangle -D POSTROUTING -o "$EGRESS_IFACE" -p udp --sport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || true + + PORT_ID=$((PORT_ID + 1)) + done + fi + + # Remove tc qdisc from both interfaces tc qdisc del dev "$BRIDGE" root 2>/dev/null || true + tc qdisc del dev "$EGRESS_IFACE" root 2>/dev/null || true echo -e "${GREEN}✓ Bandwidth limiting removed${NC}" ;; status) echo -e "${YELLOW}Current bandwidth limiting status:${NC}" echo "" - echo -e "${BLUE}TC Qdisc on ${BRIDGE}:${NC}" - tc qdisc show dev "$BRIDGE" 2>/dev/null || echo " No qdisc configured" + echo -e "${BLUE}TC Qdisc on ${EGRESS_IFACE} (egress interface):${NC}" + tc qdisc show dev "$EGRESS_IFACE" 2>/dev/null || echo " No qdisc configured" echo "" - echo -e "${BLUE}TC Classes:${NC}" - tc class show dev "$BRIDGE" 2>/dev/null || echo " No classes configured" + echo -e "${BLUE}TC Classes on ${EGRESS_IFACE}:${NC}" + tc class show dev "$EGRESS_IFACE" 2>/dev/null || echo " No classes configured" + echo "" + echo -e "${BLUE}TC Statistics on ${EGRESS_IFACE}:${NC}" + tc -s class show dev "$EGRESS_IFACE" 2>/dev/null | head -20 || echo " No statistics available" echo "" echo -e "${BLUE}Iptables rules (OUTPUT - outgoing from host):${NC}" iptables -t mangle -L OUTPUT -n --line-numbers | grep -E "MARK|${PORTS[0]}" || echo " No rules found" @@ -365,6 +1054,9 @@ case "$ACTION" in echo -e "${BLUE}Iptables rules (FORWARD - outgoing from containers):${NC}" iptables -t mangle -L FORWARD -n --line-numbers | grep -E "MARK|${PORTS[0]}" || echo " No rules found" echo "" + echo -e "${BLUE}Iptables rules (POSTROUTING - after NAT):${NC}" + iptables -t mangle -L POSTROUTING -n --line-numbers | grep -E "MARK|${PORTS[0]}" || echo " No rules found" + echo "" echo -e "${YELLOW}Note: Only outgoing traffic is limited. Incoming traffic is not limited${NC}" echo -e "${YELLOW} because you can't control what other nodes send you.${NC}" echo "" diff --git a/op/xlayer/testnet/rollup.json b/op/xlayer/testnet/rollup.json index dab29799..0ff61096 100644 --- a/op/xlayer/testnet/rollup.json +++ b/op/xlayer/testnet/rollup.json @@ -1,45 +1,47 @@ { - "genesis": { - "l1": { - "hash": "0x0ec957b104f8125b88f874dde8d8f236e9f952eb941102076406b108afaafc6e", - "number": 9430730 - }, - "l2": { - "hash": "0xccb16eb07b7a718c2ee374df57b0e28c9ac9d8d18ca6d3204cfbba661067855a", - "number": 12241700 - }, - "l2_time": 1760699568, - "system_config": { - "batcherAddr": "0x8edf9b54e1c693b7b0caea85e6a005c35e229124", - "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", - "scalar": "0x010000000000000000000000000000000000000000000000000c3c9d00000558", - "gasLimit": 30000000, - "eip1559Params": "0x0000000000000000", - "operatorFeeParams": "0x0000000000000000000000000000000000000000000000000000000000000000", - "minBaseFee": 0 - } + "genesis": { + "l1": { + "hash": "0x0ec957b104f8125b88f874dde8d8f236e9f952eb941102076406b108afaafc6e", + "number": 9430730 }, - "block_time": 1, - "max_sequencer_drift": 600, - "seq_window_size": 3600, - "channel_timeout": 300, - "l1_chain_id": 11155111, - "l2_chain_id": 1952, - "regolith_time": 0, - "canyon_time": 0, - "delta_time": 0, - "ecotone_time": 0, - "fjord_time": 0, - "granite_time": 0, - "holocene_time": 0, - "isthmus_time": 0, - "batch_inbox_address": "0x006737cc6980a7786a477ce46b491845509b19dc", - "deposit_contract_address": "0x1529a34331d7d85c8868fc88ec730ae56d3ec9c0", - "l1_system_config_address": "0x06be4b4a9a28ff8eed6da09447bc5daa676efac3", - "protocol_versions_address": "0x4e753a62ad7da17508dbc54a58e1e231c152baa2", - "chain_op_config": { - "eip1559Elasticity": 1, - "eip1559Denominator": 100000000, - "eip1559DenominatorCanyon": 250 + "l2": { + "hash": "0xccb16eb07b7a718c2ee374df57b0e28c9ac9d8d18ca6d3204cfbba661067855a", + "number": 12241700 + }, + "l2_time": 1760700537, + "system_config": { + "batcherAddr": "0x8edf9b54e1c693b7b0caea85e6a005c35e229124", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c3c9d00000558", + "gasLimit": 30000000, + "eip1559Params": "0x0000000000000000", + "operatorFeeParams": "0x0000000000000000000000000000000000000000000000000000000000000000", + "minBaseFee": 0, + "daFootprintGasScalar": 0 } - } \ No newline at end of file + }, + "block_time": 1, + "max_sequencer_drift": 600, + "seq_window_size": 3600, + "channel_timeout": 300, + "l1_chain_id": 11155111, + "l2_chain_id": 1952, + "regolith_time": 0, + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 0, + "isthmus_time": 0, + "jovian_time": 1764327600, + "batch_inbox_address": "0x006737cc6980a7786a477ce46b491845509b19dc", + "deposit_contract_address": "0x1529a34331d7d85c8868fc88ec730ae56d3ec9c0", + "l1_system_config_address": "0x06be4b4a9a28ff8eed6da09447bc5daa676efac3", + "protocol_versions_address": "0x4e753a62ad7da17508dbc54a58e1e231c152baa2", + "chain_op_config": { + "eip1559Elasticity": 1, + "eip1559Denominator": 100000000, + "eip1559DenominatorCanyon": 250 + } +} \ No newline at end of file