Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions mac-address-cloner/01-setup-dhcp-server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# 01-setup-dhcp-server.sh
#
# Step 1 of 4 – Configure the Raspberry Pi's ethernet interface as a DHCP
# server so the target device can obtain an IP address. This makes it easy
# to spot the device in the lease table and harvest its MAC address.
#
# Prerequisites:
# sudo apt-get install -y dnsmasq
#
# Usage:
# sudo ./01-setup-dhcp-server.sh [INTERFACE]
#
# Default interface: eth0
# After running this script, connect the target device via ethernet.
# Then run 02-capture-mac.sh.

set -euo pipefail

IFACE="${1:-eth0}"
PI_IP="192.168.100.1"
PI_CIDR="192.168.100.1/24"
DHCP_RANGE_START="192.168.100.100"
DHCP_RANGE_END="192.168.100.200"
DHCP_LEASE_TIME="12h"
LEASE_FILE="/var/lib/misc/dnsmasq.leases"
CONFIG_FILE="/etc/dnsmasq.d/mac-cloner.conf"
SAVED_STATE_DIR="/var/lib/mac-address-cloner"

# ── helpers ──────────────────────────────────────────────────────────────────

require_root() {
if [[ $EUID -ne 0 ]]; then
echo "ERROR: This script must be run as root (sudo)." >&2
exit 1
fi
}

require_command() {
local cmd="$1"
if ! command -v "$cmd" &>/dev/null; then
echo "ERROR: Required command '$cmd' not found." >&2
echo " Install it with: sudo apt-get install -y dnsmasq" >&2
exit 1
fi
}

# ── main ─────────────────────────────────────────────────────────────────────

require_root
require_command dnsmasq
require_command ip

echo "==> Setting up DHCP server on interface: $IFACE"

# Save original MAC so it can be restored later
mkdir -p "$SAVED_STATE_DIR"
ORIGINAL_MAC=$(ip link show "$IFACE" | awk '/link\/ether/ {print $2}')
echo "$ORIGINAL_MAC" > "$SAVED_STATE_DIR/original_mac"
echo "$IFACE" > "$SAVED_STATE_DIR/interface"
echo " Original MAC saved: $ORIGINAL_MAC → $SAVED_STATE_DIR/original_mac"

# Bring the interface up and assign a static IP
ip link set "$IFACE" up
ip addr flush dev "$IFACE"
ip addr add "$PI_CIDR" dev "$IFACE"

echo " Interface $IFACE configured with IP $PI_IP"

# Write a minimal dnsmasq configuration scoped to this interface only
cat > "$CONFIG_FILE" <<EOF
# mac-cloner.conf – generated by 01-setup-dhcp-server.sh
interface=$IFACE
bind-interfaces
dhcp-range=$DHCP_RANGE_START,$DHCP_RANGE_END,$DHCP_LEASE_TIME
dhcp-leasefile=$LEASE_FILE
log-dhcp
EOF

echo " dnsmasq config written to $CONFIG_FILE"

# Restart dnsmasq so it picks up the new config
systemctl restart dnsmasq
systemctl is-active --quiet dnsmasq && echo " dnsmasq is running." || {
echo "ERROR: dnsmasq failed to start. Check: journalctl -u dnsmasq" >&2
exit 1
}

echo ""
echo "✓ DHCP server ready on $IFACE ($PI_IP)"
echo ""
echo "NEXT STEP:"
echo " 1. Connect the target device to the Raspberry Pi via ethernet."
echo " 2. Wait for it to obtain an IP address (usually a few seconds)."
echo " 3. Run: sudo ./02-capture-mac.sh"
96 changes: 96 additions & 0 deletions mac-address-cloner/02-capture-mac.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# 02-capture-mac.sh
#
# Step 2 of 4 – Discover the MAC address of the device that obtained a DHCP
# lease from the Pi, and save it to disk so the next script can clone it.
#
# The script first reads dnsmasq's lease file. If no lease is found there it
# falls back to the kernel ARP table.
#
# Usage:
# sudo ./02-capture-mac.sh [INTERFACE]
#
# After running this script, disconnect the target device from the Pi and run
# 03-clone-and-connect.sh.

set -euo pipefail

IFACE="${1:-eth0}"
LEASE_FILE="/var/lib/misc/dnsmasq.leases"
SAVED_STATE_DIR="/var/lib/mac-address-cloner"
MAC_FILE="$SAVED_STATE_DIR/captured_mac"

# ── helpers ──────────────────────────────────────────────────────────────────

require_root() {
if [[ $EUID -ne 0 ]]; then
echo "ERROR: This script must be run as root (sudo)." >&2
exit 1
fi
}

# Try to find a MAC from the dnsmasq lease file.
# Returns the first non-Pi MAC found, or empty string.
mac_from_leases() {
[[ -f "$LEASE_FILE" ]] || return 0
# Lease file format: <expiry> <mac> <ip> <hostname> <client-id>
awk '{print $2}' "$LEASE_FILE" | head -1
}

# Fall back to the ARP table for the given interface.
# Returns the MAC of the first host reachable on that interface, or empty.
mac_from_arp() {
local iface="$1"
ip neigh show dev "$iface" 2>/dev/null \
| awk '/lladdr/ {print $5}' \
| head -1
}

# ── main ─────────────────────────────────────────────────────────────────────

require_root

echo "==> Searching for target device MAC address …"

CAPTURED_MAC=""

# Primary source: dnsmasq lease file
CAPTURED_MAC=$(mac_from_leases)
if [[ -n "$CAPTURED_MAC" ]]; then
echo " Found in dnsmasq lease file: $CAPTURED_MAC"
else
echo " No lease found – falling back to ARP table on $IFACE …"
# Ping the broadcast address to refresh the ARP cache
ping -c 2 -b "192.168.100.255" -I "$IFACE" &>/dev/null || true
sleep 1
CAPTURED_MAC=$(mac_from_arp "$IFACE")
fi

if [[ -z "$CAPTURED_MAC" ]]; then
echo "" >&2
echo "ERROR: Could not detect any device on $IFACE." >&2
echo " Make sure the device is connected and has obtained an IP." >&2
echo " You can verify with: cat $LEASE_FILE" >&2
echo " Or manually provide the MAC:" >&2
echo " echo 'aa:bb:cc:dd:ee:ff' | sudo tee $MAC_FILE" >&2
exit 1
fi

# Validate MAC format (xx:xx:xx:xx:xx:xx)
if ! echo "$CAPTURED_MAC" | grep -qE '^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$'; then
echo "ERROR: Captured value '$CAPTURED_MAC' does not look like a valid MAC." >&2
exit 1
fi

mkdir -p "$SAVED_STATE_DIR"
echo "$CAPTURED_MAC" > "$MAC_FILE"

echo ""
echo "✓ MAC address captured and saved."
echo " MAC : $CAPTURED_MAC"
echo " File : $MAC_FILE"
echo ""
echo "NEXT STEP:"
echo " 1. Disconnect the target device from the Raspberry Pi."
echo " 2. Connect the Pi to your LAN with the same ethernet cable."
echo " 3. Run: sudo ./03-clone-and-connect.sh"
108 changes: 108 additions & 0 deletions mac-address-cloner/03-clone-and-connect.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env bash
# 03-clone-and-connect.sh
#
# Step 3 of 4 – Clone the previously captured MAC address onto the ethernet
# interface and connect to the LAN via DHCP.
#
# Usage:
# sudo ./03-clone-and-connect.sh [INTERFACE]
#
# After the Pi has an IP from the LAN you can use it as normal.
# To restore the original MAC run 04-restore-mac.sh.

set -euo pipefail

IFACE="${1:-eth0}"
SAVED_STATE_DIR="/var/lib/mac-address-cloner"
MAC_FILE="$SAVED_STATE_DIR/captured_mac"
IFACE_FILE="$SAVED_STATE_DIR/interface"
DNSMASQ_CONFIG="/etc/dnsmasq.d/mac-cloner.conf"

# ── helpers ──────────────────────────────────────────────────────────────────

require_root() {
if [[ $EUID -ne 0 ]]; then
echo "ERROR: This script must be run as root (sudo)." >&2
exit 1
fi
}

require_file() {
local file="$1"
if [[ ! -f "$file" ]]; then
echo "ERROR: Expected file not found: $file" >&2
echo " Did you run 01-setup-dhcp-server.sh and 02-capture-mac.sh first?" >&2
exit 1
fi
}

# ── main ─────────────────────────────────────────────────────────────────────

require_root
require_file "$MAC_FILE"

TARGET_MAC=$(cat "$MAC_FILE")
# Use the interface saved in step 1 unless overridden on the command line
if [[ -f "$IFACE_FILE" && "${1:-}" == "" ]]; then
IFACE=$(cat "$IFACE_FILE")
fi

echo "==> Cloning MAC address $TARGET_MAC onto $IFACE …"

# Validate MAC format
if ! echo "$TARGET_MAC" | grep -qE '^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$'; then
echo "ERROR: Saved MAC '$TARGET_MAC' is not a valid MAC address." >&2
exit 1
fi

# Stop the DHCP server – we no longer need it
if systemctl is-active --quiet dnsmasq 2>/dev/null; then
echo " Stopping dnsmasq DHCP server …"
systemctl stop dnsmasq
fi

# Remove the DHCP server config so dnsmasq doesn't bind this interface on boot
if [[ -f "$DNSMASQ_CONFIG" ]]; then
rm -f "$DNSMASQ_CONFIG"
echo " Removed dnsmasq config: $DNSMASQ_CONFIG"
fi

# Bring the interface down, change MAC, bring it back up
ip link set "$IFACE" down
ip link set "$IFACE" address "$TARGET_MAC"
ip link set "$IFACE" up

CURRENT_MAC=$(ip link show "$IFACE" | awk '/link\/ether/ {print $2}')
if [[ "${CURRENT_MAC,,}" != "${TARGET_MAC,,}" ]]; then
echo "ERROR: MAC address was not applied (got $CURRENT_MAC)." >&2
exit 1
fi

echo " MAC address set to $CURRENT_MAC on $IFACE"

# Clear any leftover static IP from step 1
ip addr flush dev "$IFACE"

# Obtain a new DHCP lease from the LAN
echo " Requesting DHCP lease from LAN …"
if command -v dhclient &>/dev/null; then
dhclient -v "$IFACE" 2>&1 | grep -E 'bound|DHCPACK|DHCPDISCOVER|error' || true
elif command -v dhcpcd &>/dev/null; then
dhcpcd "$IFACE"
else
echo "WARNING: Neither dhclient nor dhcpcd found." >&2
echo " You may need to bring up the interface manually:" >&2
echo " sudo dhcpcd $IFACE OR sudo dhclient $IFACE" >&2
fi

# Show the resulting IP
sleep 2
ASSIGNED_IP=$(ip addr show "$IFACE" | awk '/inet / {print $2}' | head -1)
echo ""
echo "✓ MAC cloned and connected to LAN."
echo " Interface : $IFACE"
echo " MAC : $CURRENT_MAC (cloned from target device)"
echo " IP : ${ASSIGNED_IP:-<not yet assigned – check with 'ip addr show $IFACE'>}"
echo ""
echo "NEXT STEP (when done):"
echo " To restore the original MAC, run: sudo ./04-restore-mac.sh"
81 changes: 81 additions & 0 deletions mac-address-cloner/04-restore-mac.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# 04-restore-mac.sh
#
# Step 4 of 4 (optional) – Restore the Raspberry Pi's original (burned-in)
# hardware MAC address on the ethernet interface and release any DHCP lease
# that was obtained with the cloned MAC.
#
# Usage:
# sudo ./04-restore-mac.sh [INTERFACE]

set -euo pipefail

IFACE="${1:-eth0}"
SAVED_STATE_DIR="/var/lib/mac-address-cloner"
ORIGINAL_MAC_FILE="$SAVED_STATE_DIR/original_mac"
IFACE_FILE="$SAVED_STATE_DIR/interface"

# ── helpers ──────────────────────────────────────────────────────────────────

require_root() {
if [[ $EUID -ne 0 ]]; then
echo "ERROR: This script must be run as root (sudo)." >&2
exit 1
fi
}

# ── main ─────────────────────────────────────────────────────────────────────

require_root

# Use the interface saved in step 1 unless overridden on the command line
if [[ -f "$IFACE_FILE" && "${1:-}" == "" ]]; then
IFACE=$(cat "$IFACE_FILE")
fi

if [[ ! -f "$ORIGINAL_MAC_FILE" ]]; then
echo "ERROR: Original MAC file not found: $ORIGINAL_MAC_FILE" >&2
echo " Was 01-setup-dhcp-server.sh run before the MAC was changed?" >&2
echo ""
echo " You can read the hardware-burned MAC directly from the kernel:" >&2
echo " cat /sys/class/net/$IFACE/address" >&2
echo " On Raspberry Pi OS you can also reboot – the original MAC is" >&2
echo " restored automatically on reboot unless made persistent." >&2
exit 1
fi

ORIGINAL_MAC=$(cat "$ORIGINAL_MAC_FILE")

echo "==> Restoring original MAC address $ORIGINAL_MAC on $IFACE …"

# Release the DHCP lease obtained with the cloned MAC
if command -v dhclient &>/dev/null; then
dhclient -r "$IFACE" 2>/dev/null || true
elif command -v dhcpcd &>/dev/null; then
dhcpcd -k "$IFACE" 2>/dev/null || true
fi

# Restore MAC
ip link set "$IFACE" down
ip link set "$IFACE" address "$ORIGINAL_MAC"
ip link set "$IFACE" up

CURRENT_MAC=$(ip link show "$IFACE" | awk '/link\/ether/ {print $2}')
echo " MAC address is now: $CURRENT_MAC"

# Request a new DHCP lease with the original MAC
echo " Requesting DHCP lease …"
if command -v dhclient &>/dev/null; then
dhclient "$IFACE" 2>/dev/null || true
elif command -v dhcpcd &>/dev/null; then
dhcpcd "$IFACE" 2>/dev/null || true
fi

sleep 2
ASSIGNED_IP=$(ip addr show "$IFACE" | awk '/inet / {print $2}' | head -1)

echo ""
echo "✓ Original MAC restored."
echo " Interface : $IFACE"
echo " MAC : $CURRENT_MAC"
echo " IP : ${ASSIGNED_IP:-<not yet assigned – check with 'ip addr show $IFACE'>}"
Loading