This commit is contained in:
2026-01-08 12:47:07 +01:00
parent 9c285b6004
commit ee98772660
2 changed files with 813 additions and 119 deletions

View File

@@ -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 <compose-file> [start|stop|status] [--limit BANDWIDTH]
# ./limit-bandwidth.sh <compose-file> [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 <compose-file> start
# TOTAL_LIMIT=false BANDWIDTH_LIMIT=5mbit ./limit-bandwidth.sh <compose-file> 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 <compose-file> [start|stop|status] [--limit BANDWIDTH]"
echo "Usage: $0 <compose-file> [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 ""

View File

@@ -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
}
}
},
"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
}
}