Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"containerEnv": {
"NODE_OPTIONS": "--max-old-space-size=4096",
"CLAUDE_CONFIG_DIR": "/home/node/.claude",
"POWERLEVEL9K_DISABLE_GITSTATUS": "true"
"POWERLEVEL9K_DISABLE_GITSTATUS": "true",
"WHITELIST_DOMAINS": "${localEnv:WHITELIST_DOMAINS:}"
},
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
"workspaceFolder": "/workspace",
Expand Down
83 changes: 64 additions & 19 deletions .devcontainer/init-firewall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@
set -euo pipefail # Exit on error, undefined vars, and pipeline failures
IFS=$'\n\t' # Stricter word splitting

# Configuration - Domain list as shell array for easy maintenance
declare -a DYNAMIC_DOMAINS=(
Copy link
Copy Markdown

@MarkS-AL MarkS-AL Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow additional domains, eg "pypi.org" to be passed in externally, eg via WHITELIST_DOMAINS env var?
Concat the user whitelist with this list of domain names?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late reply — it's been a while since this was raised!

Great suggestion. I've now added support for a WHITELIST_DOMAINS environment variable. Users can pass additional domains (space-separated) which get merged with the default DYNAMIC_DOMAINS list at container startup.

Usage example:

export WHITELIST_DOMAINS="pypi.org files.pythonhosted.org"

The env var is passed into the container via devcontainer.json using ${localEnv:WHITELIST_DOMAINS:}, so it's fully optional and backward compatible — if unset or empty, behavior is unchanged.

"registry.npmjs.org"
"api.anthropic.com"
"sentry.io"
"statsig.anthropic.com"
"statsig.com"
"marketplace.visualstudio.com"
"vscode.blob.core.windows.net"
"update.code.visualstudio.com"
)

# Merge user-specified domains from WHITELIST_DOMAINS env var (space-separated)
if [ -n "${WHITELIST_DOMAINS:-}" ]; then
IFS=' ' read -ra USER_DOMAINS <<< "$WHITELIST_DOMAINS"
DYNAMIC_DOMAINS+=("${USER_DOMAINS[@]}")
fi

# IPSet configuration
IPSET_STATIC="allowed-static" # For GitHub and other static IPs
IPSET_DYNAMIC="allowed-dynamic" # For dynamically resolved domains
DNS_TTL=600 # DNS cache timeout in seconds

# 1. Extract Docker DNS info BEFORE any flushing
DOCKER_DNS_RULES=$(iptables-save -t nat | grep "127\.0\.0\.11" || true)

Expand All @@ -12,6 +35,8 @@ iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X
ipset destroy "$IPSET_STATIC" 2>/dev/null || true
ipset destroy "$IPSET_DYNAMIC" 2>/dev/null || true
ipset destroy allowed-domains 2>/dev/null || true

# 2. Selectively restore ONLY internal Docker DNS resolution
Expand All @@ -37,8 +62,9 @@ iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
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
# Create ipsets
ipset create "$IPSET_STATIC" hash:net
ipset create "$IPSET_DYNAMIC" hash:ip timeout "$DNS_TTL"

# Fetch GitHub meta information and aggregate + add their IP ranges
echo "Fetching GitHub IP ranges..."
Expand All @@ -59,20 +85,11 @@ while read -r cidr; do
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" \
"marketplace.visualstudio.com" \
"vscode.blob.core.windows.net" \
"update.code.visualstudio.com"; do
ipset add "$IPSET_STATIC" "$cidr"
done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q 2>/dev/null || echo "$gh_ranges" | jq -r '(.web + .api + .git)[]')

# Resolve and add other allowed domains with TTL
for domain in "${DYNAMIC_DOMAINS[@]}"; do
echo "Resolving $domain..."
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
if [ -z "$ips" ]; then
Expand All @@ -85,8 +102,7 @@ for domain in \
echo "ERROR: Invalid IP from DNS for $domain: $ip"
exit 1
fi
echo "Adding $ip for $domain"
ipset add allowed-domains "$ip"
ipset add "$IPSET_DYNAMIC" "$ip" timeout "$DNS_TTL" -exist
done < <(echo "$ips")
done

Expand Down Expand Up @@ -114,7 +130,36 @@ iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Then allow only specific outbound traffic to allowed domains
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
iptables -A OUTPUT -m set --match-set "$IPSET_STATIC" dst -j ACCEPT
iptables -A OUTPUT -m set --match-set "$IPSET_DYNAMIC" dst -j ACCEPT

# Create DNS refresh script
cat > /usr/local/bin/refresh-dynamic-domains.sh << EOF
#!/bin/bash
IPSET_DYNAMIC="$IPSET_DYNAMIC"
DNS_TTL=$DNS_TTL

# Domain list passed as arguments
for domain in $@; do
ips=\$(dig +short A "\$domain" 2>/dev/null | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\$')
if [ -n "\$ips" ]; then
while IFS= read -r ip; do
ipset add "\$IPSET_DYNAMIC" "\$ip" timeout "\$DNS_TTL" -exist 2>/dev/null
done <<< "\$ips"
fi
done
EOF

chmod +x /usr/local/bin/refresh-dynamic-domains.sh 2>/dev/null || true

# Setup cron job for automatic refresh
if command -v crontab &> /dev/null; then
DOMAIN_ARGS="${DYNAMIC_DOMAINS[@]}"
(crontab -l 2>/dev/null || true; echo "*/5 * * * * /usr/local/bin/refresh-dynamic-domains.sh $DOMAIN_ARGS") | \
grep -v refresh-dynamic-domains | \
(cat; echo "*/5 * * * * /usr/local/bin/refresh-dynamic-domains.sh $DOMAIN_ARGS") | \
crontab - 2>/dev/null || true
fi

# Explicitly REJECT all other outbound traffic for immediate feedback
iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited
Expand Down