|
#!/run/current-system/sw/bin/bash |
|
# PoC: DNS-based domain blocking in Cloud Foundry via BOSH DNS handlers |
|
# |
|
# Demonstrates blocking a domain by injecting a handlers.json into the |
|
# BOSH DNS handlers_files_glob path on a Diego cell, then proving it |
|
# via cf ssh from an app container. |
|
# |
|
# Prerequisites: |
|
# - cf CLI logged in and targeting an org/space |
|
# - An app already pushed (or this script will push one) |
|
# |
|
# Usage: |
|
# ./poc-dns-block.sh [domain-to-block] |
|
# ./poc-dns-block.sh neverssl.com |
|
|
|
set -euo pipefail |
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
|
source "$SCRIPT_DIR/bosh.env" |
|
|
|
DOMAIN_TO_BLOCK="${1:-neverssl.com}" |
|
APP_NAME="${2:-dns-block-test}" |
|
DEPLOYMENT="${BOSH_DEPLOYMENT:-cf}" |
|
|
|
echo "=== CF DNS Blocking PoC ===" |
|
echo "Domain to block: $DOMAIN_TO_BLOCK" |
|
echo "App name: $APP_NAME" |
|
echo "" |
|
|
|
# --- Step 1: Ensure we have a test app --- |
|
echo "[1/6] Ensuring test app '$APP_NAME' exists..." |
|
|
|
if ! cf app "$APP_NAME" &>/dev/null; then |
|
echo " Pushing a simple test app..." |
|
APP_DIR=$(mktemp -d) |
|
cat > "$APP_DIR/main.go" <<'EOF' |
|
package main |
|
|
|
import ( |
|
"fmt" |
|
"net/http" |
|
"os" |
|
) |
|
|
|
func main() { |
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
|
fmt.Fprintln(w, "dns-block-test running") |
|
}) |
|
port := os.Getenv("PORT") |
|
if port == "" { port = "8080" } |
|
http.ListenAndServe(":"+port, nil) |
|
} |
|
EOF |
|
cat > "$APP_DIR/go.mod" <<'EOF' |
|
module dns-block-test |
|
go 1.21 |
|
EOF |
|
cf push "$APP_NAME" -p "$APP_DIR" -b go_buildpack -m 64M -k 256M |
|
rm -rf "$APP_DIR" |
|
else |
|
echo " App already exists." |
|
fi |
|
|
|
echo "" |
|
|
|
# --- Step 2: Baseline — prove the domain resolves BEFORE blocking --- |
|
echo "[2/6] Baseline test: resolving '$DOMAIN_TO_BLOCK' from app container..." |
|
echo " Running: dig $DOMAIN_TO_BLOCK +short" |
|
|
|
BEFORE=$(cf ssh "$APP_NAME" -c "dig $DOMAIN_TO_BLOCK +short 2>/dev/null || nslookup $DOMAIN_TO_BLOCK 2>&1 | grep -A2 'Name:'" 2>&1 | grep -v "Stats server") || true |
|
echo " BEFORE blocking:" |
|
echo " $BEFORE" |
|
|
|
if [ -z "$BEFORE" ]; then |
|
echo " NOTE: dig returned empty, trying curl..." |
|
BEFORE=$(cf ssh "$APP_NAME" -c "curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 http://$DOMAIN_TO_BLOCK/" 2>&1 | grep -v "Stats server") || true |
|
echo " curl http_code: $BEFORE" |
|
fi |
|
echo "" |
|
|
|
# --- Step 3: Find the Diego cell hosting our app --- |
|
echo "[3/6] Finding Diego cell for app..." |
|
APP_GUID=$(cf app "$APP_NAME" --guid) |
|
|
|
# Try v3 stats endpoint |
|
CELL_IP=$(cf curl "/v3/apps/$APP_GUID/processes/web/stats" 2>/dev/null | \ |
|
jq -r '(.resources // [])[0].host // empty' 2>/dev/null) || true |
|
|
|
if [ -z "$CELL_IP" ]; then |
|
# v3 didn't work, try v2 |
|
CELL_IP=$(cf curl "/v2/apps/$APP_GUID/stats" 2>/dev/null | \ |
|
jq -r '.["0"].stats.host // empty' 2>/dev/null) || true |
|
fi |
|
|
|
if [ -z "$CELL_IP" ]; then |
|
echo " WARNING: Could not determine cell IP from app stats. Using first compute/diego cell..." |
|
DIEGO_CELL=$(bosh -d "$DEPLOYMENT" vms --json 2>/dev/null | \ |
|
sed -n '/^{/,$p' | \ |
|
jq -r '.Tables[0].Rows[] | select(.instance | test("compute|diego.cell|diego-cell")) | .instance' 2>/dev/null | head -1) || true |
|
else |
|
echo " App is on cell with IP: $CELL_IP" |
|
DIEGO_CELL=$(bosh -d "$DEPLOYMENT" vms --json 2>/dev/null | \ |
|
sed -n '/^{/,$p' | \ |
|
jq -r --arg ip "$CELL_IP" '.Tables[0].Rows[] | select(.ips | contains($ip)) | .instance' 2>/dev/null | head -1) || true |
|
fi |
|
|
|
if [ -z "$DIEGO_CELL" ]; then |
|
echo " ERROR: Could not find Diego cell. Aborting." |
|
exit 1 |
|
fi |
|
|
|
|
|
|
|
echo " BOSH instance: $DIEGO_CELL" |
|
echo "" |
|
|
|
# --- Step 4: Inject the blocking handler on the Diego cell --- |
|
echo "[4/6] Injecting DNS block handler on Diego cell via bosh ssh..." |
|
echo " Creating /var/vcap/jobs/dns-blocklist/dns/handlers.json" |
|
echo " with domain: $DOMAIN_TO_BLOCK" |
|
|
|
# The handlers_files_glob is /var/vcap/jobs/*/dns/handlers.json |
|
# We create a fake job directory that matches this glob. |
|
# The handler uses type "dns" pointing to a dead recursor (port 1 on loopback). |
|
# This causes BOSH DNS to return NXDOMAIN after failing to reach the recursor. |
|
BLOCK_SCRIPT=$(cat <<SCRIPT |
|
#!/bin/bash |
|
set -e |
|
|
|
# Create fake job dir matching handlers_files_glob |
|
sudo mkdir -p /var/vcap/jobs/dns-blocklist/dns |
|
|
|
# Write the blocking handler config |
|
sudo tee /var/vcap/jobs/dns-blocklist/dns/handlers.json > /dev/null <<'HANDLER' |
|
[ |
|
{ |
|
"domain": "${DOMAIN_TO_BLOCK}.", |
|
"source": { |
|
"type": "dns", |
|
"recursors": ["127.0.0.1:1"] |
|
} |
|
} |
|
] |
|
HANDLER |
|
|
|
echo "Handler file written:" |
|
cat /var/vcap/jobs/dns-blocklist/dns/handlers.json |
|
|
|
# Check current bosh-dns config to see if disable_recursors is set |
|
echo "" |
|
echo "Current bosh-dns config (relevant fields):" |
|
sudo grep -E '"(disable_recursors|configure_systemd_resolved|handlers_files_glob)"' /var/vcap/jobs/bosh-dns/config/config.json || true |
|
|
|
# Restart bosh-dns to pick up the new handler |
|
echo "" |
|
echo "Restarting bosh-dns..." |
|
sudo /var/vcap/bosh/bin/monit restart bosh-dns |
|
|
|
# Wait for bosh-dns to come back |
|
echo "Waiting for bosh-dns to restart..." |
|
sleep 3 |
|
for i in {1..10}; do |
|
if dig @169.254.0.2 +short +time=1 health.bosh-dns. | grep -q "127.0.0.1"; then |
|
echo " bosh-dns is back up (attempt \$i)" |
|
break |
|
fi |
|
sleep 1 |
|
done |
|
SCRIPT |
|
) |
|
|
|
bosh -d "$DEPLOYMENT" ssh "$DIEGO_CELL" -c "$BLOCK_SCRIPT" |
|
echo "" |
|
|
|
# --- Step 5: Test — prove the domain is now BLOCKED --- |
|
echo "[5/6] Testing: resolving '$DOMAIN_TO_BLOCK' from app container AFTER blocking..." |
|
|
|
sleep 2 |
|
|
|
AFTER=$(cf ssh "$APP_NAME" -c "dig $DOMAIN_TO_BLOCK +short +time=3 2>/dev/null; echo EXIT_CODE=\$?" 2>&1 | grep -v "Stats server") || true |
|
echo " AFTER blocking (dig):" |
|
echo " $AFTER" |
|
|
|
# Also test with curl to show the practical effect |
|
echo "" |
|
echo " Testing curl to http://$DOMAIN_TO_BLOCK/ ..." |
|
CURL_AFTER=$(cf ssh "$APP_NAME" -c "curl -s -o /dev/null -w 'http_code=%{http_code} time=%{time_total}s' --connect-timeout 5 http://$DOMAIN_TO_BLOCK/ 2>&1; echo ' exit='\$?" 2>&1 | grep -v "Stats server") || true |
|
echo " $CURL_AFTER" |
|
echo "" |
|
|
|
# --- Step 6: Summary --- |
|
echo "[6/6] Results summary" |
|
echo "============================================" |
|
echo "BEFORE blocking:" |
|
echo " $BEFORE" |
|
echo "" |
|
echo "AFTER blocking:" |
|
echo " dig: $AFTER" |
|
echo " curl: $CURL_AFTER" |
|
echo "============================================" |
|
echo "" |
|
echo "=== Cleanup ===" |
|
echo "To remove the block:" |
|
echo " bosh -d $DEPLOYMENT ssh $DIEGO_CELL -c 'sudo rm -rf /var/vcap/jobs/dns-blocklist && sudo /var/vcap/bosh/bin/monit restart bosh-dns'" |
|
echo "" |
|
echo "To delete the test app:" |
|
echo " cf delete $APP_NAME -f -r" |