|
#!/usr/bin/env bash |
|
# |
|
# setup_vpn_gateway.sh - Configure this Raspberry Pi as a VPN gateway. |
|
# |
|
# Clients that set their default gateway to 192.168.0.6 (eth1) will have all |
|
# traffic routed through the Windscribe VPN tunnel (tun0). The Pi's own |
|
# traffic on eth0 (192.168.0.5) continues to route normally via 192.168.0.1. |
|
# |
|
# Usage: |
|
# 1. Edit VPN_USER and VPN_PASS below |
|
# 2. sudo bash setup_vpn_gateway.sh |
|
# 3. sudo bash setup_vpn_gateway.sh --uninstall (to reverse everything) |
|
|
|
set -euo pipefail |
|
|
|
# - - User-editable credentials - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
VPN_USER="" |
|
VPN_PASS="" |
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
OVPN_SOURCE="/home/rpi/openvpn_gateway/Windscribe-StaticIP.ovpn" |
|
VPN_REMOTE="<insert your VPN static ip>" |
|
ETH0_IP="192.168.0.5" |
|
ETH1_IP="192.168.0.6" |
|
SUBNET="192.168.0.0/24" |
|
LAN_GW="192.168.0.1" |
|
VPN_STATIC_PORT="12345" # Windscribe static port |
|
LOCAL_DEST="192.168.0.35:12345" # LAN IP:port to forward to |
|
|
|
if [[ $EUID -ne 0 ]]; then |
|
echo "Error: Run this script with sudo." >&2 |
|
exit 1 |
|
fi |
|
|
|
# - - Uninstall mode - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
if [[ "${1:-}" == "--uninstall" ]]; then |
|
echo "=== Uninstalling VPN gateway ===" |
|
|
|
# Stop and disable OpenVPN |
|
systemctl stop openvpn-client@windscribe.service 2>/dev/null || true |
|
systemctl disable openvpn-client@windscribe.service 2>/dev/null || true |
|
|
|
# Remove systemd drop-in override |
|
rm -rf /etc/systemd/system/openvpn-client@windscribe.service.d |
|
systemctl daemon-reload |
|
|
|
# Remove OpenVPN config files |
|
rm -f /etc/openvpn/client/windscribe.conf |
|
rm -f /etc/openvpn/auth.txt |
|
rm -f /etc/openvpn/client/tun-up.sh |
|
rm -f /etc/openvpn/client/tun-down.sh |
|
|
|
# Remove NM dispatcher script |
|
rm -f /etc/NetworkManager/dispatcher.d/10-eth1-policy-route |
|
|
|
# Remove sysctl config |
|
rm -f /etc/sysctl.d/50-vpn-gateway.conf |
|
|
|
# Remove routing table entries from rt_tables |
|
sed -i '/eth1rt/d' /usr/share/iproute2/rt_tables |
|
sed -i '/vpntunnel/d' /usr/share/iproute2/rt_tables |
|
|
|
# Flush ip rules and routing tables |
|
ip rule del from $ETH1_IP table 100 2>/dev/null || true |
|
ip rule del fwmark 0x1 table 200 2>/dev/null || true |
|
ip route flush table 200 2>/dev/null || true |
|
ip route flush table 100 2>/dev/null || true |
|
|
|
# Reset iptables |
|
iptables -F |
|
iptables -t nat -F |
|
iptables -t mangle -F |
|
iptables -X 2>/dev/null || true |
|
iptables -P FORWARD ACCEPT |
|
netfilter-persistent save 2>/dev/null || true |
|
|
|
# Disable IP forwarding |
|
sysctl -qw net.ipv4.ip_forward=0 |
|
|
|
# Remove eth1 NM connection |
|
nmcli con delete eth1-static 2>/dev/null || true |
|
|
|
echo "" |
|
echo "Remove port ${LOCAL_DEST##*:} from target machine (${LOCAL_DEST%%:*}) firewall:" |
|
echo " sudo iptables -D INPUT_direct -p tcp --dport ${LOCAL_DEST##*:} -j ACCEPT" |
|
echo " Or using firewalld:" |
|
echo " sudo firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT_direct 0 -p tcp --dport ${LOCAL_DEST##*:} -j ACCEPT" |
|
echo " sudo firewall-cmd --reload" |
|
echo "" |
|
echo "=== Uninstall complete. All VPN gateway config removed. ===" |
|
exit 0 |
|
fi |
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
if [[ "$VPN_USER" == "your_windscribe_username" ]]; then |
|
echo "Error: Edit VPN_USER and VPN_PASS in this script before running." >&2 |
|
exit 1 |
|
fi |
|
|
|
if [[ ! -f "$OVPN_SOURCE" ]]; then |
|
echo "Error: $OVPN_SOURCE not found." >&2 |
|
exit 1 |
|
fi |
|
|
|
echo "=== 1/6 Configuring eth1 via nmcli ===" |
|
|
|
# Remove existing connection for eth1 if any |
|
existing=$(nmcli -t -f NAME,DEVICE con show 2>/dev/null | grep ':eth1$' | cut -d: -f1 || true) |
|
if [[ -n "$existing" ]]; then |
|
while read -r name; do |
|
nmcli con delete "$name" 2>/dev/null || true |
|
done <<< "$existing" |
|
fi |
|
|
|
nmcli con add type ethernet con-name eth1-static ifname eth1 \ |
|
ipv4.method manual \ |
|
ipv4.addresses "${ETH1_IP}/24" \ |
|
ipv4.never-default yes \ |
|
ipv6.method disabled \ |
|
connection.autoconnect yes |
|
|
|
nmcli con up eth1-static || echo "Warning: eth1 may not be plugged in yet; will activate on connect." |
|
|
|
echo "=== 2/6 Policy-based routing (dual-NIC same-subnet fix) ===" |
|
|
|
# Add routing tables if not already present |
|
if ! grep -q 'eth1rt' /usr/share/iproute2/rt_tables; then |
|
echo "100 eth1rt" >> /usr/share/iproute2/rt_tables |
|
fi |
|
if ! grep -q 'vpntunnel' /usr/share/iproute2/rt_tables; then |
|
echo "200 vpntunnel" >> /usr/share/iproute2/rt_tables |
|
fi |
|
|
|
# NM dispatcher script - re-applies policy routing whenever eth1 comes up |
|
cat > /etc/NetworkManager/dispatcher.d/10-eth1-policy-route << DISPATCHER |
|
#!/usr/bin/env bash |
|
IFACE="\$1" |
|
ACTION="\$2" |
|
|
|
if [[ "\$IFACE" == "eth1" && "\$ACTION" == "up" ]]; then |
|
# Policy rule: traffic originating from eth1's IP uses table 100 |
|
ip rule del from $ETH1_IP table 100 2>/dev/null || true |
|
ip rule add from $ETH1_IP table 100 |
|
|
|
# Subnet route in table 100 so ARP/local replies go out eth1 |
|
ip route replace $SUBNET dev eth1 src $ETH1_IP table 100 |
|
|
|
# ARP / reverse-path filtering for both interfaces |
|
sysctl -qw net.ipv4.conf.eth0.arp_filter=1 |
|
sysctl -qw net.ipv4.conf.eth1.arp_filter=1 |
|
sysctl -qw net.ipv4.conf.eth0.rp_filter=2 |
|
sysctl -qw net.ipv4.conf.eth1.rp_filter=2 |
|
fi |
|
DISPATCHER |
|
chmod 755 /etc/NetworkManager/dispatcher.d/10-eth1-policy-route |
|
|
|
# Persist sysctl settings (also enables IP forwarding) |
|
cat > /etc/sysctl.d/50-vpn-gateway.conf << 'SYSCTL' |
|
net.ipv4.ip_forward = 1 |
|
net.ipv4.conf.eth0.arp_filter = 1 |
|
net.ipv4.conf.eth1.arp_filter = 1 |
|
net.ipv4.conf.eth0.rp_filter = 2 |
|
net.ipv4.conf.eth1.rp_filter = 2 |
|
SYSCTL |
|
|
|
echo "=== 3/6 Configuring OpenVPN client ===" |
|
|
|
# Credentials file |
|
cat > /etc/openvpn/auth.txt << AUTHEOF |
|
${VPN_USER} |
|
${VPN_PASS} |
|
AUTHEOF |
|
chmod 600 /etc/openvpn/auth.txt |
|
|
|
# Copy .ovpn and append customizations |
|
cp "$OVPN_SOURCE" /etc/openvpn/client/windscribe.conf |
|
cat >> /etc/openvpn/client/windscribe.conf << VPNCONF |
|
|
|
# --- Gateway customizations --- |
|
auth-user-pass /etc/openvpn/auth.txt |
|
route-nopull |
|
route ${VPN_REMOTE} 255.255.255.255 ${LAN_GW} |
|
script-security 2 |
|
up /etc/openvpn/client/tun-up.sh |
|
down /etc/openvpn/client/tun-down.sh |
|
VPNCONF |
|
|
|
# tun-up script - adds VPN default route in table 200 for marked packets |
|
cat > /etc/openvpn/client/tun-up.sh << 'TUNUP' |
|
#!/usr/bin/env bash |
|
# OpenVPN passes: $1=tun_dev, env has route_vpn_gateway or ifconfig_remote |
|
DEV="$1" |
|
# Prefer route_vpn_gateway (pushed by server), fall back to ifconfig_remote |
|
GW="${route_vpn_gateway:-${ifconfig_remote:-}}" |
|
if [[ -n "$GW" ]]; then |
|
ip route replace default via "$GW" dev "$DEV" table 200 |
|
else |
|
ip route replace default dev "$DEV" table 200 |
|
fi |
|
ip rule del fwmark 0x1 table 200 2>/dev/null || true |
|
ip rule add fwmark 0x1 table 200 |
|
TUNUP |
|
chmod 755 /etc/openvpn/client/tun-up.sh |
|
|
|
# tun-down script - cleans up table 200 |
|
cat > /etc/openvpn/client/tun-down.sh << 'TUNDOWN' |
|
#!/usr/bin/env bash |
|
ip route flush table 200 |
|
ip rule del fwmark 0x1 table 200 2>/dev/null || true |
|
TUNDOWN |
|
chmod 755 /etc/openvpn/client/tun-down.sh |
|
|
|
echo "=== 4/6 Enabling OpenVPN service (with restart on failure) ===" |
|
|
|
systemctl enable openvpn-client@windscribe.service |
|
|
|
# Add drop-in override for automatic restart on failure |
|
mkdir -p /etc/systemd/system/openvpn-client@windscribe.service.d |
|
cat > /etc/systemd/system/openvpn-client@windscribe.service.d/restart.conf << 'DROPIN' |
|
[Service] |
|
Restart=on-failure |
|
RestartSec=5 |
|
DROPIN |
|
systemctl daemon-reload |
|
|
|
echo "=== 5/6 Configuring iptables (NAT + kill switch) ===" |
|
|
|
# Ensure iptables-persistent is installed |
|
if ! dpkg -s iptables-persistent &>/dev/null; then |
|
DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent |
|
fi |
|
|
|
# Flush existing rules for a clean slate |
|
iptables -F |
|
iptables -t nat -F |
|
iptables -t mangle -F |
|
iptables -X 2>/dev/null || true |
|
|
|
# Default policies |
|
iptables -P INPUT ACCEPT |
|
iptables -P OUTPUT ACCEPT |
|
iptables -P FORWARD DROP # Kill switch: no forwarding unless tun0 is up |
|
|
|
# Mangle: mark client subnet traffic for VPN routing (skip Pi's own IPs) |
|
# Both NICs share the 192.168.0.0/24 subnet, so clients may arrive on either interface |
|
iptables -t mangle -A PREROUTING -s $ETH0_IP -j RETURN |
|
iptables -t mangle -A PREROUTING -s $ETH1_IP -j RETURN |
|
iptables -t mangle -A PREROUTING -s $SUBNET -j MARK --set-mark 0x1 |
|
|
|
# NAT: masquerade client traffic leaving via tun0 |
|
iptables -t nat -A POSTROUTING -s $SUBNET -o tun0 -j MASQUERADE |
|
|
|
# FORWARD: allow marked client traffic -> tun0 |
|
iptables -A FORWARD -m mark --mark 0x1 -o tun0 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT |
|
# Port forwarding: DNAT incoming VPN static port to LAN client |
|
iptables -t nat -A PREROUTING -i tun0 -p tcp --dport "$VPN_STATIC_PORT" -j DNAT --to-destination "$LOCAL_DEST" |
|
iptables -t nat -A PREROUTING -i tun0 -p udp --dport "$VPN_STATIC_PORT" -j DNAT --to-destination "$LOCAL_DEST" |
|
# Allow the DNAT'd forwarded traffic through |
|
iptables -A FORWARD -i tun0 -p tcp -d "${LOCAL_DEST%%:*}" --dport "${LOCAL_DEST##*:}" -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT |
|
iptables -A FORWARD -i tun0 -p udp -d "${LOCAL_DEST%%:*}" --dport "${LOCAL_DEST##*:}" -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT |
|
# Hairpin NAT: SNAT DNAT'd traffic so replies return through the Pi |
|
iptables -t nat -A POSTROUTING -d "${LOCAL_DEST%%:*}" -p tcp --dport "${LOCAL_DEST##*:}" -j MASQUERADE |
|
iptables -t nat -A POSTROUTING -d "${LOCAL_DEST%%:*}" -p udp --dport "${LOCAL_DEST##*:}" -j MASQUERADE |
|
|
|
# FORWARD: allow return traffic from tun0 |
|
iptables -A FORWARD -i tun0 -m state --state ESTABLISHED,RELATED -j ACCEPT |
|
|
|
# Save rules |
|
netfilter-persistent save |
|
|
|
echo "=== 6/6 Enabling IP forwarding ===" |
|
|
|
sysctl -p /etc/sysctl.d/50-vpn-gateway.conf |
|
|
|
echo "" |
|
echo "=== Setup complete - starting OpenVPN ===" |
|
echo "" |
|
|
|
systemctl start openvpn-client@windscribe.service |
|
|
|
echo "Verify with:" |
|
echo " sudo systemctl status openvpn-client@windscribe" |
|
echo " ip addr show tun0" |
|
echo " ip route show table 200" |
|
echo " From a client (gateway=$ETH1_IP): curl https://ifconfig.me" |
|
echo " Kill switch test: sudo systemctl stop openvpn-client@windscribe" |
|
echo " -> client traffic should stop" |
|
echo " Pi's own traffic: curl https://ifconfig.me -> should show home IP" |
|
echo "Port forwarding target machine (${LOCAL_DEST%%:*}) firewall setup:" |
|
echo " sudo iptables -I INPUT_direct 15 -p tcp --dport ${LOCAL_DEST##*:} -j ACCEPT" |
|
echo " Or using firewalld (persistent):" |
|
echo " sudo firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT_direct 0 -p tcp --dport ${LOCAL_DEST##*:} -j ACCEPT" |
|
echo " sudo firewall-cmd --reload" |
|
echo "" |
|
echo "To uninstall: sudo bash $0 --uninstall" |