@jgamaraalv/ts-dev-kit 3.1.2 → 3.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.1.3] - 2026-02-27
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- `/yolo` firewall script: revert to strict upstream reference (`set -euo pipefail`, no fallbacks) — remove `exec > >(tee ...)` that caused VS Code to hang with "Unable to resolve resource", remove `|| true` fallbacks that masked failures, remove `| tee` from `postStartCommand`; keep only the `sort -u` dedup fix for duplicate DNS IPs
|
|
13
|
+
- `/yolo` SKILL.md: document critical anti-patterns (process substitution logging, loose error handling, piped postStartCommand)
|
|
14
|
+
|
|
8
15
|
## [3.1.2] - 2026-02-27
|
|
9
16
|
|
|
10
17
|
### Fixed
|
package/package.json
CHANGED
package/skills/yolo/SKILL.md
CHANGED
|
@@ -252,14 +252,23 @@ Write the file using the reference firewall script. See [references/init-firewal
|
|
|
252
252
|
|
|
253
253
|
Key security features:
|
|
254
254
|
|
|
255
|
+
- `set -euo pipefail` — exits immediately on any error (strict mode)
|
|
255
256
|
- Default-deny policy (DROP all INPUT, OUTPUT, FORWARD)
|
|
256
|
-
- Whitelisted outbound only: npm registry, GitHub (dynamic IP fetch), Claude API, Sentry, StatsIG, VS Code marketplace
|
|
257
|
+
- Whitelisted outbound only: npm registry, GitHub (dynamic IP fetch via `api.github.com/meta`), Claude API, Sentry, StatsIG, VS Code marketplace
|
|
257
258
|
- DNS and SSH allowed
|
|
258
259
|
- Localhost and host network allowed
|
|
260
|
+
- Docker DNS rules preserved before flushing
|
|
261
|
+
- Uses `ipset hash:net` for efficient CIDR matching (required — do NOT add fallback logic)
|
|
259
262
|
- Startup verification: confirms `example.com` is blocked and `api.github.com` is reachable
|
|
260
|
-
-
|
|
261
|
-
|
|
262
|
-
-
|
|
263
|
+
- Exits with error if any domain fails to resolve or any IP range is invalid
|
|
264
|
+
|
|
265
|
+
**Critical anti-patterns to avoid in this script:**
|
|
266
|
+
- Do NOT use `exec > >(tee ...)` for logging — process substitutions run via `sudo` never receive EOF, causing the script to hang indefinitely and VS Code to fail with "Unable to resolve resource"
|
|
267
|
+
- Do NOT remove the `-e` flag from `set -euo pipefail` — silent error handling masks failures
|
|
268
|
+
- Do NOT add `|| true` fallbacks for core iptables/ipset commands
|
|
269
|
+
|
|
270
|
+
**Critical anti-pattern to avoid in `devcontainer.json`:**
|
|
271
|
+
- Do NOT pipe `postStartCommand` through `tee` (e.g., `... | tee /tmp/firewall-init.log`) — this hides the script's real exit code and creates redundant I/O. The correct value is simply: `"postStartCommand": "sudo /usr/local/bin/init-firewall.sh"`
|
|
263
272
|
|
|
264
273
|
### Step 5 — Add `.devcontainer` to `.gitignore` (optional)
|
|
265
274
|
|
|
@@ -57,7 +57,7 @@ Write this file to `.devcontainer/devcontainer.json`:
|
|
|
57
57
|
},
|
|
58
58
|
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
|
|
59
59
|
"workspaceFolder": "/workspace",
|
|
60
|
-
"postStartCommand": "sudo /usr/local/bin/init-firewall.sh
|
|
60
|
+
"postStartCommand": "sudo /usr/local/bin/init-firewall.sh",
|
|
61
61
|
"waitFor": "postStartCommand"
|
|
62
62
|
}
|
|
63
63
|
```
|
|
@@ -4,279 +4,162 @@ Write this file to `.devcontainer/init-firewall.sh`:
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
#!/bin/bash
|
|
7
|
-
set -
|
|
8
|
-
IFS=$'\n\t'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
echo "[FAIL] iptables is NOT functional — firewall cannot be configured"
|
|
24
|
-
echo "Hint: ensure --cap-add=NET_ADMIN is set in devcontainer.json runArgs"
|
|
25
|
-
exit 1
|
|
26
|
-
fi
|
|
27
|
-
|
|
28
|
-
if ipset list >/dev/null 2>&1 || ipset create _test hash:net 2>/dev/null; then
|
|
29
|
-
ipset destroy _test 2>/dev/null || true
|
|
30
|
-
HAVE_IPSET=true
|
|
31
|
-
echo "[OK] ipset is functional"
|
|
32
|
-
else
|
|
33
|
-
echo "[WARN] ipset is NOT functional — falling back to iptables-only rules"
|
|
34
|
-
fi
|
|
35
|
-
|
|
36
|
-
if command -v dig >/dev/null 2>&1; then
|
|
37
|
-
echo "[OK] dig is available"
|
|
38
|
-
else
|
|
39
|
-
echo "[WARN] dig not found — DNS resolution will use getent"
|
|
40
|
-
fi
|
|
41
|
-
|
|
42
|
-
if command -v aggregate >/dev/null 2>&1; then
|
|
43
|
-
echo "[OK] aggregate is available"
|
|
44
|
-
else
|
|
45
|
-
echo "[WARN] aggregate not found — GitHub CIDRs will be added individually"
|
|
46
|
-
fi
|
|
47
|
-
|
|
48
|
-
# --- Helper: resolve domain to IPs ----------------------------------------
|
|
49
|
-
resolve_domain() {
|
|
50
|
-
local domain="$1"
|
|
51
|
-
local ips=""
|
|
52
|
-
if command -v dig >/dev/null 2>&1; then
|
|
53
|
-
ips=$(dig +noall +answer +short A "$domain" 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | sort -u)
|
|
54
|
-
fi
|
|
55
|
-
if [ -z "$ips" ] && command -v getent >/dev/null 2>&1; then
|
|
56
|
-
ips=$(getent ahostsv4 "$domain" 2>/dev/null | awk '{print $1}' | sort -u | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')
|
|
57
|
-
fi
|
|
58
|
-
echo "$ips"
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
# --- 1. Preserve Docker DNS -----------------------------------------------
|
|
62
|
-
echo "--- Preserving Docker DNS rules ---"
|
|
63
|
-
DOCKER_DNS_RULES=$(iptables-save -t nat 2>/dev/null | grep "127\.0\.0\.11" || true)
|
|
64
|
-
|
|
65
|
-
# --- 2. Flush existing rules -----------------------------------------------
|
|
66
|
-
echo "--- Flushing existing rules ---"
|
|
67
|
-
iptables -F || true
|
|
68
|
-
iptables -X || true
|
|
69
|
-
iptables -t nat -F || true
|
|
70
|
-
iptables -t nat -X || true
|
|
71
|
-
iptables -t mangle -F || true
|
|
72
|
-
iptables -t mangle -X || true
|
|
73
|
-
if [ "$HAVE_IPSET" = true ]; then
|
|
74
|
-
ipset destroy allowed-domains 2>/dev/null || true
|
|
75
|
-
fi
|
|
76
|
-
|
|
77
|
-
# --- 3. Restore Docker DNS -------------------------------------------------
|
|
7
|
+
set -euo pipefail # Exit on error, undefined vars, and pipeline failures
|
|
8
|
+
IFS=$'\n\t' # Stricter word splitting
|
|
9
|
+
|
|
10
|
+
# 1. Extract Docker DNS info BEFORE any flushing
|
|
11
|
+
DOCKER_DNS_RULES=$(iptables-save -t nat | grep "127\.0\.0\.11" || true)
|
|
12
|
+
|
|
13
|
+
# Flush existing rules and delete existing ipsets
|
|
14
|
+
iptables -F
|
|
15
|
+
iptables -X
|
|
16
|
+
iptables -t nat -F
|
|
17
|
+
iptables -t nat -X
|
|
18
|
+
iptables -t mangle -F
|
|
19
|
+
iptables -t mangle -X
|
|
20
|
+
ipset destroy allowed-domains 2>/dev/null || true
|
|
21
|
+
|
|
22
|
+
# 2. Selectively restore ONLY internal Docker DNS resolution
|
|
78
23
|
if [ -n "$DOCKER_DNS_RULES" ]; then
|
|
79
24
|
echo "Restoring Docker DNS rules..."
|
|
80
25
|
iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true
|
|
81
26
|
iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true
|
|
82
|
-
echo "$DOCKER_DNS_RULES" |
|
|
83
|
-
iptables -t nat $rule 2>/dev/null || true
|
|
84
|
-
done
|
|
27
|
+
echo "$DOCKER_DNS_RULES" | xargs -L 1 iptables -t nat
|
|
85
28
|
else
|
|
86
29
|
echo "No Docker DNS rules to restore"
|
|
87
30
|
fi
|
|
88
31
|
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
# Outbound DNS
|
|
32
|
+
# First allow DNS and localhost before any restrictions
|
|
33
|
+
# Allow outbound DNS
|
|
92
34
|
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
|
|
93
|
-
|
|
94
|
-
# Inbound DNS responses
|
|
35
|
+
# Allow inbound DNS responses
|
|
95
36
|
iptables -A INPUT -p udp --sport 53 -j ACCEPT
|
|
96
|
-
|
|
97
|
-
# Outbound SSH
|
|
37
|
+
# Allow outbound SSH
|
|
98
38
|
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
|
|
39
|
+
# Allow inbound SSH responses
|
|
99
40
|
iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
|
|
100
|
-
#
|
|
41
|
+
# Allow localhost
|
|
101
42
|
iptables -A INPUT -i lo -j ACCEPT
|
|
102
43
|
iptables -A OUTPUT -o lo -j ACCEPT
|
|
103
44
|
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
if [ -n "$HOST_IP" ]; then
|
|
107
|
-
HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/16/")
|
|
108
|
-
echo "Host network: $HOST_NETWORK (via $HOST_IP)"
|
|
109
|
-
iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT
|
|
110
|
-
iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT
|
|
111
|
-
else
|
|
112
|
-
echo "[WARN] Could not detect host IP — allowing RFC1918 ranges for Docker connectivity"
|
|
113
|
-
iptables -A INPUT -s 172.16.0.0/12 -j ACCEPT
|
|
114
|
-
iptables -A OUTPUT -d 172.16.0.0/12 -j ACCEPT
|
|
115
|
-
iptables -A INPUT -s 192.168.0.0/16 -j ACCEPT
|
|
116
|
-
iptables -A OUTPUT -d 192.168.0.0/16 -j ACCEPT
|
|
117
|
-
iptables -A INPUT -s 10.0.0.0/8 -j ACCEPT
|
|
118
|
-
iptables -A OUTPUT -d 10.0.0.0/8 -j ACCEPT
|
|
119
|
-
fi
|
|
45
|
+
# Create ipset with CIDR support
|
|
46
|
+
ipset create allowed-domains hash:net
|
|
120
47
|
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
"
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"marketplace.visualstudio.com"
|
|
129
|
-
"vscode.blob.core.windows.net"
|
|
130
|
-
"update.code.visualstudio.com"
|
|
131
|
-
)
|
|
48
|
+
# Fetch GitHub meta information and aggregate + add their IP ranges
|
|
49
|
+
echo "Fetching GitHub IP ranges..."
|
|
50
|
+
gh_ranges=$(curl -s https://api.github.com/meta)
|
|
51
|
+
if [ -z "$gh_ranges" ]; then
|
|
52
|
+
echo "ERROR: Failed to fetch GitHub IP ranges"
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
132
55
|
|
|
133
|
-
if
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
56
|
+
if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then
|
|
57
|
+
echo "ERROR: GitHub API response missing required fields"
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
137
60
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
while read -r cidr; do
|
|
144
|
-
[[ "$cidr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]] && ipset add allowed-domains "$cidr" 2>/dev/null || true
|
|
145
|
-
done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q 2>/dev/null)
|
|
146
|
-
else
|
|
147
|
-
while read -r cidr; do
|
|
148
|
-
[[ "$cidr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]] && ipset add allowed-domains "$cidr" 2>/dev/null || true
|
|
149
|
-
done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]')
|
|
150
|
-
fi
|
|
151
|
-
echo "[OK] GitHub IP ranges added"
|
|
152
|
-
else
|
|
153
|
-
echo "[WARN] Could not fetch GitHub IPs — resolving github.com directly"
|
|
154
|
-
for gh_domain in "github.com" "api.github.com" "raw.githubusercontent.com" "objects.githubusercontent.com"; do
|
|
155
|
-
ips=$(resolve_domain "$gh_domain")
|
|
156
|
-
while read -r ip; do
|
|
157
|
-
[ -n "$ip" ] && ipset add allowed-domains "$ip" 2>/dev/null || true
|
|
158
|
-
done <<< "$ips"
|
|
159
|
-
done
|
|
61
|
+
echo "Processing GitHub IPs..."
|
|
62
|
+
while read -r cidr; do
|
|
63
|
+
if [[ ! "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
|
|
64
|
+
echo "ERROR: Invalid CIDR range from GitHub meta: $cidr"
|
|
65
|
+
exit 1
|
|
160
66
|
fi
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
echo "--- Building iptables allowlist (no ipset) ---"
|
|
181
|
-
|
|
182
|
-
# GitHub IPs
|
|
183
|
-
echo "Fetching GitHub IP ranges..."
|
|
184
|
-
gh_ranges=$(curl -sf --connect-timeout 10 https://api.github.com/meta 2>/dev/null || true)
|
|
185
|
-
if [ -n "$gh_ranges" ] && echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null 2>&1; then
|
|
186
|
-
while read -r cidr; do
|
|
187
|
-
[[ "$cidr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]] && \
|
|
188
|
-
iptables -A OUTPUT -d "$cidr" -j ACCEPT 2>/dev/null || true
|
|
189
|
-
done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]')
|
|
190
|
-
echo "[OK] GitHub IP ranges added via iptables"
|
|
191
|
-
else
|
|
192
|
-
echo "[WARN] Could not fetch GitHub IPs — resolving github.com directly"
|
|
193
|
-
for gh_domain in "github.com" "api.github.com" "raw.githubusercontent.com" "objects.githubusercontent.com"; do
|
|
194
|
-
ips=$(resolve_domain "$gh_domain")
|
|
195
|
-
while read -r ip; do
|
|
196
|
-
[ -n "$ip" ] && iptables -A OUTPUT -d "$ip" -j ACCEPT 2>/dev/null || true
|
|
197
|
-
done <<< "$ips"
|
|
198
|
-
done
|
|
67
|
+
echo "Adding GitHub range $cidr"
|
|
68
|
+
ipset add allowed-domains "$cidr"
|
|
69
|
+
done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | sort -u | aggregate -q)
|
|
70
|
+
|
|
71
|
+
# Resolve and add other allowed domains
|
|
72
|
+
for domain in \
|
|
73
|
+
"registry.npmjs.org" \
|
|
74
|
+
"api.anthropic.com" \
|
|
75
|
+
"sentry.io" \
|
|
76
|
+
"statsig.anthropic.com" \
|
|
77
|
+
"statsig.com" \
|
|
78
|
+
"marketplace.visualstudio.com" \
|
|
79
|
+
"vscode.blob.core.windows.net" \
|
|
80
|
+
"update.code.visualstudio.com"; do
|
|
81
|
+
echo "Resolving $domain..."
|
|
82
|
+
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}' | sort -u)
|
|
83
|
+
if [ -z "$ips" ]; then
|
|
84
|
+
echo "ERROR: Failed to resolve $domain"
|
|
85
|
+
exit 1
|
|
199
86
|
fi
|
|
200
87
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if [ -z "$ips" ]; then
|
|
206
|
-
echo "[WARN] Failed to resolve $domain — skipping"
|
|
207
|
-
continue
|
|
88
|
+
while read -r ip; do
|
|
89
|
+
if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
|
90
|
+
echo "ERROR: Invalid IP from DNS for $domain: $ip"
|
|
91
|
+
exit 1
|
|
208
92
|
fi
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
93
|
+
echo "Adding $ip for $domain"
|
|
94
|
+
ipset add allowed-domains "$ip"
|
|
95
|
+
done < <(echo "$ips")
|
|
96
|
+
done
|
|
97
|
+
|
|
98
|
+
# Get host IP from default route
|
|
99
|
+
HOST_IP=$(ip route | grep default | cut -d" " -f3)
|
|
100
|
+
if [ -z "$HOST_IP" ]; then
|
|
101
|
+
echo "ERROR: Failed to detect host IP"
|
|
102
|
+
exit 1
|
|
213
103
|
fi
|
|
214
104
|
|
|
215
|
-
|
|
216
|
-
echo "
|
|
105
|
+
HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/")
|
|
106
|
+
echo "Host network detected as: $HOST_NETWORK"
|
|
107
|
+
|
|
108
|
+
# Set up remaining iptables rules
|
|
109
|
+
iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT
|
|
110
|
+
iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT
|
|
111
|
+
|
|
112
|
+
# Set default policies to DROP first
|
|
217
113
|
iptables -P INPUT DROP
|
|
218
114
|
iptables -P FORWARD DROP
|
|
219
115
|
iptables -P OUTPUT DROP
|
|
220
116
|
|
|
221
|
-
#
|
|
117
|
+
# First allow established connections for already approved traffic
|
|
222
118
|
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
|
223
119
|
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
|
224
120
|
|
|
225
|
-
#
|
|
226
|
-
iptables -A OUTPUT -
|
|
121
|
+
# Then allow only specific outbound traffic to allowed domains
|
|
122
|
+
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
|
|
227
123
|
|
|
228
|
-
#
|
|
229
|
-
|
|
230
|
-
VERIFIED=true
|
|
124
|
+
# Explicitly REJECT all other outbound traffic for immediate feedback
|
|
125
|
+
iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited
|
|
231
126
|
|
|
127
|
+
echo "Firewall configuration complete"
|
|
128
|
+
echo "Verifying firewall rules..."
|
|
232
129
|
if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then
|
|
233
|
-
echo "
|
|
234
|
-
|
|
130
|
+
echo "ERROR: Firewall verification failed - was able to reach https://example.com"
|
|
131
|
+
exit 1
|
|
235
132
|
else
|
|
236
|
-
echo "
|
|
133
|
+
echo "Firewall verification passed - unable to reach https://example.com as expected"
|
|
237
134
|
fi
|
|
238
135
|
|
|
239
|
-
|
|
240
|
-
|
|
136
|
+
# Verify GitHub API access
|
|
137
|
+
if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then
|
|
138
|
+
echo "ERROR: Firewall verification failed - unable to reach https://api.github.com"
|
|
139
|
+
exit 1
|
|
241
140
|
else
|
|
242
|
-
echo "
|
|
243
|
-
echo " Claude Code will still work, but git operations may fail"
|
|
141
|
+
echo "Firewall verification passed - able to reach https://api.github.com as expected"
|
|
244
142
|
fi
|
|
143
|
+
```
|
|
245
144
|
|
|
246
|
-
|
|
247
|
-
echo "=== FIREWALL VERIFICATION FAILED ==="
|
|
248
|
-
exit 1
|
|
249
|
-
fi
|
|
145
|
+
## Differences from the upstream Claude Code reference
|
|
250
146
|
|
|
251
|
-
|
|
252
|
-
echo "Mode: $([ "$HAVE_IPSET" = true ] && echo 'ipset' || echo 'iptables-only')"
|
|
253
|
-
echo "Log: $LOG"
|
|
254
|
-
```
|
|
147
|
+
The only change from the [upstream reference](https://github.com/anthropics/claude-code/blob/main/.devcontainer/init-firewall.sh) is `| sort -u` added to two pipelines to deduplicate IPs before `ipset add`:
|
|
255
148
|
|
|
256
|
-
|
|
149
|
+
1. **GitHub CIDRs** (line with `aggregate -q`): `jq -r '...'[]' | sort -u | aggregate -q` — deduplicates before aggregation
|
|
150
|
+
2. **Domain resolution** (line with `dig`): `awk ... | sort -u` — deduplicates IPs from DNS (e.g., `marketplace.visualstudio.com` returns the same IP twice)
|
|
257
151
|
|
|
258
|
-
|
|
259
|
-
|-------|-------------------|-------------------|
|
|
260
|
-
| Duplicate IPs from DNS | `ipset add` fatal error (exit 1) | Deduplicates via `sort -u` + `ipset add ... \|\| true` |
|
|
261
|
-
| `ipset` not available | Fatal crash | Falls back to iptables-only rules |
|
|
262
|
-
| `aggregate` not available | Fatal crash | Adds CIDRs individually without aggregation |
|
|
263
|
-
| `dig` not available | Fatal crash | Falls back to `getent ahostsv4` |
|
|
264
|
-
| GitHub API unreachable | Fatal crash | Resolves github.com/api.github.com directly |
|
|
265
|
-
| Domain resolution fails | Fatal crash | Skips domain with warning, continues |
|
|
266
|
-
| Host IP detection fails | Fatal crash | Allows all RFC1918 ranges as fallback |
|
|
267
|
-
| Docker DNS restore fails | Silent + potential crash | Handles per-rule with `|| true` |
|
|
268
|
-
| No diagnostic output | Blind exit code 1 | Full log at `/tmp/firewall-init.log` |
|
|
269
|
-
| GitHub unreachable after setup | Fatal crash | Warning only (Claude API is the critical path) |
|
|
152
|
+
Without deduplication, `ipset add` fails with `"Element cannot be added to the set: it's already added"` and `set -euo pipefail` terminates the script.
|
|
270
153
|
|
|
271
154
|
## Firewall rules summary
|
|
272
155
|
|
|
273
156
|
| Rule | Direction | Purpose |
|
|
274
157
|
|------|-----------|---------|
|
|
275
|
-
| DNS (UDP
|
|
158
|
+
| DNS (UDP 53) | Outbound | Domain name resolution |
|
|
276
159
|
| SSH (TCP 22) | Outbound | Git over SSH |
|
|
277
160
|
| Localhost | Both | Container-internal communication |
|
|
278
161
|
| Host network | Both | Docker host ↔ container communication |
|
|
279
|
-
| GitHub IPs | Outbound | Git operations, GitHub API (dynamically fetched
|
|
162
|
+
| GitHub IPs | Outbound | Git operations, GitHub API (dynamically fetched from api.github.com/meta) |
|
|
280
163
|
| npm registry | Outbound | Package installation |
|
|
281
164
|
| api.anthropic.com | Outbound | Claude API calls |
|
|
282
165
|
| sentry.io | Outbound | Error reporting |
|
|
@@ -287,7 +170,7 @@ echo "Log: $LOG"
|
|
|
287
170
|
## Verification on startup
|
|
288
171
|
|
|
289
172
|
The script verifies the firewall by:
|
|
290
|
-
1. Confirming `https://example.com` is **blocked** (
|
|
291
|
-
2. Confirming `https://api.github.com` is **reachable** (
|
|
173
|
+
1. Confirming `https://example.com` is **blocked** (should fail)
|
|
174
|
+
2. Confirming `https://api.github.com` is **reachable** (should succeed)
|
|
292
175
|
|
|
293
|
-
|
|
176
|
+
If either check fails, the script exits with an error and the container will not start.
|