allow limiting bandwidth usage on public ports inspred by gnosis pulling 500 MBit/s

This commit is contained in:
goldsquid
2026-01-08 09:26:38 +07:00
parent c02425462b
commit e9bd0dd120
2 changed files with 407 additions and 0 deletions

320
limit-bandwidth.sh Executable file
View File

@@ -0,0 +1,320 @@
#!/bin/bash
# General script to limit bandwidth for all public ports in a Docker Compose file
# Limits each port to specified bandwidth (default: 100 MBit/s)
#
# Usage:
# ./limit-bandwidth.sh <compose-file> [start|stop|status] [--limit BANDWIDTH]
# ./limit-bandwidth.sh rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace.yml start
# ./limit-bandwidth.sh rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace.yml start --limit 20mbit
#
# Environment variable:
# BANDWIDTH_LIMIT=20mbit ./limit-bandwidth.sh <compose-file> start
#
# For cronjob:
# */5 * * * * /path/to/rpc/limit-bandwidth.sh /path/to/compose.yml start
# */5 * * * * BANDWIDTH_LIMIT=20mbit /path/to/rpc/limit-bandwidth.sh /path/to/compose.yml start
set -uo pipefail
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}"
BURST_MULTIPLIER="${BURST_MULTIPLIER:-0.1}" # Burst is 10% of limit by default
LATENCY="50ms" # Latency for shaping
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Error: This script must be run as root (use sudo)${NC}"
exit 1
fi
# Check if required tools are installed
if ! command -v tc >/dev/null 2>&1; then
echo -e "${RED}Error: 'tc' command not found${NC}"
echo "Please install iproute2:"
echo " sudo apt-get update && sudo apt-get install -y iproute2"
exit 1
fi
if ! command -v iptables >/dev/null 2>&1; then
echo -e "${RED}Error: 'iptables' command not found${NC}"
echo "Please install iptables:"
echo " sudo apt-get update && sudo apt-get install -y iptables"
exit 1
fi
# Parse arguments
if [ $# -lt 1 ]; then
echo "Usage: $0 <compose-file> [start|stop|status] [--limit BANDWIDTH]"
echo ""
echo "Options:"
echo " --limit BANDWIDTH Set bandwidth limit (e.g., 20mbit, 100mbit)"
echo " Can also use BANDWIDTH_LIMIT environment variable"
echo ""
echo "Examples:"
echo " $0 rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace.yml start"
echo " $0 rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace.yml start --limit 20mbit"
echo " BANDWIDTH_LIMIT=20mbit $0 rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace.yml start"
echo " $0 rpc/ethereum/geth/ethereum-mainnet-geth-pruned-pebble-path.yml status"
exit 1
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
fi
# Resolve compose file path
if [ ! -f "$COMPOSE_FILE" ]; then
# Try relative to BASEPATH
if [ -f "$BASEPATH/$COMPOSE_FILE" ]; then
COMPOSE_FILE="$BASEPATH/$COMPOSE_FILE"
else
echo -e "${RED}Error: Compose file not found: $1${NC}"
exit 1
fi
fi
COMPOSE_FILE=$(realpath "$COMPOSE_FILE")
COMPOSE_DIR=$(dirname "$COMPOSE_FILE")
# Calculate burst from limit (10% of limit)
# Extract number and unit from BANDWIDTH_LIMIT (e.g., "20mbit" -> "20" and "mbit")
if [[ "$BANDWIDTH_LIMIT" =~ ^([0-9]+)([a-zA-Z]+)$ ]]; then
LIMIT_NUM="${BASH_REMATCH[1]}"
LIMIT_UNIT="${BASH_REMATCH[2]}"
# Calculate 10% of limit (simple integer division for compatibility)
BURST_NUM=$((LIMIT_NUM / 10))
# Ensure minimum burst of 1
if [ "$BURST_NUM" -lt 1 ]; then
BURST_NUM=1
fi
BURST="${BURST_NUM}${LIMIT_UNIT}"
else
# Fallback: use 10% of limit as string manipulation
BURST="${BANDWIDTH_LIMIT}"
fi
echo -e "${BLUE}Compose file: ${COMPOSE_FILE}${NC}"
echo -e "${BLUE}Bandwidth limit: ${BANDWIDTH_LIMIT} per port${NC}"
# Function to extract ports from YAML
extract_ports() {
local file="$1"
# Extract ports in format "HOST:CONTAINER" or "HOST:CONTAINER/PROTO"
# Look for lines after "ports:" that contain port mappings
awk '
/^[[:space:]]*ports:[[:space:]]*$/ { in_ports=1; next }
/^[[:space:]]*-/ && in_ports {
# Match patterns like "13516:13516" or "13516:13516/udp"
if (match($0, /[0-9]+:[0-9]+/)) {
port = substr($0, RSTART, RLENGTH)
# Extract just the host port (first number)
if (match(port, /^[0-9]+/)) {
print substr(port, RSTART, RLENGTH)
}
}
}
/^[[:space:]]*[a-zA-Z]/ && in_ports { in_ports=0 }
' "$file" | sort -u
}
# Function to find Docker bridge for compose project
find_bridge() {
local compose_dir="$1"
local project_name=$(basename "$compose_dir" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g')
# Try to find the network by checking running containers from this compose file
local container_name=$(docker compose -f "$COMPOSE_FILE" ps --format json 2>/dev/null | \
jq -r '.[0].Name' 2>/dev/null | head -1)
if [ -n "$container_name" ] && [ "$container_name" != "null" ]; then
# Get network from container
local network=$(docker inspect "$container_name" --format '{{range $net, $conf := .NetworkSettings.Networks}}{{$net}}{{end}}' 2>/dev/null | head -1)
if [ -n "$network" ]; then
# Find bridge interface for this network
local bridge=$(docker network inspect "$network" --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null)
if [ -n "$bridge" ]; then
# Find interface with this gateway IP
local iface=$(ip route | grep "$bridge" | awk '{print $3}' | head -1)
if [ -n "$iface" ]; then
echo "$iface"
return 0
fi
fi
fi
fi
# Fallback: find Docker bridge interfaces
local bridges=$(ip link show | grep -E "^[0-9]+: (br-|docker0)" | cut -d: -f2 | tr -d ' ' | head -1)
if [ -n "$bridges" ]; then
echo "$bridges"
return 0
fi
# Last resort: use docker0
if ip link show docker0 >/dev/null 2>&1; then
echo "docker0"
return 0
fi
echo "eth0" # Final fallback
}
# Extract ports from compose file
PORTS_RAW=$(extract_ports "$COMPOSE_FILE")
if [ -z "$PORTS_RAW" ]; then
echo -e "${YELLOW}Warning: No public ports found in compose file${NC}"
exit 0
fi
# Convert to array (simple approach)
PORTS=()
while IFS= read -r line; do
[ -n "$line" ] && PORTS+=("$line")
done <<< "$PORTS_RAW"
if [ ${#PORTS[@]} -eq 0 ]; then
echo -e "${YELLOW}Warning: No public ports found in compose file${NC}"
exit 0
fi
echo -e "${GREEN}Found ${#PORTS[@]} public port(s): ${PORTS[*]}${NC}"
# Find the bridge interface
BRIDGE=$(find_bridge "$COMPOSE_DIR")
echo -e "${BLUE}Using network interface: ${BRIDGE}${NC}"
# Verify interface exists
if ! ip link show "$BRIDGE" >/dev/null 2>&1; then
echo -e "${RED}Error: Network interface '$BRIDGE' not found${NC}"
exit 1
fi
case "$ACTION" in
start)
echo -e "${YELLOW}Setting up bandwidth limiting...${NC}"
# Remove existing qdisc if any
tc qdisc del dev "$BRIDGE" root 2>/dev/null || true
# Create HTB (Hierarchical Token Bucket) qdisc
tc qdisc add dev "$BRIDGE" root handle 1: htb default 30
# Create root class with high bandwidth
tc class add dev "$BRIDGE" parent 1: classid 1:1 htb rate 1000mbit
# Create unlimited class for non-limited traffic
tc class add dev "$BRIDGE" parent 1:1 classid 1:30 htb rate 1000mbit
# Process each port
PORT_ID=10
for port in "${PORTS[@]}"; do
echo -e "${BLUE} Limiting port ${port} to ${BANDWIDTH_LIMIT}...${NC}"
# Create limited class for this port
tc class add dev "$BRIDGE" parent 1:1 classid 1:${PORT_ID} htb rate ${BANDWIDTH_LIMIT} burst ${BURST} ceil ${BANDWIDTH_LIMIT}
# Add filter to route marked packets to this class
tc filter add dev "$BRIDGE" parent 1: protocol ip prio ${PORT_ID} handle ${PORT_ID} fw flowid 1:${PORT_ID}
# Mark TCP traffic for this port
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 tcp --dport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || \
iptables -t mangle -A OUTPUT -p tcp --dport ${port} -j MARK --set-mark ${PORT_ID}
# Mark UDP traffic for this port
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}
iptables -t mangle -C OUTPUT -p udp --dport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || \
iptables -t mangle -A OUTPUT -p udp --dport ${port} -j MARK --set-mark ${PORT_ID}
# Mark in FORWARD chain (container traffic)
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 tcp --dport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || \
iptables -t mangle -A FORWARD -p tcp --dport ${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}
iptables -t mangle -C FORWARD -p udp --dport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || \
iptables -t mangle -A FORWARD -p udp --dport ${port} -j MARK --set-mark ${PORT_ID}
PORT_ID=$((PORT_ID + 1))
done
echo -e "${GREEN}✓ Bandwidth limiting configured!${NC}"
echo -e "${GREEN}All ports are now limited to ${BANDWIDTH_LIMIT} each${NC}"
;;
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}"
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 tcp --dport ${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
iptables -t mangle -D OUTPUT -p udp --dport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || true
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 tcp --dport ${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
iptables -t mangle -D FORWARD -p udp --dport ${port} -j MARK --set-mark ${PORT_ID} 2>/dev/null || true
PORT_ID=$((PORT_ID + 1))
done
# Remove tc qdisc
tc qdisc del dev "$BRIDGE" 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 ""
echo -e "${BLUE}TC Classes:${NC}"
tc class show dev "$BRIDGE" 2>/dev/null || echo " No classes configured"
echo ""
echo -e "${BLUE}Iptables rules (OUTPUT):${NC}"
iptables -t mangle -L OUTPUT -n --line-numbers | grep -E "MARK|${PORTS[0]}" || echo " No rules found"
echo ""
echo -e "${BLUE}Iptables rules (FORWARD):${NC}"
iptables -t mangle -L FORWARD -n --line-numbers | grep -E "MARK|${PORTS[0]}" || echo " No rules found"
echo ""
echo -e "${BLUE}Monitored ports: ${PORTS[*]}${NC}"
;;
*)
echo -e "${RED}Error: Invalid action '$ACTION'${NC}"
echo "Usage: $0 <compose-file> [start|stop|status]"
exit 1
;;
esac

87
setup-bandwidth-limit-cron.sh Executable file
View File

@@ -0,0 +1,87 @@
#!/bin/bash
# Helper script to set up cronjob for bandwidth limiting
# This will add a cronjob entry to apply bandwidth limits every 5 minutes
#
# Usage: ./setup-bandwidth-limit-cron.sh <compose-file> [BANDWIDTH_LIMIT]
# Example: ./setup-bandwidth-limit-cron.sh rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace.yml
# Example: ./setup-bandwidth-limit-cron.sh rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace.yml 20mbit
set -euo pipefail
BASEPATH="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_PATH="$BASEPATH/limit-bandwidth.sh"
if [ $# -lt 1 ]; then
echo "Usage: $0 <compose-file>"
echo ""
echo "This script sets up a cronjob to apply bandwidth limits every 5 minutes."
echo "The cronjob will run: $SCRIPT_PATH <compose-file> start"
echo ""
echo "Example:"
echo " $0 rpc/gnosis/reth/gnosis-mainnet-reth-pruned-trace.yml"
exit 1
fi
COMPOSE_FILE="$1"
BANDWIDTH_LIMIT="${2:-100mbit}" # Default to 100mbit if not specified
# Resolve compose file path
if [ ! -f "$COMPOSE_FILE" ]; then
if [ -f "$BASEPATH/$COMPOSE_FILE" ]; then
COMPOSE_FILE="$BASEPATH/$COMPOSE_FILE"
else
echo "Error: Compose file not found: $1"
exit 1
fi
fi
COMPOSE_FILE=$(realpath "$COMPOSE_FILE")
SCRIPT_PATH=$(realpath "$SCRIPT_PATH")
# Create cronjob entry with bandwidth limit
if [ "$BANDWIDTH_LIMIT" != "100mbit" ]; then
CRON_ENTRY="*/5 * * * * sudo BANDWIDTH_LIMIT=$BANDWIDTH_LIMIT $SCRIPT_PATH $COMPOSE_FILE start"
else
CRON_ENTRY="*/5 * * * * sudo $SCRIPT_PATH $COMPOSE_FILE start"
fi
echo "Setting up cronjob for bandwidth limiting..."
echo ""
echo "Compose file: $COMPOSE_FILE"
echo "Script: $SCRIPT_PATH"
echo "Bandwidth limit: $BANDWIDTH_LIMIT per port"
echo "Cron entry: $CRON_ENTRY"
echo ""
# Check if cronjob already exists
if crontab -l 2>/dev/null | grep -qF "$COMPOSE_FILE start"; then
echo "Warning: A cronjob for this compose file already exists."
echo ""
echo "Current crontab entries:"
crontab -l 2>/dev/null | grep "$COMPOSE_FILE" || true
echo ""
read -p "Do you want to replace it? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Cancelled."
exit 0
fi
# Remove existing entry
crontab -l 2>/dev/null | grep -vF "$COMPOSE_FILE start" | crontab -
fi
# Add new cronjob
(crontab -l 2>/dev/null; echo "$CRON_ENTRY") | crontab -
echo "✓ Cronjob added successfully!"
echo ""
echo "To view your crontab:"
echo " crontab -l"
echo ""
echo "To remove this cronjob:"
echo " crontab -e"
echo " (then delete the line containing: $COMPOSE_FILE start)"
echo ""
echo "To test the script manually:"
echo " sudo $SCRIPT_PATH $COMPOSE_FILE start"