From b827318214da7899a169ec841b063d2433e3a87e Mon Sep 17 00:00:00 2001 From: Manuel Alejandro de Brito Fontes Date: Tue, 11 Mar 2025 21:09:43 -0700 Subject: [PATCH 1/7] Improve firewall to avoid affecting existing iptables rules --- .devcontainer/init-firewall.sh | 531 ++++++++++++++++++++++++++------- 1 file changed, 430 insertions(+), 101 deletions(-) diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh index e45908c..6415da2 100644 --- a/.devcontainer/init-firewall.sh +++ b/.devcontainer/init-firewall.sh @@ -1,119 +1,448 @@ #!/bin/bash -set -euo pipefail # Exit on error, undefined vars, and pipeline failures -IFS=$'\n\t' # Stricter word splitting - -# Flush existing rules and delete existing ipsets -iptables -F -iptables -X -iptables -t nat -F -iptables -t nat -X -iptables -t mangle -F -iptables -t mangle -X -ipset destroy allowed-domains 2>/dev/null || true - -# First allow DNS and localhost before any restrictions -# Allow outbound DNS -iptables -A OUTPUT -p udp --dport 53 -j ACCEPT -# Allow inbound DNS responses -iptables -A INPUT -p udp --sport 53 -j ACCEPT -# Allow outbound SSH -iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT -# Allow inbound SSH responses -iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT -# Allow localhost -iptables -A INPUT -i lo -j ACCEPT -iptables -A OUTPUT -o lo -j ACCEPT - -# Create ipset with CIDR support -ipset create allowed-domains hash:net - -# Fetch GitHub meta information and aggregate + add their IP ranges -echo "Fetching GitHub IP ranges..." -gh_ranges=$(curl -s https://api.github.com/meta) -if [ -z "$gh_ranges" ]; then - echo "ERROR: Failed to fetch GitHub IP ranges" - exit 1 +set -uo pipefail +IFS=$'\n\t' + +# Global variables +DEBUG=${DEBUG:-false} +ADDED_IPS_FILE="/tmp/claude-fw-added-ips.txt" +IPV6_ENABLED=false +IPSET_AVAILABLE=true + +# Logging functions +log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"; } +error() { log "ERROR: $1"; } +warning() { log "WARNING: $1"; } +debug_log() { [ "$DEBUG" = true ] && log "DEBUG: $1"; } + +# Execute command with fallback +try_cmd() { + debug_log "Trying: $1" + if eval "$1" &>/dev/null; then return 0; fi + if [ -n "$2" ]; then + debug_log "Trying fallback: $2" + if eval "$2" &>/dev/null; then return 0; fi + fi + warning "Failed: ${3:-Command}" + return 1 +} + +# Add IP to allowed list with deduplication +add_ip() { + local ip="$1" + [ -f "$ADDED_IPS_FILE" ] && grep -q "^$ip$" "$ADDED_IPS_FILE" && return 0 + + if [ "$IPSET_AVAILABLE" = true ] && ipset add claude-allowed-domains "$ip" 2>/dev/null; then + echo "$ip" >>"$ADDED_IPS_FILE" + return 0 + elif iptables -A CLAUDE_OUTPUT -d "$ip" -j ACCEPT 2>/dev/null; then + echo "$ip" >>"$ADDED_IPS_FILE" + return 0 + else + debug_log "Failed to add IP: $ip" + return 1 + fi +} + +# Add IPv6 if supported +add_ipv6() { + [ "$IPV6_ENABLED" != true ] && return 0 + local ip="$1" + + # Check if IP is too long for ip6tables + if [ ${#ip} -gt 39 ]; then + debug_log "IPv6 address too long: $ip" + return 1 + fi + + [ -f "$ADDED_IPS_FILE" ] && grep -q "^$ip$" "$ADDED_IPS_FILE" && return 0 + + # First make sure we have IPv6 chains created + if ! ip6tables -L CLAUDE_OUTPUT &>/dev/null; then + create_ipv6_chains + fi + + if ip6tables -A CLAUDE_OUTPUT -d "$ip" -j ACCEPT 2>/dev/null; then + echo "$ip" >>"$ADDED_IPS_FILE" + return 0 + else + debug_log "Failed to add IPv6: $ip" + return 1 + fi +} + +# Create IPv6 chains +create_ipv6_chains() { + log "Creating IPv6 chains..." + for chain in CLAUDE_INPUT CLAUDE_OUTPUT CLAUDE_FORWARD; do + ip6tables -N $chain 2>/dev/null || ip6tables -F $chain 2>/dev/null + done + + ip6tables -D INPUT -j CLAUDE_INPUT 2>/dev/null + ip6tables -D OUTPUT -j CLAUDE_OUTPUT 2>/dev/null + ip6tables -D FORWARD -j CLAUDE_FORWARD 2>/dev/null + + ip6tables -I INPUT 1 -j CLAUDE_INPUT 2>/dev/null || ip6tables -A INPUT -j CLAUDE_INPUT 2>/dev/null + ip6tables -I OUTPUT 1 -j CLAUDE_OUTPUT 2>/dev/null || ip6tables -A OUTPUT -j CLAUDE_OUTPUT 2>/dev/null + ip6tables -I FORWARD 1 -j CLAUDE_FORWARD 2>/dev/null || ip6tables -A FORWARD -j CLAUDE_FORWARD 2>/dev/null + + # IPv6 basic rules + ip6tables -A CLAUDE_INPUT -i lo -j ACCEPT 2>/dev/null + ip6tables -A CLAUDE_OUTPUT -o lo -j ACCEPT 2>/dev/null + ip6tables -A CLAUDE_INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null + ip6tables -A CLAUDE_OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null + ip6tables -A CLAUDE_OUTPUT -p udp --dport 53 -j ACCEPT 2>/dev/null + ip6tables -A CLAUDE_OUTPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null + ip6tables -A CLAUDE_INPUT -p udp --sport 53 -j ACCEPT 2>/dev/null + ip6tables -A CLAUDE_INPUT -p tcp --sport 53 -j ACCEPT 2>/dev/null +} + +# Resolve domain and add IPs +add_domain() { + local domain="$1" + log "Resolving $domain..." + + local ips=$(dig +short A "$domain" || echo "") + if [ -z "$ips" ]; then + warning "Failed to resolve $domain" + return 1 + fi + + local count=0 + while read -r ip; do + if [[ -n "$ip" && "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + add_ip "$ip" && count=$((count + 1)) + fi + done < <(echo "$ips") + + # Also try IPv6 resolution if enabled + if [ "$IPV6_ENABLED" = true ]; then + local ipv6s=$(dig +short AAAA "$domain" || echo "") + if [ -n "$ipv6s" ]; then + while read -r ip; do + if [[ -n "$ip" && "$ip" =~ : ]]; then + add_ipv6 "$ip" && count=$((count + 1)) + fi + done < <(echo "$ipv6s") + fi + fi + + debug_log "Added $count IPs for $domain" + return 0 +} + +# Add networks for interface +add_interface() { + local iface="$1" + log "Adding networks for interface $iface..." + + local addresses=$(ip -o addr show dev "$iface" | grep -w inet | awk '{print $4}') + [ -z "$addresses" ] && debug_log "No addresses for $iface" && return 1 + + local count=0 + for addr in $addresses; do + if try_cmd "iptables -A CLAUDE_INPUT -s $addr -j ACCEPT" "" "INPUT rule for $addr" && + try_cmd "iptables -A CLAUDE_OUTPUT -d $addr -j ACCEPT" "" "OUTPUT rule for $addr"; then + count=$((count + 1)) + fi + done + + # Also add IPv6 rules for the interface if enabled + if [ "$IPV6_ENABLED" = true ]; then + local ipv6_addresses=$(ip -o addr show dev "$iface" | grep -w inet6 | awk '{print $4}') + + for addr in $ipv6_addresses; do + if [ ${#addr} -le 39 ]; then # Check length to prevent hostname too long error + if ip6tables -A CLAUDE_INPUT -s $addr -j ACCEPT 2>/dev/null && + ip6tables -A CLAUDE_OUTPUT -d $addr -j ACCEPT 2>/dev/null; then + count=$((count + 1)) + fi + fi + done + fi + + log "Added $count network rules for $iface" + return 0 +} + +# Clean up rules +cleanup() { + log "Cleaning up..." + iptables -D INPUT -j CLAUDE_INPUT 2>/dev/null || true + iptables -D OUTPUT -j CLAUDE_OUTPUT 2>/dev/null || true + iptables -D FORWARD -j CLAUDE_FORWARD 2>/dev/null || true + iptables -F CLAUDE_INPUT 2>/dev/null || true + iptables -F CLAUDE_OUTPUT 2>/dev/null || true + iptables -F CLAUDE_FORWARD 2>/dev/null || true + iptables -X CLAUDE_INPUT 2>/dev/null || true + iptables -X CLAUDE_OUTPUT 2>/dev/null || true + iptables -X CLAUDE_FORWARD 2>/dev/null || true + + if [ "$IPV6_ENABLED" = true ]; then + ip6tables -D INPUT -j CLAUDE_INPUT 2>/dev/null || true + ip6tables -D OUTPUT -j CLAUDE_OUTPUT 2>/dev/null || true + ip6tables -D FORWARD -j CLAUDE_FORWARD 2>/dev/null || true + ip6tables -F CLAUDE_INPUT 2>/dev/null || true + ip6tables -F CLAUDE_OUTPUT 2>/dev/null || true + ip6tables -F CLAUDE_FORWARD 2>/dev/null || true + ip6tables -X CLAUDE_INPUT 2>/dev/null || true + ip6tables -X CLAUDE_OUTPUT 2>/dev/null || true + ip6tables -X CLAUDE_FORWARD 2>/dev/null || true + fi + + ipset destroy claude-allowed-domains 2>/dev/null || true + rm -f "$ADDED_IPS_FILE" + log "Cleanup complete" +} + +# Test connectivity +test_conn() { + local domain="$1" + local allowed="$2" + + log "Testing connectivity to $domain (should be ${allowed})" + if [ "$allowed" = true ]; then + # test we can reach the domain with a 5 second timeout using https + curl --connect-timeout 5 -s "https://$domain" >/dev/null 2>&1 + local status=$? + if [ $status -ne 0 ]; then + warning "Expected curl to succeed for https://$domain, but got $status" + return 1 + fi + log "Connection to https://$domain successful as expected" + + # Also test HTTP if this is an allowed domain + curl --connect-timeout 5 -s "http://$domain" >/dev/null 2>&1 + local http_status=$? + log "HTTP connection to $domain returned status $http_status (should work for allowed domains)" + else + # test we can't reach the domain with a 5 second timeout using https + curl --connect-timeout 5 -s "https://$domain" >/dev/null 2>&1 + local status=$? + if [ $status -eq 0 ]; then + warning "Expected curl to fail for https://$domain, but got $status" + return 1 + fi + log "Connection to https://$domain failed as expected" + + # Also test HTTP is blocked + curl --connect-timeout 5 -s "http://$domain" >/dev/null 2>&1 + local http_status=$? + if [ $http_status -eq 0 ]; then + warning "Expected curl to fail for http://$domain, but got success" + return 1 + fi + log "HTTP connection to $domain blocked as expected" + fi + return 0 +} + +# Set up trap for cleanup +trap cleanup INT TERM EXIT + +# Check for command availability +for cmd in iptables curl dig; do + command -v "$cmd" &>/dev/null || { + error "Required command '$cmd' not found" + exit 1 + } +done + +for cmd in ipset ip6tables; do + if ! command -v "$cmd" &>/dev/null; then + warning "Optional command '$cmd' not found, limited functionality" + [ "$cmd" = "ipset" ] && IPSET_AVAILABLE=false + [ "$cmd" = "ip6tables" ] && IPV6_ENABLED=false + fi +done + +# Check IPv6 support +if [ "$IPV6_ENABLED" != true ] && ip -6 addr show &>/dev/null && command -v ip6tables &>/dev/null; then + if ip6tables -L INPUT &>/dev/null; then + log "IPv6 detected and enabled" + IPV6_ENABLED=true + fi +fi + +# Initialize tracking file +>"$ADDED_IPS_FILE" + +# Start configuration +log "Starting Claude firewall configuration..." + +# Create custom chains +log "Creating custom chains..." +for chain in CLAUDE_INPUT CLAUDE_OUTPUT CLAUDE_FORWARD; do + try_cmd "iptables -N $chain" "iptables -F $chain" "Creating chain $chain" +done + +# Add chain references +log "Adding chain references..." +iptables -D INPUT -j CLAUDE_INPUT 2>/dev/null || true +iptables -D OUTPUT -j CLAUDE_OUTPUT 2>/dev/null || true +iptables -D FORWARD -j CLAUDE_FORWARD 2>/dev/null || true + +try_cmd "iptables -I INPUT 1 -j CLAUDE_INPUT" "iptables -A INPUT -j CLAUDE_INPUT" "Jump to CLAUDE_INPUT" +try_cmd "iptables -I OUTPUT 1 -j CLAUDE_OUTPUT" "iptables -A OUTPUT -j CLAUDE_OUTPUT" "Jump to CLAUDE_OUTPUT" +try_cmd "iptables -I FORWARD 1 -j CLAUDE_FORWARD" "iptables -A FORWARD -j CLAUDE_FORWARD" "Jump to CLAUDE_FORWARD" + +# Create ipset +if [ "$IPSET_AVAILABLE" = true ]; then + log "Creating ipset..." + ipset destroy claude-allowed-domains 2>/dev/null || true + if ! ipset create claude-allowed-domains hash:net; then + warning "Failed to create ipset, using direct rules" + IPSET_AVAILABLE=false + fi +fi + +# Basic connectivity rules +log "Setting up basic connectivity..." +try_cmd "iptables -A CLAUDE_INPUT -i lo -j ACCEPT" "iptables -I INPUT 1 -i lo -j ACCEPT" "Localhost input" +try_cmd "iptables -A CLAUDE_OUTPUT -o lo -j ACCEPT" "iptables -I OUTPUT 1 -o lo -j ACCEPT" "Localhost output" +try_cmd "iptables -A CLAUDE_OUTPUT -p udp --dport 53 -j ACCEPT" "" "DNS out UDP" +try_cmd "iptables -A CLAUDE_OUTPUT -p tcp --dport 53 -j ACCEPT" "" "DNS out TCP" +try_cmd "iptables -A CLAUDE_INPUT -p udp --sport 53 -j ACCEPT" "" "DNS in UDP" +try_cmd "iptables -A CLAUDE_INPUT -p tcp --sport 53 -j ACCEPT" "" "DNS in TCP" +try_cmd "iptables -A CLAUDE_OUTPUT -p tcp --dport 22 -j ACCEPT" "" "SSH out" +try_cmd "iptables -A CLAUDE_INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT" \ + "iptables -A CLAUDE_INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" "ESTABLISHED in" +try_cmd "iptables -A CLAUDE_OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT" \ + "iptables -A CLAUDE_OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" "ESTABLISHED out" + +# Initialize IPv6 chains if enabled +if [ "$IPV6_ENABLED" = true ]; then + create_ipv6_chains fi -if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then - echo "ERROR: GitHub API response missing required fields" - exit 1 +# Add GitHub IPs +log "Adding GitHub IPs..." +gh_ranges=$(curl -s --connect-timeout 5 https://api.github.com/meta) +if [ -n "$gh_ranges" ] && echo "$gh_ranges" | grep -q "api"; then + while read -r cidr; do + [[ -n "$cidr" ]] && add_ip "$cidr" + done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' 2>/dev/null || echo "") +else + # Fallback GitHub IPs + for ip in "140.82.112.0/20" "192.30.252.0/22" "185.199.108.0/22" "143.55.64.0/20"; do + add_ip "$ip" + done fi -echo "Processing GitHub IPs..." -while read -r cidr; do - if [[ ! "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then - echo "ERROR: Invalid CIDR range from GitHub meta: $cidr" - exit 1 - fi - echo "Adding GitHub range $cidr" - ipset add allowed-domains "$cidr" -done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q) - -# Resolve and add other allowed domains -for domain in \ - "registry.npmjs.org" \ - "api.anthropic.com" \ - "sentry.io" \ - "statsig.anthropic.com" \ - "statsig.com"; do - echo "Resolving $domain..." - ips=$(dig +short A "$domain") - if [ -z "$ips" ]; then - echo "ERROR: Failed to resolve $domain" - exit 1 - fi - - while read -r ip; do - if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then - echo "ERROR: Invalid IP from DNS for $domain: $ip" - exit 1 - fi - echo "Adding $ip for $domain" - ipset add allowed-domains "$ip" - done < <(echo "$ips") +# Add important domains +log "Adding important domains..." +for domain in "registry.npmjs.org" "api.anthropic.com" "sentry.io" "statsig.anthropic.com" \ + "cursor.blob.core.windows.net" "statsig.com" "marketplace.visualstudio.com" \ + "vscode.blob.core.windows.net" "vsmarketplacebadge.apphb.com" "example.com"; do + add_domain "$domain" done -# Get host IP from default route -HOST_IP=$(ip route | grep default | cut -d" " -f3) -if [ -z "$HOST_IP" ]; then - echo "ERROR: Failed to detect host IP" - exit 1 +# Azure IP Ranges +log "Adding Azure IPs..." +azure_json=$(curl -sSL --connect-timeout 5 https://download.microsoft.com/download/7/1/d/71d86715-5596-4529-9b13-da13a5de5b63/ServiceTags_Public_20250303.json 2>/dev/null) +if [ -n "$azure_json" ]; then + # Extract IPv4 addresses only for Azure Front Door + AZURE_RANGES=$(echo "$azure_json" | jq -r '.values[] | select(.name=="AzureFrontDoor.Frontend") | .properties.addressPrefixes[] | select(contains(":") | not)' 2>/dev/null) + + for azure_ip in $AZURE_RANGES; do + add_ip "$azure_ip" + done + + # Handle IPv6 addresses separately if enabled + if [ "$IPV6_ENABLED" = true ]; then + AZURE_IPV6_RANGES=$(echo "$azure_json" | jq -r '.values[] | select(.name=="AzureFrontDoor.Frontend") | .properties.addressPrefixes[] | select(contains(":"))' 2>/dev/null) + for azure_ipv6 in $AZURE_IPV6_RANGES; do + # Only process IPv6 addresses that aren't too long + if [ ${#azure_ipv6} -le 39 ]; then + add_ipv6 "$azure_ipv6" + fi + done + fi fi -HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/") -echo "Host network detected as: $HOST_NETWORK" +# Add AWS S3 IPs +log "Adding AWS S3 IPs..." +aws_json=$(curl -s --connect-timeout 5 "https://ip-ranges.amazonaws.com/ip-ranges.json" 2>/dev/null) +if [ -n "$aws_json" ]; then + while read -r ip; do + [[ -n "$ip" ]] && add_ip "$ip" + done < <(echo "$aws_json" | jq -r '.prefixes[] | select(.service=="S3") | .ip_prefix' 2>/dev/null || echo "") + + if [ "$IPV6_ENABLED" = true ]; then + while read -r ip; do + if [[ -n "$ip" ]] && [ ${#ip} -le 39 ]; then + add_ipv6 "$ip" + fi + done < <(echo "$aws_json" | jq -r '.ipv6_prefixes[] | select(.service=="S3") | .ipv6_prefix' 2>/dev/null || echo "") + fi +fi -# Set up remaining iptables rules -iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT -iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT +# Host network configuration +log "Configuring host network..." +HOST_IP=$(ip route | grep default | awk '{print $3}' || hostname -I | awk '{print $1}') +if [ -n "$HOST_IP" ]; then + log "Host IP: $HOST_IP" + IFS='.' read -r a b c d <<<"$HOST_IP" + HOST_NETWORK="${a}.${b}.${c}.0/24" -# Set default policies to DROP first -# Set default policies to DROP first -iptables -P INPUT DROP -iptables -P FORWARD DROP -iptables -P OUTPUT DROP + try_cmd "iptables -A CLAUDE_INPUT -s $HOST_NETWORK -j ACCEPT" "" "Host network IN" + try_cmd "iptables -A CLAUDE_OUTPUT -d $HOST_NETWORK -j ACCEPT" "" "Host network OUT" -# First allow established connections for already approved traffic -iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT -iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + # Default gateway + DEFAULT_GATEWAY=$(ip route | grep default | awk '{print $3}') + if [ -n "$DEFAULT_GATEWAY" ]; then + try_cmd "iptables -A CLAUDE_INPUT -s $DEFAULT_GATEWAY -j ACCEPT" "" "Gateway IN" + try_cmd "iptables -A CLAUDE_OUTPUT -d $DEFAULT_GATEWAY -j ACCEPT" "" "Gateway OUT" + fi +fi -# Then allow only specific outbound traffic to allowed domains -iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT +# Interface networks +log "Adding interface networks..." +for iface in $(ip -o link show | grep -v lo | awk -F': ' '{print $2}'); do + add_interface "$iface" +done -echo "Firewall configuration complete" -echo "Verifying firewall rules..." -if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then - echo "ERROR: Firewall verification failed - was able to reach https://example.com" - exit 1 -else - echo "Firewall verification passed - unable to reach https://example.com as expected" +# HTTPS traffic +log "Adding HTTPS rules..." +try_cmd "iptables -A CLAUDE_INPUT -p tcp --sport 443 -j ACCEPT" "" "HTTPS in" +try_cmd "iptables -A CLAUDE_OUTPUT -p tcp --dport 443 -j ACCEPT" "" "HTTPS out" + +# HTTP traffic - carefully controlled +log "Configuring HTTP rules..." +# We DO NOT add a blanket rule allowing port 80 outbound +# Instead, rely on the explicitly allowed domains via ipset or individual rules + +# ipset rule (if available) +if [ "$IPSET_AVAILABLE" = true ]; then + try_cmd "iptables -A CLAUDE_OUTPUT -m set --match-set claude-allowed-domains dst -j ACCEPT" "" "ipset rule" +fi + +# Optional logging - place before the DROP rule +try_cmd "iptables -A CLAUDE_OUTPUT -m limit --limit 5/min -j LOG --log-prefix \"CLAUDE_FIREWALL: \" --log-level 4" "" "Logging" +if [ "$IPV6_ENABLED" = true ]; then + ip6tables -A CLAUDE_OUTPUT -m limit --limit 5/min -j LOG --log-prefix "CLAUDE_FIREWALL_IPV6: " --log-level 4 2>/dev/null || true fi -# Verify GitHub API access -if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then - echo "ERROR: Firewall verification failed - unable to reach https://api.github.com" - exit 1 +# Final drop rule +log "Adding default drop rule..." +try_cmd "iptables -A CLAUDE_OUTPUT -j DROP" "iptables -A CLAUDE_OUTPUT -j REJECT" "Default DROP" +if [ "$IPV6_ENABLED" = true ]; then + ip6tables -A CLAUDE_OUTPUT -j DROP 2>/dev/null || ip6tables -A CLAUDE_OUTPUT -j REJECT 2>/dev/null || true +fi + +# Verification +log "Verifying configuration..." +test_conn "api.github.com" true +test_conn "marketplace.visualstudio.com" true +test_conn "example.com" false + +# Test HTTP is properly blocked for non-allowed domains +log "Testing HTTP blocking..." +curl --connect-timeout 5 -s "http://non-allowed-domain.example" >/dev/null 2>&1 +if [ $? -eq 0 ]; then + warning "HTTP blocking failed - port 80 traffic is allowed when it shouldn't be" else - echo "Firewall verification passed - able to reach https://api.github.com as expected" + log "HTTP blocking successful - port 80 is properly restricted" fi + +log "Claude firewall configuration finished" +exit 0 \ No newline at end of file From 67f796bd5ed9bb458395c77bb88e0b5ce1fc4301 Mon Sep 17 00:00:00 2001 From: Manuel Alejandro de Brito Fontes Date: Sun, 16 Mar 2025 13:08:42 -0700 Subject: [PATCH 2/7] Address feedback --- .devcontainer/init-firewall.sh | 199 ++++++++++++++++----------------- 1 file changed, 97 insertions(+), 102 deletions(-) diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh index 6415da2..0a29540 100644 --- a/.devcontainer/init-firewall.sh +++ b/.devcontainer/init-firewall.sh @@ -14,16 +14,14 @@ error() { log "ERROR: $1"; } warning() { log "WARNING: $1"; } debug_log() { [ "$DEBUG" = true ] && log "DEBUG: $1"; } -# Execute command with fallback -try_cmd() { - debug_log "Trying: $1" - if eval "$1" &>/dev/null; then return 0; fi - if [ -n "$2" ]; then - debug_log "Trying fallback: $2" - if eval "$2" &>/dev/null; then return 0; fi +# Execute command with no fallback - all failures are fatal +execute_cmd() { + debug_log "Executing: $1" + if ! eval "$1" &>/dev/null; then + error "Failed: ${2:-Command}" + exit 1 fi - warning "Failed: ${3:-Command}" - return 1 + return 0 } # Add IP to allowed list with deduplication @@ -140,10 +138,9 @@ add_interface() { local count=0 for addr in $addresses; do - if try_cmd "iptables -A CLAUDE_INPUT -s $addr -j ACCEPT" "" "INPUT rule for $addr" && - try_cmd "iptables -A CLAUDE_OUTPUT -d $addr -j ACCEPT" "" "OUTPUT rule for $addr"; then - count=$((count + 1)) - fi + execute_cmd "iptables -A CLAUDE_INPUT -s $addr -j ACCEPT" "INPUT rule for $addr" + execute_cmd "iptables -A CLAUDE_OUTPUT -d $addr -j ACCEPT" "OUTPUT rule for $addr" + count=$((count + 1)) done # Also add IPv6 rules for the interface if enabled @@ -151,11 +148,10 @@ add_interface() { local ipv6_addresses=$(ip -o addr show dev "$iface" | grep -w inet6 | awk '{print $4}') for addr in $ipv6_addresses; do - if [ ${#addr} -le 39 ]; then # Check length to prevent hostname too long error - if ip6tables -A CLAUDE_INPUT -s $addr -j ACCEPT 2>/dev/null && - ip6tables -A CLAUDE_OUTPUT -d $addr -j ACCEPT 2>/dev/null; then - count=$((count + 1)) - fi + if [ ${#addr} -le 39 ]; then # Check length to prevent hostname too long error + execute_cmd "ip6tables -A CLAUDE_INPUT -s $addr -j ACCEPT" "IPv6 INPUT rule for $addr" + execute_cmd "ip6tables -A CLAUDE_OUTPUT -d $addr -j ACCEPT" "IPv6 OUTPUT rule for $addr" + count=$((count + 1)) fi done fi @@ -201,31 +197,27 @@ test_conn() { log "Testing connectivity to $domain (should be ${allowed})" if [ "$allowed" = true ]; then - # test we can reach the domain with a 5 second timeout using https - curl --connect-timeout 5 -s "https://$domain" >/dev/null 2>&1 + # Force a new connection using --no-keepalive + curl --connect-timeout 5 --no-keepalive -s "https://$domain" >/dev/null 2>&1 local status=$? if [ $status -ne 0 ]; then warning "Expected curl to succeed for https://$domain, but got $status" return 1 fi log "Connection to https://$domain successful as expected" - - # Also test HTTP if this is an allowed domain - curl --connect-timeout 5 -s "http://$domain" >/dev/null 2>&1 + curl --connect-timeout 5 --no-keepalive -s "http://$domain" >/dev/null 2>&1 local http_status=$? log "HTTP connection to $domain returned status $http_status (should work for allowed domains)" else - # test we can't reach the domain with a 5 second timeout using https - curl --connect-timeout 5 -s "https://$domain" >/dev/null 2>&1 + # For disallowed domains, force a fresh connection attempt + curl --connect-timeout 5 --no-keepalive -s "https://$domain" >/dev/null 2>&1 local status=$? if [ $status -eq 0 ]; then warning "Expected curl to fail for https://$domain, but got $status" return 1 fi log "Connection to https://$domain failed as expected" - - # Also test HTTP is blocked - curl --connect-timeout 5 -s "http://$domain" >/dev/null 2>&1 + curl --connect-timeout 5 --no-keepalive -s "http://$domain" >/dev/null 2>&1 local http_status=$? if [ $http_status -eq 0 ]; then warning "Expected curl to fail for http://$domain, but got success" @@ -236,8 +228,8 @@ test_conn() { return 0 } -# Set up trap for cleanup -trap cleanup INT TERM EXIT +# Set up trap for cleanup only on errors or interruptions, not normal exit +trap 'cleanup; exit 1' INT TERM HUP # Check for command availability for cmd in iptables curl dig; do @@ -269,10 +261,40 @@ fi # Start configuration log "Starting Claude firewall configuration..." +cleanup + +# Temporary allow ALL outbound HTTPS during initial setup +log "Temporarily allowing all outbound HTTPS traffic for initial setup..." +iptables -I OUTPUT -p tcp --dport 443 -j ACCEPT + +# Download required files +log "Fetching GitHub IPs..." +gh_ranges=$(curl -sSL --connect-timeout 5 https://api.github.com/meta) +if [ -n "$gh_ranges" ] && echo "$gh_ranges" | jq -e . >/dev/null 2>&1; then + log "Successfully fetched GitHub IP ranges" +else + error "Failed to fetch GitHub IP ranges - cannot continue without current GitHub IPs" + exit 1 +fi + +# Fetch Azure IP ranges +log "Fetching Azure IPs..." +azure_json=$(curl -sSL --connect-timeout 5 https://download.microsoft.com/download/7/1/d/71d86715-5596-4529-9b13-da13a5de5b63/ServiceTags_Public_20250303.json 2>/dev/null) +if [ -z "$azure_json" ]; then + warning "Failed to fetch Azure IP ranges - will continue without them" +fi + +# Remove temporary all-allow rule +log "Removing temporary HTTPS allow rule..." +iptables -D OUTPUT -p tcp --dport 443 -j ACCEPT + # Create custom chains log "Creating custom chains..." for chain in CLAUDE_INPUT CLAUDE_OUTPUT CLAUDE_FORWARD; do - try_cmd "iptables -N $chain" "iptables -F $chain" "Creating chain $chain" + # Try to create the chain, if it exists already then flush it + if ! iptables -N $chain 2>/dev/null; then + execute_cmd "iptables -F $chain" "Flushing existing chain $chain" + fi done # Add chain references @@ -281,9 +303,10 @@ iptables -D INPUT -j CLAUDE_INPUT 2>/dev/null || true iptables -D OUTPUT -j CLAUDE_OUTPUT 2>/dev/null || true iptables -D FORWARD -j CLAUDE_FORWARD 2>/dev/null || true -try_cmd "iptables -I INPUT 1 -j CLAUDE_INPUT" "iptables -A INPUT -j CLAUDE_INPUT" "Jump to CLAUDE_INPUT" -try_cmd "iptables -I OUTPUT 1 -j CLAUDE_OUTPUT" "iptables -A OUTPUT -j CLAUDE_OUTPUT" "Jump to CLAUDE_OUTPUT" -try_cmd "iptables -I FORWARD 1 -j CLAUDE_FORWARD" "iptables -A FORWARD -j CLAUDE_FORWARD" "Jump to CLAUDE_FORWARD" +# Always insert rules at the beginning of chains - fail if this doesn't work +execute_cmd "iptables -I INPUT 1 -j CLAUDE_INPUT" "Jump to CLAUDE_INPUT" +execute_cmd "iptables -I OUTPUT 1 -j CLAUDE_OUTPUT" "Jump to CLAUDE_OUTPUT" +execute_cmd "iptables -I FORWARD 1 -j CLAUDE_FORWARD" "Jump to CLAUDE_FORWARD" # Create ipset if [ "$IPSET_AVAILABLE" = true ]; then @@ -297,49 +320,47 @@ fi # Basic connectivity rules log "Setting up basic connectivity..." -try_cmd "iptables -A CLAUDE_INPUT -i lo -j ACCEPT" "iptables -I INPUT 1 -i lo -j ACCEPT" "Localhost input" -try_cmd "iptables -A CLAUDE_OUTPUT -o lo -j ACCEPT" "iptables -I OUTPUT 1 -o lo -j ACCEPT" "Localhost output" -try_cmd "iptables -A CLAUDE_OUTPUT -p udp --dport 53 -j ACCEPT" "" "DNS out UDP" -try_cmd "iptables -A CLAUDE_OUTPUT -p tcp --dport 53 -j ACCEPT" "" "DNS out TCP" -try_cmd "iptables -A CLAUDE_INPUT -p udp --sport 53 -j ACCEPT" "" "DNS in UDP" -try_cmd "iptables -A CLAUDE_INPUT -p tcp --sport 53 -j ACCEPT" "" "DNS in TCP" -try_cmd "iptables -A CLAUDE_OUTPUT -p tcp --dport 22 -j ACCEPT" "" "SSH out" -try_cmd "iptables -A CLAUDE_INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT" \ - "iptables -A CLAUDE_INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" "ESTABLISHED in" -try_cmd "iptables -A CLAUDE_OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT" \ - "iptables -A CLAUDE_OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" "ESTABLISHED out" +execute_cmd "iptables -A CLAUDE_INPUT -i lo -j ACCEPT" "Localhost input" +execute_cmd "iptables -A CLAUDE_OUTPUT -o lo -j ACCEPT" "Localhost output" +execute_cmd "iptables -A CLAUDE_OUTPUT -p udp --dport 53 -j ACCEPT" "DNS out UDP" +execute_cmd "iptables -A CLAUDE_OUTPUT -p tcp --dport 53 -j ACCEPT" "DNS out TCP" +execute_cmd "iptables -A CLAUDE_INPUT -p udp --sport 53 -j ACCEPT" "DNS in UDP" +execute_cmd "iptables -A CLAUDE_INPUT -p tcp --sport 53 -j ACCEPT" "DNS in TCP" +execute_cmd "iptables -A CLAUDE_OUTPUT -p tcp --dport 22 -j ACCEPT" "SSH out" + +# Move these rules just before the final DROP rule +if ! iptables -A CLAUDE_INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null; then + execute_cmd "iptables -A CLAUDE_INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" "ESTABLISHED in" +fi + +if ! iptables -A CLAUDE_OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null; then + execute_cmd "iptables -A CLAUDE_OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" "ESTABLISHED out" +fi # Initialize IPv6 chains if enabled if [ "$IPV6_ENABLED" = true ]; then create_ipv6_chains fi -# Add GitHub IPs -log "Adding GitHub IPs..." -gh_ranges=$(curl -s --connect-timeout 5 https://api.github.com/meta) -if [ -n "$gh_ranges" ] && echo "$gh_ranges" | grep -q "api"; then +# Add GitHub IPs that we fetched earlier +log "Adding GitHub IPs to allowed list..." +if [ -n "$gh_ranges" ]; then while read -r cidr; do [[ -n "$cidr" ]] && add_ip "$cidr" done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' 2>/dev/null || echo "") -else - # Fallback GitHub IPs - for ip in "140.82.112.0/20" "192.30.252.0/22" "185.199.108.0/22" "143.55.64.0/20"; do - add_ip "$ip" - done fi # Add important domains log "Adding important domains..." for domain in "registry.npmjs.org" "api.anthropic.com" "sentry.io" "statsig.anthropic.com" \ "cursor.blob.core.windows.net" "statsig.com" "marketplace.visualstudio.com" \ - "vscode.blob.core.windows.net" "vsmarketplacebadge.apphb.com" "example.com"; do + "vscode.blob.core.windows.net" "api.github.com"; do add_domain "$domain" done -# Azure IP Ranges -log "Adding Azure IPs..." -azure_json=$(curl -sSL --connect-timeout 5 https://download.microsoft.com/download/7/1/d/71d86715-5596-4529-9b13-da13a5de5b63/ServiceTags_Public_20250303.json 2>/dev/null) +# Add Azure IPs if we fetched them successfully if [ -n "$azure_json" ]; then + log "Adding Azure IPs..." # Extract IPv4 addresses only for Azure Front Door AZURE_RANGES=$(echo "$azure_json" | jq -r '.values[] | select(.name=="AzureFrontDoor.Frontend") | .properties.addressPrefixes[] | select(contains(":") | not)' 2>/dev/null) @@ -359,23 +380,6 @@ if [ -n "$azure_json" ]; then fi fi -# Add AWS S3 IPs -log "Adding AWS S3 IPs..." -aws_json=$(curl -s --connect-timeout 5 "https://ip-ranges.amazonaws.com/ip-ranges.json" 2>/dev/null) -if [ -n "$aws_json" ]; then - while read -r ip; do - [[ -n "$ip" ]] && add_ip "$ip" - done < <(echo "$aws_json" | jq -r '.prefixes[] | select(.service=="S3") | .ip_prefix' 2>/dev/null || echo "") - - if [ "$IPV6_ENABLED" = true ]; then - while read -r ip; do - if [[ -n "$ip" ]] && [ ${#ip} -le 39 ]; then - add_ipv6 "$ip" - fi - done < <(echo "$aws_json" | jq -r '.ipv6_prefixes[] | select(.service=="S3") | .ipv6_prefix' 2>/dev/null || echo "") - fi -fi - # Host network configuration log "Configuring host network..." HOST_IP=$(ip route | grep default | awk '{print $3}' || hostname -I | awk '{print $1}') @@ -384,14 +388,14 @@ if [ -n "$HOST_IP" ]; then IFS='.' read -r a b c d <<<"$HOST_IP" HOST_NETWORK="${a}.${b}.${c}.0/24" - try_cmd "iptables -A CLAUDE_INPUT -s $HOST_NETWORK -j ACCEPT" "" "Host network IN" - try_cmd "iptables -A CLAUDE_OUTPUT -d $HOST_NETWORK -j ACCEPT" "" "Host network OUT" + execute_cmd "iptables -A CLAUDE_INPUT -s $HOST_NETWORK -j ACCEPT" "Host network IN" + execute_cmd "iptables -A CLAUDE_OUTPUT -d $HOST_NETWORK -j ACCEPT" "Host network OUT" # Default gateway DEFAULT_GATEWAY=$(ip route | grep default | awk '{print $3}') if [ -n "$DEFAULT_GATEWAY" ]; then - try_cmd "iptables -A CLAUDE_INPUT -s $DEFAULT_GATEWAY -j ACCEPT" "" "Gateway IN" - try_cmd "iptables -A CLAUDE_OUTPUT -d $DEFAULT_GATEWAY -j ACCEPT" "" "Gateway OUT" + execute_cmd "iptables -A CLAUDE_INPUT -s $DEFAULT_GATEWAY -j ACCEPT" "Gateway IN" + execute_cmd "iptables -A CLAUDE_OUTPUT -d $DEFAULT_GATEWAY -j ACCEPT" "Gateway OUT" fi fi @@ -401,30 +405,30 @@ for iface in $(ip -o link show | grep -v lo | awk -F': ' '{print $2}'); do add_interface "$iface" done -# HTTPS traffic -log "Adding HTTPS rules..." -try_cmd "iptables -A CLAUDE_INPUT -p tcp --sport 443 -j ACCEPT" "" "HTTPS in" -try_cmd "iptables -A CLAUDE_OUTPUT -p tcp --dport 443 -j ACCEPT" "" "HTTPS out" - -# HTTP traffic - carefully controlled -log "Configuring HTTP rules..." -# We DO NOT add a blanket rule allowing port 80 outbound -# Instead, rely on the explicitly allowed domains via ipset or individual rules - # ipset rule (if available) if [ "$IPSET_AVAILABLE" = true ]; then - try_cmd "iptables -A CLAUDE_OUTPUT -m set --match-set claude-allowed-domains dst -j ACCEPT" "" "ipset rule" + iptables -A CLAUDE_OUTPUT -m set --match-set claude-allowed-domains dst -j ACCEPT 2>/dev/null || warning "Failed to add ipset rule" fi # Optional logging - place before the DROP rule -try_cmd "iptables -A CLAUDE_OUTPUT -m limit --limit 5/min -j LOG --log-prefix \"CLAUDE_FIREWALL: \" --log-level 4" "" "Logging" +iptables -A CLAUDE_OUTPUT -m limit --limit 5/min -j LOG --log-prefix "CLAUDE_FIREWALL: " --log-level 4 2>/dev/null || warning "Failed to add logging rule" if [ "$IPV6_ENABLED" = true ]; then ip6tables -A CLAUDE_OUTPUT -m limit --limit 5/min -j LOG --log-prefix "CLAUDE_FIREWALL_IPV6: " --log-level 4 2>/dev/null || true fi +# Add explicit DROP rules for non-allowed web traffic +iptables -A CLAUDE_OUTPUT -p tcp --dport 80 -j DROP 2>/dev/null || true +iptables -A CLAUDE_OUTPUT -p tcp --dport 443 -j DROP 2>/dev/null || true + # Final drop rule log "Adding default drop rule..." -try_cmd "iptables -A CLAUDE_OUTPUT -j DROP" "iptables -A CLAUDE_OUTPUT -j REJECT" "Default DROP" +if ! iptables -A CLAUDE_OUTPUT -j DROP 2>/dev/null; then + if ! iptables -A CLAUDE_OUTPUT -j REJECT 2>/dev/null; then + error "Failed to add DROP or REJECT rule" + exit 1 + fi +fi + if [ "$IPV6_ENABLED" = true ]; then ip6tables -A CLAUDE_OUTPUT -j DROP 2>/dev/null || ip6tables -A CLAUDE_OUTPUT -j REJECT 2>/dev/null || true fi @@ -435,14 +439,5 @@ test_conn "api.github.com" true test_conn "marketplace.visualstudio.com" true test_conn "example.com" false -# Test HTTP is properly blocked for non-allowed domains -log "Testing HTTP blocking..." -curl --connect-timeout 5 -s "http://non-allowed-domain.example" >/dev/null 2>&1 -if [ $? -eq 0 ]; then - warning "HTTP blocking failed - port 80 traffic is allowed when it shouldn't be" -else - log "HTTP blocking successful - port 80 is properly restricted" -fi - log "Claude firewall configuration finished" -exit 0 \ No newline at end of file +exit 0 From 4cb64db55ddc8a0eb9623cd6acdd06e11b655356 Mon Sep 17 00:00:00 2001 From: Manuel Alejandro de Brito Fontes Date: Sun, 23 Mar 2025 10:47:34 -0700 Subject: [PATCH 3/7] Document Azure IP ranges --- .devcontainer/init-firewall.sh | 214 ++++++++++++++++++++++----------- 1 file changed, 143 insertions(+), 71 deletions(-) mode change 100644 => 100755 .devcontainer/init-firewall.sh diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh old mode 100644 new mode 100755 index 0a29540..38d13c1 --- a/.devcontainer/init-firewall.sh +++ b/.devcontainer/init-firewall.sh @@ -1,5 +1,6 @@ #!/bin/bash set -uo pipefail + IFS=$'\n\t' # Global variables @@ -14,13 +15,21 @@ error() { log "ERROR: $1"; } warning() { log "WARNING: $1"; } debug_log() { [ "$DEBUG" = true ] && log "DEBUG: $1"; } -# Execute command with no fallback - all failures are fatal +# Execute command with better error handling execute_cmd() { - debug_log "Executing: $1" - if ! eval "$1" &>/dev/null; then - error "Failed: ${2:-Command}" - exit 1 + local cmd="$1" + local description="${2:-Command}" + + debug_log "Executing: $cmd" + + # Capture both stdout and stderr + local output + if ! output=$(eval "$cmd" 2>&1); then + error "Failed: $description" + error "Command output: $output" + return 1 fi + return 0 } @@ -160,64 +169,72 @@ add_interface() { return 0 } -# Clean up rules +# Clean up rules with better error handling cleanup() { log "Cleaning up..." + # Use || true to prevent failures from stopping script execution iptables -D INPUT -j CLAUDE_INPUT 2>/dev/null || true iptables -D OUTPUT -j CLAUDE_OUTPUT 2>/dev/null || true iptables -D FORWARD -j CLAUDE_FORWARD 2>/dev/null || true - iptables -F CLAUDE_INPUT 2>/dev/null || true - iptables -F CLAUDE_OUTPUT 2>/dev/null || true - iptables -F CLAUDE_FORWARD 2>/dev/null || true - iptables -X CLAUDE_INPUT 2>/dev/null || true - iptables -X CLAUDE_OUTPUT 2>/dev/null || true - iptables -X CLAUDE_FORWARD 2>/dev/null || true + + # Check if chains exist before trying to flush or delete them + for chain in CLAUDE_INPUT CLAUDE_OUTPUT CLAUDE_FORWARD; do + if iptables -L $chain -n >/dev/null 2>&1; then + iptables -F $chain 2>/dev/null || true + iptables -X $chain 2>/dev/null || true + fi + done if [ "$IPV6_ENABLED" = true ]; then ip6tables -D INPUT -j CLAUDE_INPUT 2>/dev/null || true ip6tables -D OUTPUT -j CLAUDE_OUTPUT 2>/dev/null || true ip6tables -D FORWARD -j CLAUDE_FORWARD 2>/dev/null || true - ip6tables -F CLAUDE_INPUT 2>/dev/null || true - ip6tables -F CLAUDE_OUTPUT 2>/dev/null || true - ip6tables -F CLAUDE_FORWARD 2>/dev/null || true - ip6tables -X CLAUDE_INPUT 2>/dev/null || true - ip6tables -X CLAUDE_OUTPUT 2>/dev/null || true - ip6tables -X CLAUDE_FORWARD 2>/dev/null || true + + for chain in CLAUDE_INPUT CLAUDE_OUTPUT CLAUDE_FORWARD; do + if ip6tables -L $chain -n >/dev/null 2>&1; then + ip6tables -F $chain 2>/dev/null || true + ip6tables -X $chain 2>/dev/null || true + fi + done fi - ipset destroy claude-allowed-domains 2>/dev/null || true - rm -f "$ADDED_IPS_FILE" + if ipset list claude-allowed-domains >/dev/null 2>&1; then + ipset destroy claude-allowed-domains 2>/dev/null || true + fi + + [ -f "$ADDED_IPS_FILE" ] && rm -f "$ADDED_IPS_FILE" log "Cleanup complete" } -# Test connectivity +# Test connectivity with better timeout management test_conn() { local domain="$1" local allowed="$2" + local timeout=5 log "Testing connectivity to $domain (should be ${allowed})" if [ "$allowed" = true ]; then # Force a new connection using --no-keepalive - curl --connect-timeout 5 --no-keepalive -s "https://$domain" >/dev/null 2>&1 + curl --connect-timeout $timeout --no-keepalive -s "https://$domain" >/dev/null 2>&1 local status=$? if [ $status -ne 0 ]; then warning "Expected curl to succeed for https://$domain, but got $status" return 1 fi log "Connection to https://$domain successful as expected" - curl --connect-timeout 5 --no-keepalive -s "http://$domain" >/dev/null 2>&1 + curl --connect-timeout $timeout --no-keepalive -s "http://$domain" >/dev/null 2>&1 local http_status=$? log "HTTP connection to $domain returned status $http_status (should work for allowed domains)" else # For disallowed domains, force a fresh connection attempt - curl --connect-timeout 5 --no-keepalive -s "https://$domain" >/dev/null 2>&1 + curl --connect-timeout $timeout --no-keepalive -s "https://$domain" >/dev/null 2>&1 local status=$? if [ $status -eq 0 ]; then warning "Expected curl to fail for https://$domain, but got $status" return 1 fi log "Connection to https://$domain failed as expected" - curl --connect-timeout 5 --no-keepalive -s "http://$domain" >/dev/null 2>&1 + curl --connect-timeout $timeout --no-keepalive -s "http://$domain" >/dev/null 2>&1 local http_status=$? if [ $http_status -eq 0 ]; then warning "Expected curl to fail for http://$domain, but got success" @@ -231,14 +248,20 @@ test_conn() { # Set up trap for cleanup only on errors or interruptions, not normal exit trap 'cleanup; exit 1' INT TERM HUP -# Check for command availability +# Check for command availability with improved error handling +missing_commands="" for cmd in iptables curl dig; do - command -v "$cmd" &>/dev/null || { - error "Required command '$cmd' not found" - exit 1 - } + if ! command -v "$cmd" &>/dev/null; then + missing_commands+="$cmd " + fi done +if [ -n "$missing_commands" ]; then + error "Required commands not found: $missing_commands" + log "Please install the missing packages and try again" + exit 1 +fi + for cmd in ipset ip6tables; do if ! command -v "$cmd" &>/dev/null; then warning "Optional command '$cmd' not found, limited functionality" @@ -261,39 +284,106 @@ fi # Start configuration log "Starting Claude firewall configuration..." +# Run cleanup first to ensure we start with a clean slate cleanup # Temporary allow ALL outbound HTTPS during initial setup log "Temporarily allowing all outbound HTTPS traffic for initial setup..." -iptables -I OUTPUT -p tcp --dport 443 -j ACCEPT +iptables -I OUTPUT -p tcp --dport 443 -j ACCEPT || { + error "Failed to set temporary HTTPS allow rule. Check iptables permissions." + exit 1 +} + +# Function to handle API requests with retry logic +fetch_with_retry() { + local url="$1" + local max_retries=3 + local retry=0 + local output="" + + while [ $retry -lt $max_retries ]; do + output=$(curl -sSL --connect-timeout 10 "$url" 2>&1) + if [ $? -eq 0 ] && [ -n "$output" ]; then + echo "$output" + return 0 + fi + + retry=$((retry + 1)) + log "Retry $retry/$max_retries for $url" + sleep 2 + done -# Download required files + log "Failed to fetch from $url after $max_retries attempts" + return 1 +} + +# Download required files with improved error handling log "Fetching GitHub IPs..." -gh_ranges=$(curl -sSL --connect-timeout 5 https://api.github.com/meta) +gh_ranges=$(fetch_with_retry "https://api.github.com/meta") if [ -n "$gh_ranges" ] && echo "$gh_ranges" | jq -e . >/dev/null 2>&1; then log "Successfully fetched GitHub IP ranges" else error "Failed to fetch GitHub IP ranges - cannot continue without current GitHub IPs" + cleanup exit 1 fi -# Fetch Azure IP ranges -log "Fetching Azure IPs..." -azure_json=$(curl -sSL --connect-timeout 5 https://download.microsoft.com/download/7/1/d/71d86715-5596-4529-9b13-da13a5de5b63/ServiceTags_Public_20250303.json 2>/dev/null) -if [ -z "$azure_json" ]; then - warning "Failed to fetch Azure IP ranges - will continue without them" +# Download Azure IP ranges with retry logic +log "Fetching Azure CDN IPs..." +azure_ranges=$(fetch_with_retry "https://www.microsoft.com/en-us/download/confirmation.aspx?id=56519") +if [ -n "$azure_ranges" ]; then + # Extract the download URL from the response - fix to handle only one URL + download_url=$(echo "$azure_ranges" | grep -o 'https://download.microsoft.com/download/[^"]*ServiceTags_Public[^"]*\.json' | head -1) + + if [ -n "$download_url" ]; then + log "Found Azure IP ranges download URL: $download_url" + azure_ip_json=$(fetch_with_retry "$download_url") + + if [ -n "$azure_ip_json" ] && echo "$azure_ip_json" | jq -e . >/dev/null 2>&1; then + log "Successfully fetched Azure IP ranges" + + # Extract Azure CDN IP ranges + log "Adding Azure CDN IPs to allowed list..." + while read -r cidr; do + [[ -n "$cidr" ]] && add_ip "$cidr" + done < <(echo "$azure_ip_json" | jq -r '.values[] | select(.name=="AzureFrontDoor.Frontend").properties.addressPrefixes[]' 2>/dev/null || echo "") + + # Also add Azure CDN Standard from Microsoft IP ranges + while read -r cidr; do + [[ -n "$cidr" ]] && add_ip "$cidr" + done < <(echo "$azure_ip_json" | jq -r '.values[] | select(.name=="AzureCDN").properties.addressPrefixes[]' 2>/dev/null || echo "") + else + warning "Failed to parse Azure IP ranges JSON" + fi + else + warning "Failed to extract Azure IP ranges download URL" + fi +else + warning "Failed to fetch Azure IP ranges - Azure CDN traffic may be blocked" fi # Remove temporary all-allow rule log "Removing temporary HTTPS allow rule..." -iptables -D OUTPUT -p tcp --dport 443 -j ACCEPT +iptables -D OUTPUT -p tcp --dport 443 -j ACCEPT || warning "Failed to remove temporary HTTPS rule" -# Create custom chains +# Create custom chains with better error handling log "Creating custom chains..." for chain in CLAUDE_INPUT CLAUDE_OUTPUT CLAUDE_FORWARD; do - # Try to create the chain, if it exists already then flush it - if ! iptables -N $chain 2>/dev/null; then - execute_cmd "iptables -F $chain" "Flushing existing chain $chain" + # Check if chain exists + if iptables -L $chain -n >/dev/null 2>&1; then + log "Chain $chain exists, flushing..." + iptables -F $chain || { + error "Failed to flush chain $chain" + cleanup + exit 1 + } + else + log "Creating chain $chain..." + iptables -N $chain || { + error "Failed to create chain $chain" + cleanup + exit 1 + } fi done @@ -330,11 +420,15 @@ execute_cmd "iptables -A CLAUDE_OUTPUT -p tcp --dport 22 -j ACCEPT" "SSH out" # Move these rules just before the final DROP rule if ! iptables -A CLAUDE_INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null; then - execute_cmd "iptables -A CLAUDE_INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" "ESTABLISHED in" + if ! iptables -A CLAUDE_INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null; then + warning "Failed to add ESTABLISHED,RELATED rule for input" + fi fi if ! iptables -A CLAUDE_OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null; then - execute_cmd "iptables -A CLAUDE_OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" "ESTABLISHED out" + if ! iptables -A CLAUDE_OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null; then + warning "Failed to add ESTABLISHED,RELATED rule for output" + fi fi # Initialize IPv6 chains if enabled @@ -353,33 +447,11 @@ fi # Add important domains log "Adding important domains..." for domain in "registry.npmjs.org" "api.anthropic.com" "sentry.io" "statsig.anthropic.com" \ - "cursor.blob.core.windows.net" "statsig.com" "marketplace.visualstudio.com" \ + "cursor.blob.core.windows.net" "statsig.com" "marketplace.visualstudio.com" "update.code.visualstudio.com" \ "vscode.blob.core.windows.net" "api.github.com"; do add_domain "$domain" done -# Add Azure IPs if we fetched them successfully -if [ -n "$azure_json" ]; then - log "Adding Azure IPs..." - # Extract IPv4 addresses only for Azure Front Door - AZURE_RANGES=$(echo "$azure_json" | jq -r '.values[] | select(.name=="AzureFrontDoor.Frontend") | .properties.addressPrefixes[] | select(contains(":") | not)' 2>/dev/null) - - for azure_ip in $AZURE_RANGES; do - add_ip "$azure_ip" - done - - # Handle IPv6 addresses separately if enabled - if [ "$IPV6_ENABLED" = true ]; then - AZURE_IPV6_RANGES=$(echo "$azure_json" | jq -r '.values[] | select(.name=="AzureFrontDoor.Frontend") | .properties.addressPrefixes[] | select(contains(":"))' 2>/dev/null) - for azure_ipv6 in $AZURE_IPV6_RANGES; do - # Only process IPv6 addresses that aren't too long - if [ ${#azure_ipv6} -le 39 ]; then - add_ipv6 "$azure_ipv6" - fi - done - fi -fi - # Host network configuration log "Configuring host network..." HOST_IP=$(ip route | grep default | awk '{print $3}' || hostname -I | awk '{print $1}') @@ -435,9 +507,9 @@ fi # Verification log "Verifying configuration..." -test_conn "api.github.com" true -test_conn "marketplace.visualstudio.com" true -test_conn "example.com" false +test_conn "api.github.com" true || warning "Verification failed for api.github.com" +test_conn "marketplace.visualstudio.com" true || warning "Verification failed for marketplace.visualstudio.com" +test_conn "example.com" false || warning "Verification failed for example.com" log "Claude firewall configuration finished" exit 0 From 59b431799aa0575641d73f577859b3757804870c Mon Sep 17 00:00:00 2001 From: Manuel Alejandro de Brito Fontes Date: Sun, 23 Mar 2025 18:33:44 +0000 Subject: [PATCH 4/7] Restore exit behavior --- .devcontainer/init-firewall.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh index 38d13c1..bd2bcf0 100755 --- a/.devcontainer/init-firewall.sh +++ b/.devcontainer/init-firewall.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -uo pipefail +set -euo pipefail IFS=$'\n\t' From c472dd7efb322cea1d8488854b0a0a81c0f01854 Mon Sep 17 00:00:00 2001 From: Manuel Alejandro de Brito Fontes Date: Sun, 23 Mar 2025 18:52:32 +0000 Subject: [PATCH 5/7] Lint script --- .devcontainer/init-firewall.sh | 54 +++++++++++++++++----------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh index bd2bcf0..b603a39 100755 --- a/.devcontainer/init-firewall.sh +++ b/.devcontainer/init-firewall.sh @@ -332,34 +332,34 @@ fi log "Fetching Azure CDN IPs..." azure_ranges=$(fetch_with_retry "https://www.microsoft.com/en-us/download/confirmation.aspx?id=56519") if [ -n "$azure_ranges" ]; then - # Extract the download URL from the response - fix to handle only one URL - download_url=$(echo "$azure_ranges" | grep -o 'https://download.microsoft.com/download/[^"]*ServiceTags_Public[^"]*\.json' | head -1) - - if [ -n "$download_url" ]; then - log "Found Azure IP ranges download URL: $download_url" - azure_ip_json=$(fetch_with_retry "$download_url") - - if [ -n "$azure_ip_json" ] && echo "$azure_ip_json" | jq -e . >/dev/null 2>&1; then - log "Successfully fetched Azure IP ranges" - - # Extract Azure CDN IP ranges - log "Adding Azure CDN IPs to allowed list..." - while read -r cidr; do - [[ -n "$cidr" ]] && add_ip "$cidr" - done < <(echo "$azure_ip_json" | jq -r '.values[] | select(.name=="AzureFrontDoor.Frontend").properties.addressPrefixes[]' 2>/dev/null || echo "") - - # Also add Azure CDN Standard from Microsoft IP ranges - while read -r cidr; do - [[ -n "$cidr" ]] && add_ip "$cidr" - done < <(echo "$azure_ip_json" | jq -r '.values[] | select(.name=="AzureCDN").properties.addressPrefixes[]' 2>/dev/null || echo "") - else - warning "Failed to parse Azure IP ranges JSON" - fi - else - warning "Failed to extract Azure IP ranges download URL" - fi + # Extract the download URL from the response - fix to handle only one URL + download_url=$(echo "$azure_ranges" | grep -o 'https://download.microsoft.com/download/[^"]*ServiceTags_Public[^"]*\.json' | head -1) + + if [ -n "$download_url" ]; then + log "Found Azure IP ranges download URL: $download_url" + azure_ip_json=$(fetch_with_retry "$download_url") + + if [ -n "$azure_ip_json" ] && echo "$azure_ip_json" | jq -e . >/dev/null 2>&1; then + log "Successfully fetched Azure IP ranges" + + # Extract Azure CDN IP ranges + log "Adding Azure CDN IPs to allowed list..." + while read -r cidr; do + [[ -n "$cidr" ]] && add_ip "$cidr" + done < <(echo "$azure_ip_json" | jq -r '.values[] | select(.name=="AzureFrontDoor.Frontend").properties.addressPrefixes[]' 2>/dev/null || echo "") + + # Also add Azure CDN Standard from Microsoft IP ranges + while read -r cidr; do + [[ -n "$cidr" ]] && add_ip "$cidr" + done < <(echo "$azure_ip_json" | jq -r '.values[] | select(.name=="AzureCDN").properties.addressPrefixes[]' 2>/dev/null || echo "") + else + warning "Failed to parse Azure IP ranges JSON" + fi + else + warning "Failed to extract Azure IP ranges download URL" + fi else - warning "Failed to fetch Azure IP ranges - Azure CDN traffic may be blocked" + warning "Failed to fetch Azure IP ranges - Azure CDN traffic may be blocked" fi # Remove temporary all-allow rule From fda76349411374c716dbe2f52c8ad165c50c4643 Mon Sep 17 00:00:00 2001 From: Manuel Alejandro de Brito Fontes Date: Sun, 23 Mar 2025 13:42:21 -0700 Subject: [PATCH 6/7] Improve resilience --- .devcontainer/init-firewall.sh | 78 +++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh index b603a39..62687e6 100755 --- a/.devcontainer/init-firewall.sh +++ b/.devcontainer/init-firewall.sh @@ -33,20 +33,27 @@ execute_cmd() { return 0 } +# Add IP to allowed list with deduplication # Add IP to allowed list with deduplication add_ip() { local ip="$1" [ -f "$ADDED_IPS_FILE" ] && grep -q "^$ip$" "$ADDED_IPS_FILE" && return 0 - if [ "$IPSET_AVAILABLE" = true ] && ipset add claude-allowed-domains "$ip" 2>/dev/null; then - echo "$ip" >>"$ADDED_IPS_FILE" - return 0 - elif iptables -A CLAUDE_OUTPUT -d "$ip" -j ACCEPT 2>/dev/null; then + if [ "$IPSET_AVAILABLE" = true ]; then + if ipset add claude-allowed-domains "$ip" 2>/dev/null; then + echo "$ip" >>"$ADDED_IPS_FILE" + return 0 + fi + fi + + # Try with iptables but don't fail the script if it doesn't work + if iptables -A CLAUDE_OUTPUT -d "$ip" -j ACCEPT 2>/dev/null; then echo "$ip" >>"$ADDED_IPS_FILE" return 0 else debug_log "Failed to add IP: $ip" - return 1 + # Return success even if we failed to add the IP to avoid script termination + return 0 fi } @@ -77,30 +84,33 @@ add_ipv6() { fi } +# Create IPv6 chains # Create IPv6 chains create_ipv6_chains() { log "Creating IPv6 chains..." for chain in CLAUDE_INPUT CLAUDE_OUTPUT CLAUDE_FORWARD; do - ip6tables -N $chain 2>/dev/null || ip6tables -F $chain 2>/dev/null + ip6tables -N $chain 2>/dev/null || ip6tables -F $chain 2>/dev/null || true done - ip6tables -D INPUT -j CLAUDE_INPUT 2>/dev/null - ip6tables -D OUTPUT -j CLAUDE_OUTPUT 2>/dev/null - ip6tables -D FORWARD -j CLAUDE_FORWARD 2>/dev/null - - ip6tables -I INPUT 1 -j CLAUDE_INPUT 2>/dev/null || ip6tables -A INPUT -j CLAUDE_INPUT 2>/dev/null - ip6tables -I OUTPUT 1 -j CLAUDE_OUTPUT 2>/dev/null || ip6tables -A OUTPUT -j CLAUDE_OUTPUT 2>/dev/null - ip6tables -I FORWARD 1 -j CLAUDE_FORWARD 2>/dev/null || ip6tables -A FORWARD -j CLAUDE_FORWARD 2>/dev/null - - # IPv6 basic rules - ip6tables -A CLAUDE_INPUT -i lo -j ACCEPT 2>/dev/null - ip6tables -A CLAUDE_OUTPUT -o lo -j ACCEPT 2>/dev/null - ip6tables -A CLAUDE_INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null - ip6tables -A CLAUDE_OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null - ip6tables -A CLAUDE_OUTPUT -p udp --dport 53 -j ACCEPT 2>/dev/null - ip6tables -A CLAUDE_OUTPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null - ip6tables -A CLAUDE_INPUT -p udp --sport 53 -j ACCEPT 2>/dev/null - ip6tables -A CLAUDE_INPUT -p tcp --sport 53 -j ACCEPT 2>/dev/null + # Delete chains with error handling + ip6tables -D INPUT -j CLAUDE_INPUT 2>/dev/null || true + ip6tables -D OUTPUT -j CLAUDE_OUTPUT 2>/dev/null || true + ip6tables -D FORWARD -j CLAUDE_FORWARD 2>/dev/null || true + + # Add chains with error handling + ip6tables -I INPUT 1 -j CLAUDE_INPUT 2>/dev/null || ip6tables -A INPUT -j CLAUDE_INPUT 2>/dev/null || true + ip6tables -I OUTPUT 1 -j CLAUDE_OUTPUT 2>/dev/null || ip6tables -A OUTPUT -j CLAUDE_OUTPUT 2>/dev/null || true + ip6tables -I FORWARD 1 -j CLAUDE_FORWARD 2>/dev/null || ip6tables -A FORWARD -j CLAUDE_FORWARD 2>/dev/null || true + + # IPv6 basic rules - all with error handling + ip6tables -A CLAUDE_INPUT -i lo -j ACCEPT 2>/dev/null || true + ip6tables -A CLAUDE_OUTPUT -o lo -j ACCEPT 2>/dev/null || true + ip6tables -A CLAUDE_INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || true + ip6tables -A CLAUDE_OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || true + ip6tables -A CLAUDE_OUTPUT -p udp --dport 53 -j ACCEPT 2>/dev/null || true + ip6tables -A CLAUDE_OUTPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null || true + ip6tables -A CLAUDE_INPUT -p udp --sport 53 -j ACCEPT 2>/dev/null || true + ip6tables -A CLAUDE_INPUT -p tcp --sport 53 -j ACCEPT 2>/dev/null || true } # Resolve domain and add IPs @@ -330,28 +340,28 @@ fi # Download Azure IP ranges with retry logic log "Fetching Azure CDN IPs..." -azure_ranges=$(fetch_with_retry "https://www.microsoft.com/en-us/download/confirmation.aspx?id=56519") +azure_ranges=$(fetch_with_retry "https://www.microsoft.com/en-us/download/confirmation.aspx?id=56519") || true if [ -n "$azure_ranges" ]; then - # Extract the download URL from the response - fix to handle only one URL - download_url=$(echo "$azure_ranges" | grep -o 'https://download.microsoft.com/download/[^"]*ServiceTags_Public[^"]*\.json' | head -1) + # Extract the download URL from the response - more robust extraction + download_url=$(echo "$azure_ranges" | grep -o 'https://download.microsoft.com/download/[^"]*ServiceTags_Public[^"]*\.json' | head -1 | tr -d '\n\r') if [ -n "$download_url" ]; then log "Found Azure IP ranges download URL: $download_url" - azure_ip_json=$(fetch_with_retry "$download_url") + azure_ip_json=$(fetch_with_retry "$download_url") || true - if [ -n "$azure_ip_json" ] && echo "$azure_ip_json" | jq -e . >/dev/null 2>&1; then + if [ -n "$azure_ip_json" ] && (echo "$azure_ip_json" | jq -e . >/dev/null 2>&1 || true); then log "Successfully fetched Azure IP ranges" # Extract Azure CDN IP ranges log "Adding Azure CDN IPs to allowed list..." - while read -r cidr; do - [[ -n "$cidr" ]] && add_ip "$cidr" - done < <(echo "$azure_ip_json" | jq -r '.values[] | select(.name=="AzureFrontDoor.Frontend").properties.addressPrefixes[]' 2>/dev/null || echo "") + echo "$azure_ip_json" | jq -r '.values[] | select(.name=="AzureFrontDoor.Frontend").properties.addressPrefixes[]' 2>/dev/null | while read -r cidr || [ -n "$cidr" ]; do + [[ -n "$cidr" ]] && add_ip "$cidr" || true + done # Also add Azure CDN Standard from Microsoft IP ranges - while read -r cidr; do - [[ -n "$cidr" ]] && add_ip "$cidr" - done < <(echo "$azure_ip_json" | jq -r '.values[] | select(.name=="AzureCDN").properties.addressPrefixes[]' 2>/dev/null || echo "") + echo "$azure_ip_json" | jq -r '.values[] | select(.name=="AzureCDN").properties.addressPrefixes[]' 2>/dev/null | while read -r cidr || [ -n "$cidr" ]; do + [[ -n "$cidr" ]] && add_ip "$cidr" || true + done else warning "Failed to parse Azure IP ranges JSON" fi From 67cca0e61b286ab988a5ade221f3498e8a2cc25e Mon Sep 17 00:00:00 2001 From: Manuel Alejandro de Brito Fontes Date: Sun, 23 Mar 2025 14:02:22 -0700 Subject: [PATCH 7/7] Fix debug command --- .devcontainer/init-firewall.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh index 62687e6..3b97f35 100755 --- a/.devcontainer/init-firewall.sh +++ b/.devcontainer/init-firewall.sh @@ -13,7 +13,12 @@ IPSET_AVAILABLE=true log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"; } error() { log "ERROR: $1"; } warning() { log "WARNING: $1"; } -debug_log() { [ "$DEBUG" = true ] && log "DEBUG: $1"; } +debug_log() { + if [ "$DEBUG" = true ]; then + log "DEBUG: $1" + fi + return 0 +} # Execute command with better error handling execute_cmd() {