diff --git a/.gitignore b/.gitignore index 12e51a3..ae5d28e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -config.sh +.idea +config/config.sh # ---> Python # Byte-compiled / optimized / DLL files diff --git a/bridge-install.sh b/bridge-install.sh index 6cfc65d..e5c3dbd 100755 --- a/bridge-install.sh +++ b/bridge-install.sh @@ -6,17 +6,17 @@ # Config SOURCE=${BASH_SOURCE[0]} -while [ -L "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink - DIR=$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd ) +while [ -L "$SOURCE" ]; do + DIR=$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd) SOURCE=$(readlink "$SOURCE") - [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located + [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE done -DIR=$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd ) +DIR=$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd) -if [[ -f "$DIR/config.sh" ]]; then - source "$DIR/config.sh" +if [[ -f "$DIR/config/config.sh" ]]; then + source "$DIR/config/config.sh" else - echo "config.sh missing!" + echo "$DIR/config/config.sh missing!" exit 1 fi @@ -25,12 +25,35 @@ fi # Must be run as root if [ "$(id -u)" -ne 0 ]; then - echo 'This script must be run as root.' >&2 - exit 1 + echo 'This script must be run as root.' >&2 + exit 1 fi +# Reset interfaces +iptables -X +iptables -F +iptables -t nat -X +iptables -t nat -F +echo "Cleared iptables" + +ifconfig $WLAN_IFACE down +ifconfig $WLAN_IFACE hw ether $(ethtool -P $WLAN_IFACE | awk '{print $3}') +ifconfig $WLAN_IFACE up +echo "Reset $WLAN_IFACE" + +while true; do + WLAN_IFACE_IP=$(ip -4 -br addr show $WLAN_IFACE | grep -Po "\\d+\\.\\d+\\.\\d+\\.\\d+") + if [ -n "${WLAN_IFACE_IP}" ]; then + echo "Got it!" + break + fi + echo "Waiting for $WLAN_IFACE to get an IP..." + sleep 5 +done + # We only need to get the $WLAN_IFACE IP address and will copy it over to $ETH_IFACE later -WLAN_IFACE_IP=$(ip -4 -br addr show $WLAN_IFACE | grep -Po "\\d+\\.\\d+\\.\\d+\\.\\d+") +WLAN_NETMASK=$(ip addr show $WLAN_IFACE | grep -w inet | awk '{print $2}' | cut -d'/' -f2) +WLAN_NETMASK_CIDR=$(ip addr show $WLAN_IFACE | grep -w inet | awk '{print $2}' | cut -d'/' -f2) if $NON_INTERACTIVE; then NON_INTERACTIVE_APT="-y" @@ -41,16 +64,18 @@ fi # ============================================================================== # Install stuff -echo "# INSTALL THINGS #" +echo -e "\n# INSTALL THINGS #" -echo -e "\nUpdating...\n\n" +echo -e "Updating...\n\n" +service systemd-resolved start +sudo systemctl stop dnsmasq apt-get update apt-get upgrade $NON_INTERACTIVE_APT echo -e "\n" -THINGS_TO_INSTALL="parprouted dhcp-helper net-tools" +THINGS_TO_INSTALL="parprouted dhcp-helper net-tools ethtool dnsmasq" if ! $NON_INTERACTIVE; then echo "Going to install: $THINGS_TO_INSTALL" @@ -64,7 +89,9 @@ apt-get install $NON_INTERACTIVE_APT $THINGS_TO_INSTALL echo -e "\n\nSetting up services...\n" systemctl stop dhcp-helper -systemctl enable dhcp-helper +systemctl disable dhcp-helper # remember to enable it later +sudo systemctl stop dnsmasq +sudo systemctl disable dnsmasq if ! $NON_INTERACTIVE; then echo -e "\n\nGoing to replace networking with systemd-networkd." @@ -77,10 +104,10 @@ apt-get --autoremove $NON_INTERACTIVE_APT purge ifupdown dhcpcd5 isc-dhcp-client echo -e "\n\nConnecting to WiFi..." WPA_SUPP_FILE="/etc/wpa_supplicant/wpa_supplicant-$WLAN_IFACE.conf" -cat > "$WPA_SUPP_FILE" <"$WPA_SUPP_FILE" < "$NET_CONF_FILE" <"$NET_CONF_FILE" < "$DHCP_HELPER_CONF" <> /etc/dhcpcd.conf - -# Disable dhcpcd control of $ETH_IFACE. -# grep "^denyinterfaces ${ETH_IFACE}\$" /etc/dhcpcd.conf || printf "denyinterfaces $ETH_IFACE\n" >> /etc/dhcpcd.conf - -# Enable avahi reflector if it's not already enabled. -sed -i'' 's/#enable-reflector=no/enable-reflector=yes/' /etc/avahi/avahi-daemon.conf -grep '^enable-reflector=yes$' /etc/avahi/avahi-daemon.conf || { - printf "something went wrong...\n\n" - printf "Manually set 'enable-reflector=yes in /etc/avahi/avahi-daemon.conf'\n" -} -echo "Enabled avahi reflector." - -PARPROUTED_SERVICE="/etc/systemd/system/parprouted.service" -cat > "$PARPROUTED_SERVICE" </dev/null 2>&1 && pwd) + SOURCE=$(readlink "$SOURCE") + [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR=$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd) + +if [[ -f "$DIR/../config/config.sh" ]]; then + . "$DIR/../config/config.sh" +else + echo "$DIR/../config/config.sh missing!" + exit 1 +fi + +if [ "$(id -u)" -ne 0 ]; then + echo 'This script must be run as root.' >&2 + exit 1 +fi + +. "$DIR/get-dhcp-dns.sh" + +# ============================================================================== + +PRIVATE_LAN_IP="192.168.2.1" +BRIDGED_CLIENT_IP="192.168.2.2" + +# Configure the wired interface with the bridge IP address +ifconfig $ETH_IFACE $PRIVATE_LAN_IP netmask 255.255.255.0 up + +# Mirror the DNS servers to the private LAN +DHCP_DNS=($(get_dns_servers "$WLAN_IFACE")) +if [ -n "$DHCP_DNS" ]; then + dns_servers_config="" + for server in "${DHCP_DNS[@]}"; do + dns_servers_config+="server=$server"$'\n' + done + + dhcp_opt_6_config="dhcp-option=6" + for server in "${DHCP_DNS[@]}"; do + dhcp_opt_6_config+=",$server" + done + echo "Mirrored WLAN DHCP DNS servers: ${DHCP_DNS[*]}" +else + dns_servers_config="""server=1.1.1.1 +server=1.0.0.1""" + dhcp_opt_6_config="" +fi + +# Also mirror DNS domain +DHCP_DNS_DOMAIN=$(get_dns_domain $WLAN_IFACE) +if [ -n "$DHCP_DNS_DOMAIN" ]; then + dns_domain_config="domain=$DHCP_DNS_DOMAIN" + echo "Mirrored WLAN DHCP DNS domain: $DHCP_DNS_DOMAIN" +else + dns_domain_config="" +fi + +# Create the dnsmasq.conf file with the generated DNS server config +cat >/etc/dnsmasq.conf </dev/null 2>&1 && pwd) + SOURCE=$(readlink "$SOURCE") + [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE +done +DIR=$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd) + +if [[ -f "$DIR/../config/config.sh" ]]; then + source "$DIR/../config/config.sh" +else + echo "$DIR/../config/config.sh missing!" + exit 1 +fi + +# Must be run as root +if [ "$(id -u)" -ne 0 ]; then + echo 'This script must be run as root.' >&2 + exit 1 +fi + +# ============================================================================== + +iptables -X +iptables -F +iptables -t nat -X +iptables -t nat -F +echo "Cleared iptables" + +# Restore MAC address to WLAN interface +ifconfig $WLAN_IFACE down +ifconfig $WLAN_IFACE hw ether "$(ethtool -P $WLAN_IFACE | awk '{print $3}')" +ifconfig $WLAN_IFACE up +echo "Reset $WLAN_IFACE" + +while true; do + WLAN_IFACE_IP=$(ip -4 -br addr show $WLAN_IFACE | grep -Po "\\d+\\.\\d+\\.\\d+\\.\\d+") + if [ -n "${WLAN_IFACE_IP}" ]; then + echo "Got it!" + break + fi + echo "Waiting for $WLAN_IFACE to get an IP..." + sleep 5 +done + +service systemd-resolved start +sudo systemctl stop dnsmasq +echo "Reset DNS services" + +echo -e "\nConnecting to WiFi..." + +WPA_SUPP_FILE="/etc/wpa_supplicant/wpa_supplicant-$WLAN_IFACE.conf" +cat >"$WPA_SUPP_FILE" <"$NET_CONF_FILE" </dev/null 2>&1 && pwd) + SOURCE=$(readlink "$SOURCE") + [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE +done +DIR=$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd) + +if [[ -f "$DIR/../config/config.sh" ]]; then + . "$DIR/../config/config.sh" +else + echo "$DIR/../config/config.sh missing!" + exit 1 +fi + +if [ "$(id -u)" -ne 0 ]; then + echo 'This script must be run as root.' >&2 + exit 1 +fi + +. "$DIR/get_client_mac_address.sh" + +# ============================================================================== + +MAC_OTHR=$(get_client_mac_address $ETH_IFACE) +if [ -z "$MAC_OTHR" ]; then + echo "Bridged client not found! MAC address was empty." + exit 1 +else + echo "Cloning MAC: $MAC_OTHR" +fi + +# Clone MAC address of the wired-only device to the WiFi device +ifconfig $WLAN_IFACE down +ifconfig $WLAN_IFACE hw ether $MAC_OTHR +ifconfig $WLAN_IFACE up +echo "Set $WLAN_IFACE MAC to $MAC_OTHR" + +while true; do + WLAN_IFACE_IP=$(ip -4 -br addr show $WLAN_IFACE | grep -Po "\\d+\\.\\d+\\.\\d+\\.\\d+") + if [ -n "${WLAN_IFACE_IP}" ]; then + echo "Got DHCP IP: $WLAN_IFACE_IP" + break + fi + echo "Waiting for $WLAN_IFACE to get an IP..." + sleep 5 +done diff --git a/bridge/get-dhcp-dns.sh b/bridge/get-dhcp-dns.sh new file mode 100755 index 0000000..c0c8790 --- /dev/null +++ b/bridge/get-dhcp-dns.sh @@ -0,0 +1,9 @@ +get_dns_servers() { + local interface="$1" + resolvectl status | awk -v iface="Link [0-9]+ \\($interface\\)" '$0 ~ iface {flag=1; next} flag && /DNS Servers/ {gsub(",", ""); print; exit}' | awk -F ': ' '{print $2}' +} + +get_dns_domain() { + local interface="$1" + resolvectl status | awk -v iface="Link [0-9]+ \\($interface\\)" '$0 ~ iface {flag=1; next} flag && /DNS Domain/ {print $3; exit}' +} diff --git a/bridge/get_client_mac_address.sh b/bridge/get_client_mac_address.sh new file mode 100755 index 0000000..2391640 --- /dev/null +++ b/bridge/get_client_mac_address.sh @@ -0,0 +1,20 @@ +function get_client_mac_address() { + # Usage: get_client_mac_address + local ETH_IFACE="$1" + local TARGET_IP="192.168.2.2" + + # Check if the interface is plugged in + if ip link show "$ETH_IFACE" | grep -q "state UP"; then + local MAC_ADDRESS=$(arp -i "$ETH_IFACE" -n | grep "$TARGET_IP" | awk '{print $3}') + + if [ -n "$MAC_ADDRESS" ]; then + echo "$MAC_ADDRESS" + else + echo "Could not find the MAC address of the connected device with IP address $TARGET_IP" + exit 1 + fi + else + echo "Interface $ETH_IFACE is not plugged in." + exit 1 + fi +} diff --git a/config.sh.example b/config/config.sh.example similarity index 59% rename from config.sh.example rename to config/config.sh.example index dfc76c3..46da6da 100644 --- a/config.sh.example +++ b/config/config.sh.example @@ -11,5 +11,9 @@ WIFI_SSID="Example-Network" WIFI_USERNAME="username" WIFI_PWD="password" +# "transparent": the bridge device clones the client's MAC address and NATs traffic to a private LAN. Only supports one bridged client. +# TODO: support bridging multiple clients connected to an ethernet hub +# BRIDGE_MODE="transparent" + # Don't prompt the user for confirmation NON_INTERACTIVE=false diff --git a/wlan2eth.service b/wlan2eth.service new file mode 100644 index 0000000..9fbee27 --- /dev/null +++ b/wlan2eth.service @@ -0,0 +1,14 @@ +# /etc/systemd/system/wlan2eth.service +[Unit] +Description=wlan2eth +Wants=basic.target +After=basic.target network.target + +[Service] +SyslogIdentifier=wlan2eth +ExecStart=/bin/bash /opt/wlan2eth/wlan2eth.sh +Restart=always +RestartSec=2 + +[Install] +WantedBy=multi-user.target diff --git a/wlan2eth.sh b/wlan2eth.sh new file mode 100755 index 0000000..21c77f0 --- /dev/null +++ b/wlan2eth.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# ============================================================================== +# Config + +SOURCE=${BASH_SOURCE[0]} +while [ -L "$SOURCE" ]; do + DIR=$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd) + SOURCE=$(readlink "$SOURCE") + [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE +done +DIR=$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd) + +if [[ -f "$DIR/config/config.sh" ]]; then + source "$DIR/config/config.sh" +else + echo "config/config.sh missing!" + exit 1 +fi + +# Must be run as root +if [ "$(id -u)" -ne 0 ]; then + echo 'This script must be run as root.' >&2 + exit 1 +fi + +# ============================================================================== + +PREV_STATUS="" + +while true; do + STATUS=$(cat /sys/class/net/$ETH_IFACE/carrier 2>/dev/null) + + if [ "$STATUS" != "$PREV_STATUS" ]; then + if [ "$STATUS" == "0" ]; then + echo -e "\n----> Interface $ETH_IFACE has been unplugged." + bash "$DIR/bridge/bridge-reset.sh" + echo -e "--> Reset complete\n" + elif [ "$STATUS" == "1" ]; then + echo -e "\n----> Interface $ETH_IFACE has been plugged in." + bash "$DIR/bridge/bridge-lan.sh" + bash "$DIR/bridge/clone-client-mac.sh" + echo -e "--> Bridge complete\n" + else + echo -e "\n----> Interface $ETH_IFACE not found, doing nothing...\n" + fi + PREV_STATUS="$STATUS" + fi + sleep 1 +done