diff options
| author | Paul Buetow <paul@buetow.org> | 2026-01-11 21:22:21 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-01-11 21:22:21 +0200 |
| commit | a6984e1a9c59f19444bbc9013c59604e48cbf371 (patch) | |
| tree | 0b50f83e08dd4217da3799eb9f81184c0ca7b463 /wireguardmeshgenerator.rb | |
| parent | d3fe29187a6bb8b78bea2791e95c3d061d9f6aec (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>
Diffstat (limited to 'wireguardmeshgenerator.rb')
| -rw-r--r-- | wireguardmeshgenerator.rb | 52 |
1 files changed, 42 insertions, 10 deletions
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? |
