|
1 | 1 | #!/bin/bash |
2 | 2 | # Docker wrapper that injects --network awf-net and proxy env vars to all docker run commands |
3 | 3 | # This ensures spawned containers are subject to the same firewall rules |
| 4 | +# |
| 5 | +# Security: This wrapper also injects NAT rules into child containers to prevent proxy bypass. |
| 6 | +# Applications that ignore HTTP_PROXY environment variables will still have their traffic |
| 7 | +# redirected to Squid via iptables NAT rules. |
4 | 8 |
|
5 | 9 | NETWORK_NAME="awf-net" |
6 | 10 | SQUID_PROXY="http://172.30.0.10:3128" |
| 11 | +SQUID_IP="172.30.0.10" |
| 12 | +SQUID_PORT="3128" |
7 | 13 | LOG_FILE="/tmp/docker-wrapper.log" |
8 | 14 |
|
| 15 | +# NAT setup script that will be injected into child containers |
| 16 | +# This script redirects HTTP/HTTPS traffic to Squid proxy using iptables |
| 17 | +# It's designed to be minimal and work with busybox/alpine shells |
| 18 | +NAT_SETUP_SCRIPT=' |
| 19 | +if command -v iptables >/dev/null 2>&1; then |
| 20 | + iptables -t nat -F OUTPUT 2>/dev/null || true |
| 21 | + iptables -t nat -A OUTPUT -o lo -j RETURN |
| 22 | + iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN |
| 23 | + iptables -t nat -A OUTPUT -p udp -d 127.0.0.11 --dport 53 -j RETURN |
| 24 | + iptables -t nat -A OUTPUT -p tcp -d 127.0.0.11 --dport 53 -j RETURN |
| 25 | + iptables -t nat -A OUTPUT -p udp -d 8.8.8.8 --dport 53 -j RETURN |
| 26 | + iptables -t nat -A OUTPUT -p tcp -d 8.8.8.8 --dport 53 -j RETURN |
| 27 | + iptables -t nat -A OUTPUT -p udp -d 8.8.4.4 --dport 53 -j RETURN |
| 28 | + iptables -t nat -A OUTPUT -p tcp -d 8.8.4.4 --dport 53 -j RETURN |
| 29 | + iptables -t nat -A OUTPUT -d 8.8.8.8 -j RETURN |
| 30 | + iptables -t nat -A OUTPUT -d 8.8.4.4 -j RETURN |
| 31 | + iptables -t nat -A OUTPUT -d '"$SQUID_IP"' -j RETURN |
| 32 | + iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination '"$SQUID_IP:$SQUID_PORT"' |
| 33 | + iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination '"$SQUID_IP:$SQUID_PORT"' |
| 34 | +fi |
| 35 | +' |
| 36 | + |
9 | 37 | # Log all docker commands |
10 | 38 | echo "[$(date -Iseconds)] WRAPPER CALLED: docker $@" >> "$LOG_FILE" |
11 | 39 |
|
@@ -78,23 +106,250 @@ if [ "$1" = "run" ]; then |
78 | 106 | exit 1 |
79 | 107 | fi |
80 | 108 |
|
81 | | - # If --network not specified, inject it along with proxy environment variables |
| 109 | + # If --network not specified, inject it along with proxy environment variables and NAT rules |
82 | 110 | if [ "$has_network" = false ]; then |
83 | | - # Build new args: docker run --network awf-net -e HTTP_PROXY -e HTTPS_PROXY <rest of args> |
84 | 111 | shift # remove 'run' |
85 | | - echo "[$(date -Iseconds)] INJECTING --network $NETWORK_NAME and proxy env vars" >> "$LOG_FILE" |
| 112 | + echo "[$(date -Iseconds)] INJECTING --network $NETWORK_NAME, proxy env vars, and NAT rules" >> "$LOG_FILE" |
| 113 | + |
| 114 | + # We need to parse the remaining arguments to find where the image and command are |
| 115 | + # Docker run format: docker run [OPTIONS] IMAGE [COMMAND] [ARG...] |
| 116 | + # We'll inject our security flags and then wrap the command with NAT setup |
| 117 | + |
| 118 | + # Parse remaining arguments to extract: |
| 119 | + # 1. Docker options (flags that start with - or their values) |
| 120 | + # 2. Image name |
| 121 | + # 3. Command and its arguments |
| 122 | + declare -a docker_opts=() |
| 123 | + declare -a user_cmd=() |
| 124 | + image_name="" |
| 125 | + parsing_opts=true |
| 126 | + skip_next=false |
| 127 | + has_rm=false |
| 128 | + has_entrypoint=false |
| 129 | + |
| 130 | + for arg in "$@"; do |
| 131 | + if [ "$skip_next" = true ]; then |
| 132 | + docker_opts+=("$arg") |
| 133 | + skip_next=false |
| 134 | + continue |
| 135 | + fi |
| 136 | + |
| 137 | + if [ "$parsing_opts" = true ]; then |
| 138 | + # Check for --rm flag |
| 139 | + if [ "$arg" = "--rm" ]; then |
| 140 | + has_rm=true |
| 141 | + docker_opts+=("$arg") |
| 142 | + continue |
| 143 | + fi |
| 144 | + |
| 145 | + # Check for --entrypoint flag (we need to handle this specially) |
| 146 | + if [ "$arg" = "--entrypoint" ]; then |
| 147 | + has_entrypoint=true |
| 148 | + docker_opts+=("$arg") |
| 149 | + skip_next=true |
| 150 | + continue |
| 151 | + fi |
| 152 | + if [[ "$arg" == "--entrypoint="* ]]; then |
| 153 | + has_entrypoint=true |
| 154 | + docker_opts+=("$arg") |
| 155 | + continue |
| 156 | + fi |
| 157 | + |
| 158 | + # Docker options that take a value on the next argument |
| 159 | + case "$arg" in |
| 160 | + -e|--env|-l|--label|-v|--volume|-p|--publish|--name|--hostname|\ |
| 161 | + --user|-u|--workdir|-w|--mount|--network-alias|--dns|--dns-search|\ |
| 162 | + --dns-option|--cpus|--memory|-m|--memory-swap|--memory-reservation|\ |
| 163 | + --kernel-memory|--cpu-shares|--cpu-period|--cpu-quota|--cpuset-cpus|\ |
| 164 | + --cpuset-mems|--blkio-weight|--device|--device-cgroup-rule|\ |
| 165 | + --device-read-bps|--device-read-iops|--device-write-bps|\ |
| 166 | + --device-write-iops|--cap-add|--cap-drop|--security-opt|--ulimit|\ |
| 167 | + --sysctl|--restart|--stop-signal|--stop-timeout|--health-cmd|\ |
| 168 | + --health-interval|--health-retries|--health-start-period|\ |
| 169 | + --health-timeout|--log-driver|--log-opt|--storage-opt|\ |
| 170 | + --tmpfs|--ipc|--pid|--userns|--uts|--runtime|--isolation|\ |
| 171 | + --platform|--pull|--shm-size|--group-add|--cidfile|--cgroup-parent|\ |
| 172 | + --init|--read-only|--gpus|--link|--link-local-ip|--ip|--ip6|\ |
| 173 | + --mac-address|--expose|--domainname|--add-host|--annotation) |
| 174 | + docker_opts+=("$arg") |
| 175 | + skip_next=true |
| 176 | + continue |
| 177 | + ;; |
| 178 | + esac |
| 179 | + |
| 180 | + # Docker options that are flags (no value) |
| 181 | + if [[ "$arg" == -* ]]; then |
| 182 | + docker_opts+=("$arg") |
| 183 | + continue |
| 184 | + fi |
| 185 | + |
| 186 | + # First non-option argument is the image name |
| 187 | + image_name="$arg" |
| 188 | + parsing_opts=false |
| 189 | + else |
| 190 | + # Everything after the image name is the command |
| 191 | + user_cmd+=("$arg") |
| 192 | + fi |
| 193 | + done |
| 194 | + |
| 195 | + # If no image was found, just pass through |
| 196 | + if [ -z "$image_name" ]; then |
| 197 | + echo "[$(date -Iseconds)] ERROR: Could not find image name, passing through" >> "$LOG_FILE" |
| 198 | + exec /usr/bin/docker-real run "$@" |
| 199 | + fi |
| 200 | + |
| 201 | + echo "[$(date -Iseconds)] Image: $image_name, Command: ${user_cmd[*]:-<default>}, HasEntrypoint: $has_entrypoint" >> "$LOG_FILE" |
| 202 | + |
| 203 | + # Build the wrapped command that sets up NAT rules then runs the user's command |
| 204 | + # If user specified a command, wrap it; otherwise let the image's default run |
| 205 | + if [ ${#user_cmd[@]} -gt 0 ]; then |
| 206 | + # User specified a command - wrap it with NAT setup |
| 207 | + # Escape the user command for embedding in sh -c |
| 208 | + escaped_cmd="" |
| 209 | + for cmd_part in "${user_cmd[@]}"; do |
| 210 | + # Escape single quotes in the command |
| 211 | + escaped_part="${cmd_part//\'/\'\\\'\'}" |
| 212 | + escaped_cmd="$escaped_cmd '$escaped_part'" |
| 213 | + done |
| 214 | + |
| 215 | + wrapped_cmd="$NAT_SETUP_SCRIPT exec $escaped_cmd" |
| 216 | + else |
| 217 | + # No user command - NAT setup only, then exit (image entrypoint will not run with our wrapper) |
| 218 | + # For this case, we can't easily wrap the default entrypoint, so just set up NAT and warn |
| 219 | + echo "[$(date -Iseconds)] WARNING: No command specified, NAT rules may not apply to default entrypoint" >> "$LOG_FILE" |
| 220 | + # Pass through without wrapping - NAT won't apply but proxy env vars will |
| 221 | + exec /usr/bin/docker-real run \ |
| 222 | + --network "$NETWORK_NAME" \ |
| 223 | + --cap-add NET_ADMIN \ |
| 224 | + -e HTTP_PROXY="$SQUID_PROXY" \ |
| 225 | + -e HTTPS_PROXY="$SQUID_PROXY" \ |
| 226 | + -e http_proxy="$SQUID_PROXY" \ |
| 227 | + -e https_proxy="$SQUID_PROXY" \ |
| 228 | + -e SQUID_PROXY_IP="$SQUID_IP" \ |
| 229 | + -e SQUID_PROXY_PORT="$SQUID_PORT" \ |
| 230 | + "${docker_opts[@]}" \ |
| 231 | + "$image_name" |
| 232 | + fi |
| 233 | + |
| 234 | + # Execute with NAT wrapper |
86 | 235 | exec /usr/bin/docker-real run \ |
87 | 236 | --network "$NETWORK_NAME" \ |
| 237 | + --cap-add NET_ADMIN \ |
88 | 238 | -e HTTP_PROXY="$SQUID_PROXY" \ |
89 | 239 | -e HTTPS_PROXY="$SQUID_PROXY" \ |
90 | 240 | -e http_proxy="$SQUID_PROXY" \ |
91 | 241 | -e https_proxy="$SQUID_PROXY" \ |
92 | | - "$@" |
| 242 | + -e SQUID_PROXY_IP="$SQUID_IP" \ |
| 243 | + -e SQUID_PROXY_PORT="$SQUID_PORT" \ |
| 244 | + "${docker_opts[@]}" \ |
| 245 | + --entrypoint sh \ |
| 246 | + "$image_name" \ |
| 247 | + -c "$wrapped_cmd" |
93 | 248 | else |
94 | | - echo "[$(date -Iseconds)] --network $network_value already specified, passing through" >> "$LOG_FILE" |
| 249 | + # Network is already specified (and not host) - still inject NAT rules and proxy env vars |
| 250 | + echo "[$(date -Iseconds)] --network $network_value already specified, injecting NAT rules and proxy env vars" >> "$LOG_FILE" |
| 251 | + |
| 252 | + shift # remove 'run' |
| 253 | + |
| 254 | + # Parse remaining arguments similar to above |
| 255 | + declare -a docker_opts2=() |
| 256 | + declare -a user_cmd2=() |
| 257 | + image_name2="" |
| 258 | + parsing_opts2=true |
| 259 | + skip_next2=false |
| 260 | + |
| 261 | + for arg in "$@"; do |
| 262 | + if [ "$skip_next2" = true ]; then |
| 263 | + docker_opts2+=("$arg") |
| 264 | + skip_next2=false |
| 265 | + continue |
| 266 | + fi |
| 267 | + |
| 268 | + if [ "$parsing_opts2" = true ]; then |
| 269 | + # Docker options that take a value on the next argument |
| 270 | + case "$arg" in |
| 271 | + -e|--env|-l|--label|-v|--volume|-p|--publish|--name|--hostname|\ |
| 272 | + --user|-u|--workdir|-w|--mount|--network-alias|--dns|--dns-search|\ |
| 273 | + --dns-option|--cpus|--memory|-m|--memory-swap|--memory-reservation|\ |
| 274 | + --kernel-memory|--cpu-shares|--cpu-period|--cpu-quota|--cpuset-cpus|\ |
| 275 | + --cpuset-mems|--blkio-weight|--device|--device-cgroup-rule|\ |
| 276 | + --device-read-bps|--device-read-iops|--device-write-bps|\ |
| 277 | + --device-write-iops|--cap-add|--cap-drop|--security-opt|--ulimit|\ |
| 278 | + --sysctl|--restart|--stop-signal|--stop-timeout|--health-cmd|\ |
| 279 | + --health-interval|--health-retries|--health-start-period|\ |
| 280 | + --health-timeout|--log-driver|--log-opt|--storage-opt|\ |
| 281 | + --tmpfs|--ipc|--pid|--userns|--uts|--runtime|--isolation|\ |
| 282 | + --platform|--pull|--shm-size|--group-add|--cidfile|--cgroup-parent|\ |
| 283 | + --init|--read-only|--gpus|--link|--link-local-ip|--ip|--ip6|\ |
| 284 | + --mac-address|--expose|--domainname|--add-host|--annotation|\ |
| 285 | + --network|--net) |
| 286 | + docker_opts2+=("$arg") |
| 287 | + skip_next2=true |
| 288 | + continue |
| 289 | + ;; |
| 290 | + esac |
| 291 | + |
| 292 | + # Docker options that are flags (no value) or have = format |
| 293 | + if [[ "$arg" == -* ]]; then |
| 294 | + docker_opts2+=("$arg") |
| 295 | + continue |
| 296 | + fi |
| 297 | + |
| 298 | + # First non-option argument is the image name |
| 299 | + image_name2="$arg" |
| 300 | + parsing_opts2=false |
| 301 | + else |
| 302 | + # Everything after the image name is the command |
| 303 | + user_cmd2+=("$arg") |
| 304 | + fi |
| 305 | + done |
| 306 | + |
| 307 | + # If no image was found, just pass through |
| 308 | + if [ -z "$image_name2" ]; then |
| 309 | + echo "[$(date -Iseconds)] ERROR: Could not find image name, passing through" >> "$LOG_FILE" |
| 310 | + exec /usr/bin/docker-real run "$@" |
| 311 | + fi |
| 312 | + |
| 313 | + echo "[$(date -Iseconds)] Image: $image_name2, Command: ${user_cmd2[*]:-<default>}" >> "$LOG_FILE" |
| 314 | + |
| 315 | + # Build the wrapped command |
| 316 | + if [ ${#user_cmd2[@]} -gt 0 ]; then |
| 317 | + escaped_cmd2="" |
| 318 | + for cmd_part in "${user_cmd2[@]}"; do |
| 319 | + escaped_part2="${cmd_part//\'/\'\\\'\'}" |
| 320 | + escaped_cmd2="$escaped_cmd2 '$escaped_part2'" |
| 321 | + done |
| 322 | + |
| 323 | + wrapped_cmd2="$NAT_SETUP_SCRIPT exec $escaped_cmd2" |
| 324 | + |
| 325 | + exec /usr/bin/docker-real run \ |
| 326 | + --cap-add NET_ADMIN \ |
| 327 | + -e HTTP_PROXY="$SQUID_PROXY" \ |
| 328 | + -e HTTPS_PROXY="$SQUID_PROXY" \ |
| 329 | + -e http_proxy="$SQUID_PROXY" \ |
| 330 | + -e https_proxy="$SQUID_PROXY" \ |
| 331 | + -e SQUID_PROXY_IP="$SQUID_IP" \ |
| 332 | + -e SQUID_PROXY_PORT="$SQUID_PORT" \ |
| 333 | + "${docker_opts2[@]}" \ |
| 334 | + --entrypoint sh \ |
| 335 | + "$image_name2" \ |
| 336 | + -c "$wrapped_cmd2" |
| 337 | + else |
| 338 | + # No command specified - pass through with proxy env vars |
| 339 | + exec /usr/bin/docker-real run \ |
| 340 | + --cap-add NET_ADMIN \ |
| 341 | + -e HTTP_PROXY="$SQUID_PROXY" \ |
| 342 | + -e HTTPS_PROXY="$SQUID_PROXY" \ |
| 343 | + -e http_proxy="$SQUID_PROXY" \ |
| 344 | + -e https_proxy="$SQUID_PROXY" \ |
| 345 | + -e SQUID_PROXY_IP="$SQUID_IP" \ |
| 346 | + -e SQUID_PROXY_PORT="$SQUID_PORT" \ |
| 347 | + "${docker_opts2[@]}" \ |
| 348 | + "$image_name2" |
| 349 | + fi |
95 | 350 | fi |
96 | 351 | fi |
97 | 352 |
|
98 | | -# For all other commands or if --network already specified, pass through |
| 353 | +# For all other commands (not docker run), pass through |
99 | 354 | echo "[$(date -Iseconds)] PASSING THROUGH: /usr/bin/docker-real $@" >> "$LOG_FILE" |
100 | 355 | exec /usr/bin/docker-real "$@" |
0 commit comments