diff --git a/mac-address-cloner/01-setup-dhcp-server.sh b/mac-address-cloner/01-setup-dhcp-server.sh new file mode 100755 index 0000000..fbcbf4f --- /dev/null +++ b/mac-address-cloner/01-setup-dhcp-server.sh @@ -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" <&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" diff --git a/mac-address-cloner/02-capture-mac.sh b/mac-address-cloner/02-capture-mac.sh new file mode 100755 index 0000000..629aa5d --- /dev/null +++ b/mac-address-cloner/02-capture-mac.sh @@ -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: + 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" diff --git a/mac-address-cloner/03-clone-and-connect.sh b/mac-address-cloner/03-clone-and-connect.sh new file mode 100755 index 0000000..bb6cf85 --- /dev/null +++ b/mac-address-cloner/03-clone-and-connect.sh @@ -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:-}" +echo "" +echo "NEXT STEP (when done):" +echo " To restore the original MAC, run: sudo ./04-restore-mac.sh" diff --git a/mac-address-cloner/04-restore-mac.sh b/mac-address-cloner/04-restore-mac.sh new file mode 100755 index 0000000..708ec81 --- /dev/null +++ b/mac-address-cloner/04-restore-mac.sh @@ -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:-}" diff --git a/mac-address-cloner/README.md b/mac-address-cloner/README.md new file mode 100644 index 0000000..2e7ecf1 --- /dev/null +++ b/mac-address-cloner/README.md @@ -0,0 +1,151 @@ +# MAC Address Cloner for Raspberry Pi + +A set of four shell scripts that lets you: + +1. **Act as a DHCP server** – connect any device to the Pi via ethernet and automatically hand it an IP address. +2. **Capture the device's MAC address** – read it from the DHCP lease file or the ARP table. +3. **Clone the MAC onto the LAN interface** – disconnect the device, plug the Pi into the LAN and impersonate the device's MAC so the network sees the device's identity. +4. **Restore the original MAC** – when you are finished, put the Pi's own hardware MAC back. + +--- + +## Why would you need this? + +Some networks (hotels, ISPs, managed switches) tie access to a specific MAC address. If you want to share that "registered" connection with your Pi or other devices, or if you are debugging a network that expects a particular MAC, these scripts automate the whole workflow. + +--- + +## Prerequisites + +| Package | Purpose | +|---------|---------| +| `dnsmasq` | Lightweight DHCP / DNS server | +| `iproute2` | `ip` command (pre-installed on Raspberry Pi OS) | +| `dhclient` **or** `dhcpcd` | DHCP client (one of these is pre-installed) | + +```bash +sudo apt-get update +sudo apt-get install -y dnsmasq +``` + +> **Note:** `dnsmasq` may conflict with `systemd-resolved`. If port 53 is already in use, add `port=0` to `/etc/dnsmasq.d/mac-cloner.conf` (the script creates this file) to disable DNS and keep only DHCP. + +--- + +## Usage – step by step + +All scripts must be run as **root** (`sudo`). They default to the `eth0` interface; pass a different interface name as the first argument if needed (e.g. `enp3s0` or `eth1`). + +State is persisted in `/var/lib/mac-address-cloner/` so the scripts can be run at different times without repeating earlier steps. + +--- + +### Step 1 – Set up the DHCP server + +```bash +sudo ./01-setup-dhcp-server.sh [INTERFACE] +``` + +What it does: +- Saves the Pi's current (original) MAC address. +- Assigns a static IP (`192.168.100.1/24`) to the ethernet interface. +- Writes a minimal `dnsmasq` config and starts the DHCP server. + +After running this script, **connect the target device** to the Pi via ethernet. Wait a few seconds for it to obtain an IP address. + +--- + +### Step 2 – Capture the device's MAC address + +```bash +sudo ./02-capture-mac.sh [INTERFACE] +``` + +What it does: +- Reads the MAC address from `dnsmasq`'s lease file. +- Falls back to the ARP table if no lease is found. +- Saves the captured MAC to `/var/lib/mac-address-cloner/captured_mac`. + +**Disconnect the target device** after this step, then plug the Pi into the LAN. + +--- + +### Step 3 – Clone the MAC and connect to LAN + +```bash +sudo ./03-clone-and-connect.sh [INTERFACE] +``` + +What it does: +- Stops the `dnsmasq` DHCP server. +- Sets the interface's MAC address to the captured value (`ip link set … address`). +- Flushes the static IP and requests a new DHCP lease from the LAN. + +The Pi now appears on the LAN as the target device. + +--- + +### Step 4 – Restore the original MAC (optional) + +```bash +sudo ./04-restore-mac.sh [INTERFACE] +``` + +What it does: +- Releases the DHCP lease obtained with the cloned MAC. +- Restores the Pi's original hardware MAC address. +- Requests a fresh DHCP lease with the original MAC. + +> **Tip:** On Raspberry Pi OS the MAC is also restored automatically on every reboot, so a simple `sudo reboot` works as an alternative. + +--- + +## Persisted state files + +| File | Description | +|------|-------------| +| `/var/lib/mac-address-cloner/original_mac` | Pi's original MAC (saved in step 1) | +| `/var/lib/mac-address-cloner/captured_mac` | Target device's MAC (saved in step 2) | +| `/var/lib/mac-address-cloner/interface` | Interface name used in step 1 | +| `/etc/dnsmasq.d/mac-cloner.conf` | dnsmasq config (removed in step 3) | +| `/var/lib/misc/dnsmasq.leases` | dnsmasq lease database | + +--- + +## Troubleshooting + +**dnsmasq fails to start** +```bash +journalctl -u dnsmasq --no-pager | tail -30 +``` +A common cause is another process already listening on port 53 (e.g. `systemd-resolved`). Stop it temporarily: +```bash +sudo systemctl stop systemd-resolved +sudo ./01-setup-dhcp-server.sh +``` + +**No MAC captured in step 2** +- Confirm the device is connected and got an IP: `cat /var/lib/misc/dnsmasq.leases` +- Inspect the ARP table: `ip neigh show dev eth0` +- Enter the MAC manually and skip to step 3: + ```bash + sudo mkdir -p /var/lib/mac-address-cloner + echo 'aa:bb:cc:dd:ee:ff' | sudo tee /var/lib/mac-address-cloner/captured_mac + ``` + +**MAC not applied (step 3 error)** +Some network drivers or virtualized environments do not allow MAC changes. On a real Raspberry Pi this should always work. Check `dmesg | tail` for driver-level errors. + +**Pi does not get an IP from the LAN (step 3)** +```bash +sudo dhclient -v eth0 +# or +sudo dhcpcd eth0 +``` +Verify connectivity with: `ip addr show eth0` and `ping -c3 8.8.8.8`. + +--- + +## Security note + +Changing your MAC address to impersonate another device may violate the terms of service of the network you are connecting to. Use these scripts only on networks you own or have explicit permission to test.