Features: - Docker-based DHCP server with MAC address whitelisting - Binds to specific ethernet interface only - NO DNS/gateway advertised (direct link only, not a router) - Configurable network parameters (subnet, DHCP range, lease times) - Systemd service integration for Arch/Manjaro - Test environment with isolated network (172.20.0.0/24) - Auto-configuration script to detect network settings - Complete Makefile with management targets Security: - Only responds to whitelisted MAC addresses - deny unknown-clients configuration - Runs in Docker container for isolation Configuration: - Copy .example files to create your config - interface.conf: Network interface to bind to - whitelist.conf: Allowed MAC addresses - network.conf: Network parameters (optional)
176 lines
5.6 KiB
Bash
Executable File
176 lines
5.6 KiB
Bash
Executable File
#!/bin/bash
|
|
set -e
|
|
|
|
INTERFACE_FILE="/config/interface.conf"
|
|
WHITELIST_FILE="/config/whitelist.conf"
|
|
NETWORK_FILE="/config/network.conf"
|
|
DHCPD_CONF="/etc/dhcp/dhcpd.conf"
|
|
|
|
if [ ! -f "$INTERFACE_FILE" ]; then
|
|
echo "Error: Interface configuration file not found: $INTERFACE_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -f "$WHITELIST_FILE" ]; then
|
|
echo "Error: Whitelist configuration file not found: $WHITELIST_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
INTERFACE=$(cat "$INTERFACE_FILE" | tr -d '\n\r ')
|
|
echo "Using interface: $INTERFACE"
|
|
|
|
# Wait for interface to be available
|
|
for i in {1..30}; do
|
|
if ip link show "$INTERFACE" &>/dev/null; then
|
|
break
|
|
fi
|
|
echo "Waiting for interface $INTERFACE..."
|
|
sleep 1
|
|
done
|
|
|
|
if ! ip link show "$INTERFACE" &>/dev/null; then
|
|
echo "Error: Interface $INTERFACE not found"
|
|
exit 1
|
|
fi
|
|
|
|
# Load network configuration if available
|
|
if [ -f "$NETWORK_FILE" ]; then
|
|
echo "Loading network configuration from $NETWORK_FILE"
|
|
source "$NETWORK_FILE"
|
|
else
|
|
echo "No network configuration file found, using defaults"
|
|
fi
|
|
|
|
# Set defaults for unset variables
|
|
RANGE_START_OFFSET=${RANGE_START_OFFSET:-10}
|
|
RANGE_END_OFFSET=${RANGE_END_OFFSET:-100}
|
|
LEASE_TIME=${LEASE_TIME:-43200}
|
|
MAX_LEASE_TIME=${MAX_LEASE_TIME:-86400}
|
|
|
|
# Get interface IP and network info
|
|
INTERFACE_IP=$(ip -4 addr show "$INTERFACE" | grep "inet " | awk '{print $2}' | cut -d'/' -f1)
|
|
INTERFACE_CIDR=$(ip -4 addr show "$INTERFACE" | grep "inet " | awk '{print $2}' | cut -d'/' -f2)
|
|
|
|
# Check if subnet is manually configured
|
|
if [ -n "$SUBNET" ] && [ -n "$NETMASK" ]; then
|
|
echo "Using manual network configuration: $SUBNET/$NETMASK"
|
|
INTERFACE_NETWORK="$SUBNET"
|
|
# Calculate broadcast from subnet and netmask
|
|
if [ "$NETMASK" = "255.255.255.0" ]; then
|
|
BROADCAST="${SUBNET%.*}.255"
|
|
elif [ "$NETMASK" = "255.255.0.0" ]; then
|
|
BROADCAST="${SUBNET%.*.*}.255.255"
|
|
elif [ "$NETMASK" = "255.0.0.0" ]; then
|
|
BROADCAST="${SUBNET%.*.*.*}.255.255.255"
|
|
else
|
|
BROADCAST="${SUBNET%.*}.255"
|
|
fi
|
|
else
|
|
echo "Auto-detecting network configuration from interface"
|
|
# For test environment, handle Docker's internal network properly
|
|
if [ "$INTERFACE" = "eth0" ] && [ -n "$INTERFACE_IP" ]; then
|
|
# Extract network base from the actual IP
|
|
INTERFACE_NETWORK=$(echo "$INTERFACE_IP" | sed 's/\.[0-9]*$/\.0/')
|
|
else
|
|
# Fallback for physical interfaces
|
|
INTERFACE_NETWORK=$(ip -4 addr show "$INTERFACE" | grep "inet " | awk '{print $2}' | cut -d'/' -f1 | sed 's/\.[0-9]*$/\.0/')
|
|
fi
|
|
|
|
# Convert CIDR to netmask and calculate broadcast
|
|
case $INTERFACE_CIDR in
|
|
24)
|
|
NETMASK="255.255.255.0"
|
|
BROADCAST="${INTERFACE_NETWORK%.*}.255"
|
|
;;
|
|
16)
|
|
NETMASK="255.255.0.0"
|
|
BROADCAST="${INTERFACE_NETWORK%.*.*}.255.255"
|
|
;;
|
|
8)
|
|
NETMASK="255.0.0.0"
|
|
BROADCAST="${INTERFACE_NETWORK%.*.*.*}.255.255.255"
|
|
;;
|
|
*)
|
|
NETMASK="255.255.255.0"
|
|
BROADCAST="${INTERFACE_NETWORK%.*}.255"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
echo "Interface IP: $INTERFACE_IP"
|
|
echo "Network: $INTERFACE_NETWORK/$NETMASK"
|
|
echo "DHCP Range: ${INTERFACE_NETWORK%.*}.$RANGE_START_OFFSET - ${INTERFACE_NETWORK%.*}.$RANGE_END_OFFSET"
|
|
echo "Lease Time: $LEASE_TIME seconds"
|
|
|
|
# Generate dhcpd.conf from template
|
|
cp /etc/dhcp/dhcpd.conf.template "$DHCPD_CONF"
|
|
|
|
# Generate host entries for whitelisted MACs
|
|
HOST_ENTRIES=""
|
|
HOST_NUM=1
|
|
echo "Reading whitelist from $WHITELIST_FILE"
|
|
|
|
# Check if file exists and is readable
|
|
if [ ! -f "$WHITELIST_FILE" ]; then
|
|
echo "ERROR: Whitelist file does not exist!"
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -r "$WHITELIST_FILE" ]; then
|
|
echo "ERROR: Whitelist file is not readable!"
|
|
exit 1
|
|
fi
|
|
|
|
echo "File size: $(stat -c%s "$WHITELIST_FILE") bytes"
|
|
echo "File contents (hex dump):"
|
|
hexdump -C "$WHITELIST_FILE" | head -5
|
|
echo "File contents (raw):"
|
|
cat "$WHITELIST_FILE"
|
|
echo ""
|
|
echo "--- Processing MACs ---"
|
|
|
|
# Read file line by line without subshell
|
|
while IFS= read -r MAC || [ -n "$MAC" ]; do
|
|
# Remove any whitespace, carriage returns, and convert to lowercase
|
|
ORIG_MAC="$MAC"
|
|
MAC=$(echo "$MAC" | tr -d '\r\n\t ' | tr '[:upper:]' '[:lower:]')
|
|
echo "Processing: original='$ORIG_MAC' cleaned='$MAC'"
|
|
|
|
if [ -n "$MAC" ]; then
|
|
# Generate IP based on host number (starting from .10)
|
|
LAST_OCTET=$((10 + HOST_NUM - 1))
|
|
HOST_IP="${INTERFACE_NETWORK%.*}.$LAST_OCTET"
|
|
NEW_ENTRY="
|
|
host client$HOST_NUM {
|
|
hardware ethernet $MAC;
|
|
fixed-address $HOST_IP;
|
|
}"
|
|
HOST_ENTRIES="${HOST_ENTRIES}${NEW_ENTRY}"
|
|
echo "Added whitelist entry: $MAC -> $HOST_IP"
|
|
HOST_NUM=$((HOST_NUM + 1))
|
|
fi
|
|
done < "$WHITELIST_FILE"
|
|
|
|
if [ -z "$HOST_ENTRIES" ]; then
|
|
echo "Warning: No MAC addresses found in whitelist!"
|
|
else
|
|
echo "Total whitelisted MACs: $((HOST_NUM - 1))"
|
|
fi
|
|
|
|
# Replace placeholders in config
|
|
sed -i "s#__SUBNET__#$INTERFACE_NETWORK#g" "$DHCPD_CONF"
|
|
sed -i "s#__NETMASK__#$NETMASK#g" "$DHCPD_CONF"
|
|
sed -i "s#__RANGE_START__#${INTERFACE_NETWORK%.*}.$RANGE_START_OFFSET#g" "$DHCPD_CONF"
|
|
sed -i "s#__RANGE_END__#${INTERFACE_NETWORK%.*}.$RANGE_END_OFFSET#g" "$DHCPD_CONF"
|
|
sed -i "s#__BROADCAST__#$BROADCAST#g" "$DHCPD_CONF"
|
|
sed -i "s#__LEASE_TIME__#$LEASE_TIME#g" "$DHCPD_CONF"
|
|
sed -i "s#__MAX_LEASE_TIME__#$MAX_LEASE_TIME#g" "$DHCPD_CONF"
|
|
# Use a different approach for multiline HOST_ENTRIES
|
|
awk -v entries="$HOST_ENTRIES" '{gsub(/__HOST_ENTRIES__/, entries); print}' "$DHCPD_CONF" > "$DHCPD_CONF.tmp" && mv "$DHCPD_CONF.tmp" "$DHCPD_CONF"
|
|
|
|
echo "Starting DHCP server on interface $INTERFACE..."
|
|
echo "Configuration:"
|
|
cat "$DHCPD_CONF"
|
|
|
|
# Start DHCP server
|
|
exec dhcpd -4 -f -d -cf "$DHCPD_CONF" "$INTERFACE" |