summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-01-11 21:22:21 +0200
committerPaul Buetow <paul@buetow.org>2026-01-11 21:22:21 +0200
commita6984e1a9c59f19444bbc9013c59604e48cbf371 (patch)
tree0b50f83e08dd4217da3799eb9f81184c0ca7b463
parentd3fe29187a6bb8b78bea2791e95c3d061d9f6aec (diff)
Add roaming client support for earth (Fedora laptop) and pixel7pro (Android)
Core changes to wireguardmeshgenerator.rb: - Add roaming client detection (hosts without 'lan' or 'internet' sections) - Enable PersistentKeepalive for all roaming client peer connections - Route all traffic (0.0.0.0/0, ::/0) through VPN for roaming clients - Add DNS configuration (1.1.1.1, 8.8.8.8) for roaming clients - Handle CIDR notation in AllowedIPs without adding /32 - Support configurable SSH port per host (default 22, OpenBSD hosts use 2) YAML configuration changes: - Add earth roaming client (192.168.2.200, Fedora laptop) - Add pixel7pro roaming client (192.168.2.201, Android phone) - Configure client-only architecture via exclude_peers - Roaming clients connect only to blowfish and fishfinger gateways - LAN hosts (f0-f2, r0-r2) exclude roaming clients from peering - Add SSH port 2 for OpenBSD hosts (blowfish, fishfinger) Dependency updates: - Add 'rake' gem to Gemfile for task management - Add 'logger' gem to suppress Ruby 4.0 deprecation warnings Implementation notes: - Roaming clients have no fixed 'lan' or 'internet' section - All-traffic routing enables internet access through VPN gateways - NAT rules on OpenBSD gateways required for internet access - WireGuard does not support automatic failover between peers - Manual reconnection required if active gateway fails Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--wireguardmeshgenerator.rb52
-rw-r--r--wireguardmeshgenerator.yaml55
4 files changed, 102 insertions, 11 deletions
diff --git a/Gemfile b/Gemfile
index 11db786..77369a7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,5 +1,7 @@
source 'https://rubygems.org'
+gem 'logger'
gem 'net-scp'
gem 'net-ssh'
+gem 'rake'
gem 'yaml'
diff --git a/Gemfile.lock b/Gemfile.lock
index 17ee478..399a0ed 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,9 +1,11 @@
GEM
remote: https://rubygems.org/
specs:
+ logger (1.7.0)
net-scp (4.1.0)
net-ssh (>= 2.6.5, < 8.0.0)
net-ssh (7.3.0)
+ rake (13.3.1)
yaml (0.4.0)
PLATFORMS
@@ -11,8 +13,10 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
+ logger
net-scp
net-ssh
+ rake
yaml
BUNDLED WITH
diff --git a/wireguardmeshgenerator.rb b/wireguardmeshgenerator.rb
index ec7ad5a..365b2b9 100644
--- a/wireguardmeshgenerator.rb
+++ b/wireguardmeshgenerator.rb
@@ -70,12 +70,14 @@ PeerSnippet = Struct.new(:myself, :peer, :domain, :wgdomain,
# keepalive settings.
def to_s
keytool = KeyTool.new(myself)
+ # Check if allowed_ips already contains CIDR notation or is a special routing rule
+ allowed_ips_str = allowed_ips.include?('/') ? allowed_ips : "#{allowed_ips}/32"
<<~PEER_CONF
[Peer]
# #{myself}.#{domain} as #{myself}.#{wgdomain}
PublicKey = #{keytool.pub}
PresharedKey = #{keytool.psk(peer)}
- AllowedIPs = #{allowed_ips}/32
+ AllowedIPs = #{allowed_ips_str}
#{endpoint_str}
#{keepalive_str}
PEER_CONF
@@ -109,6 +111,7 @@ WireguardConfig = Struct.new(:myself, :hosts) do
#{address}
PrivateKey = #{keytool.priv}
ListenPort = 56709
+ #{dns}
#{peers(&:to_s).join("\n")}
CONF
@@ -143,22 +146,50 @@ WireguardConfig = Struct.new(:myself, :hosts) do
"Address = #{hosts[myself]['wg0']['ip']}"
end
+ # Generates DNS configuration for roaming clients.
+ # Roaming clients (no 'lan' or 'internet' sections) get DNS servers configured.
+ # Uses Cloudflare (1.1.1.1) and Google (8.8.8.8) public DNS for reliability.
+ def dns
+ is_roaming = !hosts[myself].key?('lan') && !hosts[myself].key?('internet')
+ return '# No DNS configured' unless is_roaming
+
+ 'DNS = 1.1.1.1, 8.8.8.8'
+ end
+
# Generates a list of peer configurations for the WireGuard mesh network.
# Excludes peers specified in the `exclude_peers` list and the current host itself.
# Determines the appropriate endpoint and keepalive settings for each peer.
+ # Roaming clients (no 'lan' or 'internet' sections) get PersistentKeepalive to all peers.
def peers
exclude = hosts[myself].fetch('exclude_peers', []).append(myself)
# Check if the current host is in the local area network (LAN).
in_lan = hosts[myself].key?('lan')
+ # Detect if current host is a roaming client (no lan or internet section).
+ # Roaming clients need PersistentKeepalive to all peers to maintain NAT traversal.
+ is_roaming = !hosts[myself].key?('lan') && !hosts[myself].key?('internet')
hosts.reject { exclude.include?(_1) }.map do |peer, data|
- # Determine if the peer is in the LAN.
- peer_in_lan = data.key?('lan')
- reach = data[peer_in_lan ? 'lan' : 'internet']
- endpoint = peer_in_lan == in_lan || !peer_in_lan ? reach['ip'] : :behind_nat
- # Determine if keepalive is needed (only for LAN-to-internet connections).
- keepalive = in_lan && !peer_in_lan
+ # Check if peer is roaming (no lan or internet section).
+ # Roaming peers are always behind NAT and cannot be reached directly.
+ peer_is_roaming = !data.key?('lan') && !data.key?('internet')
+
+ if peer_is_roaming
+ # Roaming peer is always behind NAT, use wg0 domain for identification
+ reach = data['wg0']
+ endpoint = :behind_nat
+ else
+ # Regular peer with lan or internet section
+ peer_in_lan = data.key?('lan')
+ reach = data[peer_in_lan ? 'lan' : 'internet']
+ endpoint = peer_in_lan == in_lan || !peer_in_lan ? reach['ip'] : :behind_nat
+ end
+
+ # Set keepalive: LAN hosts connecting to internet hosts, OR roaming clients connecting to anyone.
+ keepalive = is_roaming || (in_lan && !peer_in_lan)
+ # For roaming clients, route all traffic through VPN (0.0.0.0/0).
+ # For regular mesh peers, only route their specific IP.
+ allowed_ips = is_roaming ? '0.0.0.0/0, ::/0' : data['wg0']['ip']
PeerSnippet.new(peer, myself, reach['domain'], data['wg0']['domain'],
- data['wg0']['ip'], endpoint, keepalive)
+ allowed_ips, endpoint, keepalive)
end
end
end
@@ -174,6 +205,7 @@ InstallConfig = Struct.new(:myself, :hosts) do
domain = data.dig('lan', 'domain') || data.dig('internet', 'domain')
@fqdn = "#{myself}.#{domain}"
@ssh_user = data['ssh']['user']
+ @ssh_port = data.dig('ssh', 'port') || 22
@sudo_cmd = data['ssh']['sudo_cmd']
@reload_cmd = data['ssh']['reload_cmd']
@conf_dir = data['ssh']['conf_dir']
@@ -215,7 +247,7 @@ InstallConfig = Struct.new(:myself, :hosts) do
def scp(src, dst = '.')
puts "Uploading #{src} to #{@fqdn}:#{dst}"
raise "Upload #{src} to #{@fqdn}:#{dst} failed" unless
- Net::SCP.upload!(@fqdn, @ssh_user, src, dst)
+ Net::SCP.upload!(@fqdn, @ssh_user, src, dst, ssh: { port: @ssh_port })
end
# Executes a shell command on the remote host using SSH.
@@ -227,7 +259,7 @@ InstallConfig = Struct.new(:myself, :hosts) do
#{cmd}
rm $0
SH
- Net::SSH.start(@fqdn, @ssh_user) do |ssh|
+ Net::SSH.start(@fqdn, @ssh_user, port: @ssh_port) do |ssh|
output = ssh.exec!('sh cmd.sh')
raise output unless output.exitstatus.zero?
diff --git a/wireguardmeshgenerator.yaml b/wireguardmeshgenerator.yaml
index d984ca5..020854f 100644
--- a/wireguardmeshgenerator.yaml
+++ b/wireguardmeshgenerator.yaml
@@ -13,6 +13,9 @@ hosts:
wg0:
domain: 'wg0.wan.buetow.org'
ip: '192.168.2.130'
+ exclude_peers:
+ - earth
+ - pixel7pro
f1:
os: FreeBSD
ssh:
@@ -26,6 +29,9 @@ hosts:
wg0:
domain: 'wg0.wan.buetow.org'
ip: '192.168.2.131'
+ exclude_peers:
+ - earth
+ - pixel7pro
f2:
os: FreeBSD
ssh:
@@ -39,6 +45,9 @@ hosts:
wg0:
domain: 'wg0.wan.buetow.org'
ip: '192.168.2.132'
+ exclude_peers:
+ - earth
+ - pixel7pro
r0:
os: Linux
ssh:
@@ -52,6 +61,9 @@ hosts:
wg0:
domain: 'wg0.wan.buetow.org'
ip: '192.168.2.120'
+ exclude_peers:
+ - earth
+ - pixel7pro
r1:
os: Linux
ssh:
@@ -65,6 +77,9 @@ hosts:
wg0:
domain: 'wg0.wan.buetow.org'
ip: '192.168.2.121'
+ exclude_peers:
+ - earth
+ - pixel7pro
r2:
os: Linux
ssh:
@@ -78,10 +93,14 @@ hosts:
wg0:
domain: 'wg0.wan.buetow.org'
ip: '192.168.2.122'
+ exclude_peers:
+ - earth
+ - pixel7pro
blowfish:
os: OpenBSD
ssh:
user: rex
+ port: 2
conf_dir: /etc/wireguard
sudo_cmd: doas
reload_cmd: sh /etc/netstart wg0
@@ -95,6 +114,7 @@ hosts:
os: OpenBSD
ssh:
user: rex
+ port: 2
conf_dir: /etc/wireguard
sudo_cmd: doas
reload_cmd: sh /etc/netstart wg0
@@ -102,4 +122,37 @@ hosts:
domain: 'buetow.org'
ip: '46.23.94.99'
wg0:
- domain: 'wg0.wan.buetow.org' ip: '192.168.2.111'
+ domain: 'wg0.wan.buetow.org'
+ ip: '192.168.2.111'
+ earth:
+ os: Linux
+ wg0:
+ domain: 'wg0.wan.buetow.org'
+ ip: '192.168.2.200'
+ exclude_peers:
+ - f0
+ - f1
+ - f2
+ - r0
+ - r1
+ - r2
+ - pixel7pro
+ # Note: No 'lan' or 'internet' section = roaming client
+ # Note: No 'ssh' section = manual installation
+ # Note: Only connects to blowfish and fishfinger (internet gateways)
+ pixel7pro:
+ os: Android
+ wg0:
+ domain: 'wg0.wan.buetow.org'
+ ip: '192.168.2.201'
+ exclude_peers:
+ - f0
+ - f1
+ - f2
+ - r0
+ - r1
+ - r2
+ - earth
+ # Note: No 'lan' or 'internet' section = roaming client
+ # Note: No 'ssh' section = manual installation
+ # Note: Only connects to blowfish and fishfinger (internet gateways)