mirror of
https://github.com/Monadical-SAS/network-filter.git
synced 2025-12-20 04:09:06 +00:00
feat: use dnsmasq ipset and remove all code about refreshing
This commit is contained in:
@@ -6,7 +6,8 @@ RUN apk add --no-cache \
|
||||
bind-tools \
|
||||
curl \
|
||||
bash \
|
||||
iproute2
|
||||
iproute2 \
|
||||
ipset
|
||||
|
||||
COPY network-filter.sh /network-filter.sh
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ A lightweight Docker container that provides network filtering capabilities usin
|
||||
|
||||
The network-filter uses a combination of:
|
||||
- **iptables**: Drops all outbound traffic by default, then allows only specific IP addresses resolved from allowed domains
|
||||
- **dnsmasq**: Acts as a local DNS server that only resolves allowed domains
|
||||
- **Dynamic IP resolution**: Periodically refreshes IP addresses for allowed domains to handle DNS changes
|
||||
- **dnsmasq**: Acts as a local DNS server that only resolves allowed domains, using ipset groups
|
||||
|
||||
The filter operates at the network level, meaning any container that shares its network namespace will inherit these restrictions.
|
||||
|
||||
@@ -47,7 +46,6 @@ services:
|
||||
|---------------------|-------------|---------|
|
||||
| `ALLOWED_DOMAINS` | Comma-separated list of allowed domains with optional port specifications | (none - required) |
|
||||
| `DNS_SERVERS` | Comma-separated list of upstream DNS servers | `8.8.8.8,8.8.4.4` |
|
||||
| `REFRESH_INTERVAL` | How often to refresh domain IP addresses (seconds) | `300` |
|
||||
| `RUN_SELFTEST` | Run connectivity tests on startup | `false` |
|
||||
|
||||
### Domain and port specification
|
||||
@@ -115,7 +113,6 @@ docker run --rm --network "container:net-filter" alpine ping -c 3 google.com
|
||||
## Limitations
|
||||
|
||||
- **IPv4 only**: Currently only supports IPv4 addresses. IPv6 traffic is blocked and AAAA DNS records are filtered out
|
||||
- Requires periodic refresh to handle DNS changes
|
||||
- All containers sharing the network namespace share the same restrictions
|
||||
|
||||
## Q&A
|
||||
|
||||
@@ -1,85 +1,124 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
# Global arrays for domain/port mapping
|
||||
declare -A DOMAIN_PORTSET # domain -> port set name mapping
|
||||
declare -A PORTSET_PORTS # port set name -> actual ports mapping
|
||||
declare -A PORTSET_DOMAINS # port set name -> domains mapping
|
||||
declare -a ALL_PORTSETS # unique port set names
|
||||
|
||||
# --- Configuration ---
|
||||
setup_env() {
|
||||
DNS_SERVERS="${DNS_SERVERS:-8.8.8.8,8.8.4.4}"
|
||||
REFRESH_INTERVAL="${REFRESH_INTERVAL:-300}"
|
||||
RUN_SELFTEST="${RUN_SELFTEST:-false}"
|
||||
IPSET_TIMEOUT="${IPSET_TIMEOUT:-600}" # 10 minutes default
|
||||
|
||||
echo "--- Configuration ---"
|
||||
echo "DNS Servers: $DNS_SERVERS"
|
||||
echo "Allowed Domains: $ALLOWED_DOMAINS"
|
||||
echo "Refresh Interval: $REFRESH_INTERVAL seconds"
|
||||
echo "IPSet Timeout: $IPSET_TIMEOUT seconds"
|
||||
echo "Run Selftest on start: $RUN_SELFTEST"
|
||||
}
|
||||
|
||||
# --- Domain Parsing ---
|
||||
parse_domains() {
|
||||
if [[ -z "$ALLOWED_DOMAINS" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
IFS=',' read -ra DOMAINS <<< "$ALLOWED_DOMAINS"
|
||||
for domain_spec in "${DOMAINS[@]}"; do
|
||||
domain_spec=$(echo "$domain_spec" | xargs)
|
||||
local domain=$(echo "$domain_spec" | cut -d':' -f1)
|
||||
local ports_part=$(echo "$domain_spec" | cut -d':' -f2-)
|
||||
|
||||
local ports=()
|
||||
if [[ "$domain_spec" == *":"* ]]; then
|
||||
IFS=':' read -ra PORT_LIST <<< "$ports_part"
|
||||
for port in "${PORT_LIST[@]}"; do
|
||||
if [[ "$port" =~ ^[0-9]+$ ]] && [[ "$port" -ge 1 ]] && [[ "$port" -le 65535 ]]; then
|
||||
ports+=("$port")
|
||||
fi
|
||||
done
|
||||
else
|
||||
ports=(80 443) # Default ports
|
||||
fi
|
||||
|
||||
# Sort ports for consistent naming
|
||||
IFS=$'\n' sorted_ports=($(sort -n <<<"${ports[*]}")); unset IFS
|
||||
|
||||
# Create port set name (e.g., "80_443" or "22_80")
|
||||
local portset_name=$(IFS='_'; echo "${sorted_ports[*]}")
|
||||
|
||||
DOMAIN_PORTSET[$domain]="$portset_name"
|
||||
PORTSET_PORTS[$portset_name]="${sorted_ports[*]}"
|
||||
|
||||
# Track domains using this port set
|
||||
if [[ -z "${PORTSET_DOMAINS[$portset_name]}" ]]; then
|
||||
PORTSET_DOMAINS[$portset_name]="$domain"
|
||||
ALL_PORTSETS+=("$portset_name")
|
||||
else
|
||||
PORTSET_DOMAINS[$portset_name]="${PORTSET_DOMAINS[$portset_name]} $domain"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# --- ipset Management ---
|
||||
setup_ipsets() {
|
||||
# Clean up any existing ipsets
|
||||
for portset in "${ALL_PORTSETS[@]}"; do
|
||||
ipset destroy "allowed_${portset}" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Create ipset for each port set
|
||||
for portset in "${ALL_PORTSETS[@]}"; do
|
||||
ipset create "allowed_${portset}" hash:ip timeout "${IPSET_TIMEOUT}"
|
||||
echo "Created ipset: allowed_${portset} for ports ${PORTSET_PORTS[$portset]}"
|
||||
done
|
||||
}
|
||||
|
||||
# --- iptables ---
|
||||
setup_iptables() {
|
||||
# Clear existing rules
|
||||
iptables -t filter -F OUTPUT
|
||||
iptables -t nat -F
|
||||
iptables -P OUTPUT DROP
|
||||
|
||||
# Allow local traffic
|
||||
iptables -A OUTPUT -o lo -j ACCEPT
|
||||
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
iptables -A OUTPUT -d 127.0.0.0/8 -j ACCEPT
|
||||
iptables -A OUTPUT -d 10.0.0.0/8 -j ACCEPT
|
||||
iptables -A OUTPUT -d 172.16.0.0/12 -j ACCEPT
|
||||
iptables -A OUTPUT -d 192.168.0.0/16 -j ACCEPT
|
||||
|
||||
# Allow DNS
|
||||
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
|
||||
iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
|
||||
|
||||
# Allow configured DNS servers
|
||||
IFS=',' read -ra DNS_LIST <<< "$DNS_SERVERS"
|
||||
for dns_server in "${DNS_LIST[@]}"; do
|
||||
dns_server=$(echo "$dns_server" | xargs)
|
||||
iptables -A OUTPUT -d "$dns_server" -p udp --dport 53 -j ACCEPT
|
||||
iptables -A OUTPUT -d "$dns_server" -p tcp --dport 53 -j ACCEPT
|
||||
done
|
||||
}
|
||||
|
||||
add_domain_rule() {
|
||||
local domain_spec=$1
|
||||
local domain=$(echo "$domain_spec" | cut -d':' -f1)
|
||||
local ports_part=$(echo "$domain_spec" | cut -d':' -f2-)
|
||||
|
||||
local ports_to_allow=()
|
||||
if [[ "$domain_spec" == *":"* ]]; then
|
||||
IFS=':' read -ra PORT_LIST <<< "$ports_part"
|
||||
for port in "${PORT_LIST[@]}"; do
|
||||
if [[ "$port" =~ ^[0-9]+$ ]] && [[ "$port" -ge 1 ]] && [[ "$port" -le 65535 ]]; then
|
||||
ports_to_allow+=("$port")
|
||||
fi
|
||||
# Add ipset-based rules for each port set
|
||||
for portset in "${ALL_PORTSETS[@]}"; do
|
||||
IFS=' ' read -ra ports <<< "${PORTSET_PORTS[$portset]}"
|
||||
for port in "${ports[@]}"; do
|
||||
iptables -A OUTPUT -p tcp --dport "$port" -m set --match-set "allowed_${portset}" dst -j ACCEPT
|
||||
echo "Added iptables rule for port $port using ipset allowed_${portset}"
|
||||
done
|
||||
else
|
||||
ports_to_allow=(80 443)
|
||||
fi
|
||||
|
||||
local ipv4_addresses=$(nslookup "$domain" 127.0.0.1 2>/dev/null | awk '/^Address:/ && !/127.0.0.1/ { print $2 }' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')
|
||||
|
||||
for ip in $ipv4_addresses; do
|
||||
if [[ -n "$ip" ]]; then
|
||||
for port in "${ports_to_allow[@]}"; do
|
||||
iptables -A OUTPUT -d "$ip" -p tcp --dport "$port" -j ACCEPT
|
||||
done
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
apply_domain_rules() {
|
||||
if [[ -n "$ALLOWED_DOMAINS" ]]; then
|
||||
IFS=',' read -ra DOMAINS <<< "$ALLOWED_DOMAINS"
|
||||
for domain in "${DOMAINS[@]}"; do
|
||||
domain=$(echo "$domain" | xargs)
|
||||
add_domain_rule "$domain"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# --- DNS ---
|
||||
setup_dnsmasq() {
|
||||
PRIMARY_DNS=$(echo "$DNS_SERVERS" | cut -d',' -f1 | xargs)
|
||||
|
||||
# Start building dnsmasq config
|
||||
cat > /etc/dnsmasq.conf << EOF
|
||||
listen-address=0.0.0.0
|
||||
port=53
|
||||
@@ -89,16 +128,16 @@ no-resolv
|
||||
no-poll
|
||||
log-queries
|
||||
filter-AAAA
|
||||
min-cache-ttl=$((REFRESH_INTERVAL * 2))
|
||||
max-cache-ttl=$((REFRESH_INTERVAL * 2))
|
||||
$(if [[ -n "$ALLOWED_DOMAINS" ]]; then
|
||||
IFS=',' read -ra DOMAINS <<< "$ALLOWED_DOMAINS"
|
||||
for domain in "${DOMAINS[@]}"; do
|
||||
domain_name=$(echo "$domain" | cut -d':' -f1 | xargs)
|
||||
echo "server=/$domain_name/$PRIMARY_DNS"
|
||||
done
|
||||
fi)
|
||||
EOF
|
||||
|
||||
# Add server and ipset entries for all domains
|
||||
for domain in "${!DOMAIN_PORTSET[@]}"; do
|
||||
echo "server=/${domain}/${PRIMARY_DNS}" >> /etc/dnsmasq.conf
|
||||
echo "ipset=/${domain}/allowed_${DOMAIN_PORTSET[$domain]}" >> /etc/dnsmasq.conf
|
||||
done
|
||||
|
||||
echo "--- Generated dnsmasq configuration ---"
|
||||
cat /etc/dnsmasq.conf
|
||||
}
|
||||
|
||||
override_dns() {
|
||||
@@ -110,78 +149,125 @@ override_dns() {
|
||||
# --- Self Test ---
|
||||
run_tests() {
|
||||
echo "--- Running Self Test ---"
|
||||
echo "--- Checking ipsets ---"
|
||||
for portset in "${ALL_PORTSETS[@]}"; do
|
||||
echo "IPSet allowed_${portset} (ports: ${PORTSET_PORTS[$portset]}):"
|
||||
ipset list "allowed_${portset}" | grep -E "^(Name:|Type:|Timeout:|Number of entries:)"
|
||||
done
|
||||
|
||||
echo "--- Testing DNS functionality ---"
|
||||
ss -ln | grep :53 || echo "No process listening on port 53"
|
||||
|
||||
PRIMARY_DNS=$(echo "$DNS_SERVERS" | cut -d',' -f1 | xargs)
|
||||
echo "Testing allowed domain (github.com) with dig:"
|
||||
timeout 10 dig @127.0.0.1 github.com +short || echo "Failed to resolve github.com"
|
||||
|
||||
echo "Testing blocked domain (monadical.com) with dig:"
|
||||
timeout 10 dig @127.0.0.1 monadical.com +short || echo "Successfully blocked monadical.com"
|
||||
# Test allowed domain
|
||||
if [[ -n "${!DOMAIN_PORTSET[@]}" ]]; then
|
||||
test_domain=$(echo "${!DOMAIN_PORTSET[@]}" | cut -d' ' -f1)
|
||||
portset="${DOMAIN_PORTSET[$test_domain]}"
|
||||
echo "Testing allowed domain ($test_domain) with dig:"
|
||||
timeout 10 dig @127.0.0.1 "$test_domain" +short || echo "Failed to resolve $test_domain"
|
||||
|
||||
echo "Testing direct upstream DNS ($PRIMARY_DNS):"
|
||||
timeout 10 dig @"$PRIMARY_DNS" github.com +short || echo "Cannot reach upstream DNS"
|
||||
# Check if IPs were added to ipsets
|
||||
sleep 2 # Give dnsmasq time to populate ipsets
|
||||
echo "Checking ipset allowed_${portset} for $test_domain entries:"
|
||||
ipset list "allowed_${portset}" | grep -E "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" || echo "No entries found"
|
||||
fi
|
||||
|
||||
echo "Testing blocked domain (example.org) with dig:"
|
||||
timeout 10 dig @127.0.0.1 example.org +short || echo "Successfully blocked example.org"
|
||||
|
||||
echo "--- Self Test Complete ---"
|
||||
}
|
||||
|
||||
selftest() {
|
||||
setup_env
|
||||
setup_iptables
|
||||
setup_dnsmasq
|
||||
override_dns
|
||||
|
||||
dnsmasq --test
|
||||
|
||||
dnsmasq --no-daemon --log-facility=- &
|
||||
DNSMASQ_PID=$!
|
||||
sleep 3
|
||||
apply_domain_rules
|
||||
|
||||
|
||||
run_tests
|
||||
|
||||
kill $DNSMASQ_PID
|
||||
# --- Monitoring ---
|
||||
monitor_ipsets() {
|
||||
while true; do
|
||||
sleep 60
|
||||
echo "--- IPSet Status ---"
|
||||
for portset in "${ALL_PORTSETS[@]}"; do
|
||||
count=$(ipset list "allowed_${portset}" | grep -c "^[0-9]" || echo "0")
|
||||
echo "allowed_${portset} (ports ${PORTSET_PORTS[$portset]}): $count entries"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
start() {
|
||||
setup_env
|
||||
parse_domains
|
||||
|
||||
echo "Debug: ALL_PORTSETS array has ${#ALL_PORTSETS[@]} elements"
|
||||
echo "Debug: ALL_PORTSETS contents: ${ALL_PORTSETS[@]}"
|
||||
|
||||
if [[ ${#ALL_PORTSETS[@]} -eq 0 ]]; then
|
||||
echo "No allowed domains configured. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
setup_ipsets
|
||||
setup_iptables
|
||||
setup_dnsmasq
|
||||
override_dns
|
||||
|
||||
# Test dnsmasq config
|
||||
dnsmasq --test
|
||||
|
||||
# Start dnsmasq
|
||||
dnsmasq --no-daemon --log-facility=- &
|
||||
DNSMASQ_PID=$!
|
||||
sleep 3
|
||||
|
||||
apply_domain_rules
|
||||
|
||||
if [[ "$RUN_SELFTEST" == "true" ]]; then
|
||||
run_tests
|
||||
fi
|
||||
|
||||
echo "Network filter setup complete. Monitoring..."
|
||||
|
||||
# Start ipset monitor in background
|
||||
monitor_ipsets &
|
||||
MONITOR_PID=$!
|
||||
|
||||
# Main loop - just monitor dnsmasq health
|
||||
while true; do
|
||||
sleep "$REFRESH_INTERVAL"
|
||||
sleep 60
|
||||
|
||||
# Check dnsmasq health
|
||||
if ! kill -0 $DNSMASQ_PID 2>/dev/null; then
|
||||
dnsmasq --no-daemon &
|
||||
echo "dnsmasq died, restarting..."
|
||||
dnsmasq --no-daemon --log-facility=- &
|
||||
DNSMASQ_PID=$!
|
||||
fi
|
||||
|
||||
# Check resolv.conf
|
||||
if [[ "$(cat /etc/resolv.conf | grep -c '127.0.0.1')" -eq 0 ]]; then
|
||||
echo "resolv.conf was modified, fixing..."
|
||||
override_dns
|
||||
fi
|
||||
|
||||
# Clear dnsmasq cache before refreshing rules
|
||||
kill -HUP $DNSMASQ_PID 2>/dev/null || true
|
||||
|
||||
setup_iptables
|
||||
apply_domain_rules
|
||||
done
|
||||
}
|
||||
|
||||
# --- Cleanup ---
|
||||
cleanup() {
|
||||
echo "Cleaning up..."
|
||||
|
||||
# Kill processes
|
||||
[[ -n "$DNSMASQ_PID" ]] && kill $DNSMASQ_PID 2>/dev/null || true
|
||||
[[ -n "$MONITOR_PID" ]] && kill $MONITOR_PID 2>/dev/null || true
|
||||
|
||||
# Clean up ipsets
|
||||
for portset in "${ALL_PORTSETS[@]}"; do
|
||||
ipset destroy "allowed_${portset}" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Reset iptables
|
||||
iptables -P OUTPUT ACCEPT
|
||||
iptables -F OUTPUT
|
||||
|
||||
exit 0
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# --- Command Dispatcher ---
|
||||
CMD="${1:-start}"
|
||||
case "$CMD" in
|
||||
@@ -189,10 +275,12 @@ case "$CMD" in
|
||||
start
|
||||
;;
|
||||
selftest)
|
||||
selftest
|
||||
RUN_SELFTEST=true
|
||||
start
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $CMD"
|
||||
echo "Usage: $0 [start|selftest]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user