summaryrefslogtreecommitdiff
path: root/wireguardmeshgenerator.rb
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 /wireguardmeshgenerator.rb
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>
Diffstat (limited to 'wireguardmeshgenerator.rb')
-rw-r--r--wireguardmeshgenerator.rb52
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?