messhy 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +52 -0
- data/lib/messhy/cli.rb +10 -5
- data/lib/messhy/configuration.rb +52 -0
- data/lib/messhy/dns_manager.rb +165 -0
- data/lib/messhy/health_checker.rb +89 -3
- data/lib/messhy/installer.rb +5 -0
- data/lib/messhy/ssh_executor.rb +13 -0
- data/lib/messhy/version.rb +1 -1
- data/lib/messhy.rb +1 -0
- data/lib/tasks/messhy.rake +5 -5
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2789748ffdef7b5d7d46a0c58eed766cdfeb3f0a7f9acd69ffcc6d99def48c6d
|
|
4
|
+
data.tar.gz: 640f81d5ad62ca4d736fa5643080c91d68a616a23c18feba060fd7c5c91a8031
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8bb76d383440f0ee413e92524d5dbc275c70f4765d9f971da21ba272753a30b7d479e7ca199a4dad13d46174a456c59af3056fe4887cb6ac1c2821c463bb1e5f
|
|
7
|
+
data.tar.gz: a142538a31aacffc7dd0a80854ae09c9c98bf8430f82200e58ae8bf3c7ad4d17b6a642b17374baf23752bd428a6874084ccdb1772fa45168d5f99c2350e95ded
|
data/README.md
CHANGED
|
@@ -72,6 +72,42 @@ messhy setup --environment=production
|
|
|
72
72
|
messhy status
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
When mesh DNS is enabled, `messhy status` also prints DNS server health and record counts.
|
|
76
|
+
|
|
77
|
+
## Mesh DNS (optional)
|
|
78
|
+
|
|
79
|
+
Messhy can set up a lightweight internal DNS (dnsmasq) for the mesh so you can
|
|
80
|
+
use stable hostnames instead of raw IPs. It installs dnsmasq on designated nodes
|
|
81
|
+
and configures all mesh nodes to resolve a private domain over `wg0`.
|
|
82
|
+
|
|
83
|
+
Example config:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
production:
|
|
87
|
+
<<: *shared
|
|
88
|
+
dns:
|
|
89
|
+
enabled: true
|
|
90
|
+
provider: dnsmasq
|
|
91
|
+
domain: mesh.internal
|
|
92
|
+
interface: wg0
|
|
93
|
+
servers:
|
|
94
|
+
- app-us-1
|
|
95
|
+
- app-eu-1
|
|
96
|
+
auto_records: true
|
|
97
|
+
records:
|
|
98
|
+
db-primary.mesh.internal:
|
|
99
|
+
- db-primary
|
|
100
|
+
- db-standby-1
|
|
101
|
+
db-replica.mesh.internal:
|
|
102
|
+
- db-standby-1
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Apply DNS without touching WireGuard:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
messhy dns --environment=production
|
|
109
|
+
```
|
|
110
|
+
|
|
75
111
|
## Secret Management
|
|
76
112
|
|
|
77
113
|
`messhy setup` stores generated WireGuard key pairs inside `.secrets/wireguard/*.yml` with `0600` permissions. Each node gets its own YAML file (`.secrets/wireguard/<node>.yml`) and all peer pre‑shared keys live in `.secrets/wireguard/psks.yml`. The directory is gitignored by default, and the Rails generator ensures the ignore rules are present in your application. After provisioning, copy the YAML files into 1Password (or another vault) and remove them from disk if you do not want long‑lived local copies.
|
|
@@ -187,6 +223,22 @@ See `config/mesh.example.yml` for a complete example.
|
|
|
187
223
|
- `mtu`: MTU size (default: `1280` for reliability)
|
|
188
224
|
- `listen_port`: WireGuard port (default: `51820`)
|
|
189
225
|
- `keepalive`: Keepalive interval in seconds (default: `25`)
|
|
226
|
+
- `dns`: Optional mesh DNS configuration (see below)
|
|
227
|
+
|
|
228
|
+
### DNS Options
|
|
229
|
+
|
|
230
|
+
When `dns.enabled: true`, messhy installs dnsmasq on the specified `dns.servers`
|
|
231
|
+
and configures all mesh nodes to resolve the `dns.domain` over the WireGuard
|
|
232
|
+
interface.
|
|
233
|
+
|
|
234
|
+
- `dns.enabled`: Enable mesh DNS
|
|
235
|
+
- `dns.provider`: `dnsmasq` (only provider today)
|
|
236
|
+
- `dns.domain`: Internal DNS domain (default: `mesh`)
|
|
237
|
+
- `dns.interface`: WireGuard interface (default: `wg0`)
|
|
238
|
+
- `dns.servers`: Node names that will run dnsmasq
|
|
239
|
+
- `dns.auto_records`: Auto-create `<node>.<domain>` for every node (default: `true`)
|
|
240
|
+
- `dns.records`: Extra records (values can be IPs or node names)
|
|
241
|
+
- `dns.ttl`: Local DNS TTL seconds (default: `30`)
|
|
190
242
|
|
|
191
243
|
### Node Configuration
|
|
192
244
|
|
data/lib/messhy/cli.rb
CHANGED
|
@@ -26,6 +26,16 @@ module Messhy
|
|
|
26
26
|
handle_ssh_error(e, config)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
desc 'dns', 'Setup mesh DNS (dnsmasq)'
|
|
30
|
+
option :dry_run, type: :boolean, default: false
|
|
31
|
+
option :skip_node, type: :string
|
|
32
|
+
def dns
|
|
33
|
+
config = load_config
|
|
34
|
+
DnsManager.new(config, dry_run: options[:dry_run], skip: options[:skip_node]).setup
|
|
35
|
+
rescue SSHKit::Runner::ExecuteError => e
|
|
36
|
+
handle_ssh_error(e, config)
|
|
37
|
+
end
|
|
38
|
+
|
|
29
39
|
desc 'keygen', 'Generate WireGuard keys without deploying configs'
|
|
30
40
|
option :skip_node, type: :string
|
|
31
41
|
def keygen
|
|
@@ -45,11 +55,6 @@ module Messhy
|
|
|
45
55
|
handle_ssh_error(e, config)
|
|
46
56
|
end
|
|
47
57
|
|
|
48
|
-
desc 'health', 'Alias for status'
|
|
49
|
-
def health
|
|
50
|
-
status
|
|
51
|
-
end
|
|
52
|
-
|
|
53
58
|
desc 'ping NODE', 'Ping a specific node'
|
|
54
59
|
def ping(node)
|
|
55
60
|
config = load_config
|
data/lib/messhy/configuration.rb
CHANGED
|
@@ -7,6 +7,7 @@ module Messhy
|
|
|
7
7
|
attr_reader :environment,
|
|
8
8
|
:network,
|
|
9
9
|
:nodes,
|
|
10
|
+
:dns,
|
|
10
11
|
:user,
|
|
11
12
|
:ssh_key,
|
|
12
13
|
:mtu,
|
|
@@ -20,6 +21,7 @@ module Messhy
|
|
|
20
21
|
|
|
21
22
|
@network = env_config['network'] || '10.8.0.0/24'
|
|
22
23
|
@nodes = env_config['nodes'] || {}
|
|
24
|
+
@dns = env_config['dns'] || {}
|
|
23
25
|
@user = env_config['user'] || 'ubuntu'
|
|
24
26
|
@ssh_key = File.expand_path(env_config['ssh_key'] || '~/.ssh/id_rsa')
|
|
25
27
|
@mtu = env_config['mtu'] || 1280
|
|
@@ -68,6 +70,16 @@ module Messhy
|
|
|
68
70
|
raise Error, "Node #{name} missing 'private_ip'" unless config['private_ip']
|
|
69
71
|
end
|
|
70
72
|
|
|
73
|
+
if dns_enabled?
|
|
74
|
+
raise Error, 'DNS domain is required when dns is enabled' if dns_domain.to_s.strip.empty?
|
|
75
|
+
raise Error, 'DNS servers are required when dns is enabled' if dns_server_nodes.empty?
|
|
76
|
+
raise Error, "Unsupported DNS provider: #{dns_provider}" unless %w[dnsmasq].include?(dns_provider)
|
|
77
|
+
|
|
78
|
+
dns_server_nodes.each do |name|
|
|
79
|
+
raise Error, "DNS server node not found: #{name}" unless node_config(name)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
71
83
|
true
|
|
72
84
|
end
|
|
73
85
|
|
|
@@ -83,5 +95,45 @@ module Messhy
|
|
|
83
95
|
:always
|
|
84
96
|
end
|
|
85
97
|
end
|
|
98
|
+
|
|
99
|
+
def dns_enabled?
|
|
100
|
+
return false if @dns.nil? || @dns.empty?
|
|
101
|
+
|
|
102
|
+
@dns.key?('enabled') ? @dns['enabled'] == true : true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def dns_provider
|
|
106
|
+
value = @dns['provider'] || 'dnsmasq'
|
|
107
|
+
value.to_s.strip
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def dns_domain
|
|
111
|
+
value = @dns['domain'] || 'mesh'
|
|
112
|
+
value.to_s.strip
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def dns_interface
|
|
116
|
+
value = @dns['interface'] || 'wg0'
|
|
117
|
+
value.to_s.strip
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def dns_ttl
|
|
121
|
+
value = @dns['ttl'] || 30
|
|
122
|
+
value.to_i
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def dns_server_nodes
|
|
126
|
+
Array(@dns['servers']).map(&:to_s).reject(&:empty?)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def dns_records
|
|
130
|
+
@dns['records'] || {}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def dns_auto_records?
|
|
134
|
+
return true unless @dns.key?('auto_records')
|
|
135
|
+
|
|
136
|
+
@dns['auto_records'] == true
|
|
137
|
+
end
|
|
86
138
|
end
|
|
87
139
|
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
|
|
5
|
+
module Messhy
|
|
6
|
+
class DnsManager
|
|
7
|
+
attr_reader :config, :ssh_executor, :dry_run
|
|
8
|
+
|
|
9
|
+
def initialize(config, ssh_executor: SSHExecutor.new(config), dry_run: false, skip: nil)
|
|
10
|
+
@config = config
|
|
11
|
+
@ssh_executor = ssh_executor
|
|
12
|
+
@dry_run = dry_run
|
|
13
|
+
@skip = skip
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def setup
|
|
17
|
+
return unless config.dns_enabled?
|
|
18
|
+
|
|
19
|
+
case config.dns_provider
|
|
20
|
+
when 'dnsmasq'
|
|
21
|
+
setup_dnsmasq
|
|
22
|
+
else
|
|
23
|
+
raise Error, "Unsupported DNS provider: #{config.dns_provider}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def setup_dnsmasq
|
|
30
|
+
domain = config.dns_domain
|
|
31
|
+
interface = config.dns_interface
|
|
32
|
+
ttl = config.dns_ttl
|
|
33
|
+
server_nodes = config.dns_server_nodes
|
|
34
|
+
server_ips = server_nodes.map { |name| config.node_config(name)['private_ip'] }
|
|
35
|
+
|
|
36
|
+
records = build_records(domain)
|
|
37
|
+
|
|
38
|
+
server_nodes.each do |node|
|
|
39
|
+
next if @skip && node == @skip
|
|
40
|
+
|
|
41
|
+
server_ip = config.node_config(node)['private_ip']
|
|
42
|
+
conf = build_dnsmasq_conf(domain, interface, server_ip, records, ttl)
|
|
43
|
+
|
|
44
|
+
if dry_run
|
|
45
|
+
puts "[DRY RUN] Would install dnsmasq and write /etc/dnsmasq.d/messhy.conf on #{node}"
|
|
46
|
+
next
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
ssh_executor.execute_on_node(node) do
|
|
50
|
+
execute :sudo, 'apt-get', 'update', '-qq'
|
|
51
|
+
execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq', 'dnsmasq'
|
|
52
|
+
upload! StringIO.new(conf), '/tmp/messhy-dns.conf'
|
|
53
|
+
execute :sudo, 'mv', '/tmp/messhy-dns.conf', '/etc/dnsmasq.d/messhy.conf'
|
|
54
|
+
execute :sudo, 'chown', 'root:root', '/etc/dnsmasq.d/messhy.conf'
|
|
55
|
+
execute :sudo, 'chmod', '644', '/etc/dnsmasq.d/messhy.conf'
|
|
56
|
+
execute :sudo, 'systemctl', 'enable', 'dnsmasq'
|
|
57
|
+
execute :sudo, 'systemctl', 'restart', 'dnsmasq'
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
config.each_node do |node_name, _|
|
|
62
|
+
next if @skip && node_name == @skip
|
|
63
|
+
|
|
64
|
+
if dry_run
|
|
65
|
+
puts "[DRY RUN] Would configure DNS on #{node_name} (#{interface}) for #{domain} -> #{server_ips.join(', ')}"
|
|
66
|
+
next
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
configure_client_dns(node_name, interface, domain, server_ips)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def configure_client_dns(node_name, interface, domain, server_ips)
|
|
74
|
+
ssh_executor.execute_on_node(node_name) do
|
|
75
|
+
if test('which', 'resolvectl', raise_on_error: false)
|
|
76
|
+
execute :sudo, 'systemctl', 'enable', '--now', 'systemd-resolved', raise_on_error: false
|
|
77
|
+
execute :sudo, 'resolvectl', 'dns', interface, *server_ips
|
|
78
|
+
execute :sudo, 'resolvectl', 'domain', interface, "~#{domain}"
|
|
79
|
+
execute :sudo, 'resolvectl', 'flush-caches', raise_on_error: false
|
|
80
|
+
elsif test('[ -d /etc/resolvconf/resolv.conf.d ]', raise_on_error: false)
|
|
81
|
+
head_path = '/etc/resolvconf/resolv.conf.d/head'
|
|
82
|
+
content = server_ips.map { |ip| "nameserver #{ip}" }.join("\n") + "\n"
|
|
83
|
+
upload! StringIO.new(content), '/tmp/messhy-resolv.conf'
|
|
84
|
+
execute :sudo, 'mv', '/tmp/messhy-resolv.conf', head_path
|
|
85
|
+
execute :sudo, 'chmod', '644', head_path
|
|
86
|
+
execute :sudo, 'resolvconf', '-u'
|
|
87
|
+
else
|
|
88
|
+
info 'resolvectl not found; skipping DNS config'
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_records(domain)
|
|
94
|
+
records = {}
|
|
95
|
+
|
|
96
|
+
if config.dns_auto_records?
|
|
97
|
+
config.each_node do |name, node_config|
|
|
98
|
+
hostname = "#{sanitize_dns_label(name)}.#{domain}"
|
|
99
|
+
records[hostname] ||= []
|
|
100
|
+
records[hostname] << node_config['private_ip']
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
config.dns_records.each do |hostname, targets|
|
|
105
|
+
fqdn = normalize_hostname(hostname, domain)
|
|
106
|
+
Array(targets).each do |target|
|
|
107
|
+
ip = resolve_target(target)
|
|
108
|
+
next if ip.to_s.strip.empty?
|
|
109
|
+
|
|
110
|
+
records[fqdn] ||= []
|
|
111
|
+
records[fqdn] << ip
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
records.transform_values { |ips| ips.uniq }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def resolve_target(target)
|
|
119
|
+
return '' if target.nil?
|
|
120
|
+
|
|
121
|
+
node = config.node_config(target.to_s)
|
|
122
|
+
return node['private_ip'] if node
|
|
123
|
+
|
|
124
|
+
target.to_s
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def normalize_hostname(name, domain)
|
|
128
|
+
value = name.to_s.strip
|
|
129
|
+
return '' if value.empty?
|
|
130
|
+
|
|
131
|
+
return value if value.include?('.')
|
|
132
|
+
|
|
133
|
+
"#{sanitize_dns_label(value)}.#{domain}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def sanitize_dns_label(value)
|
|
137
|
+
value.to_s.downcase.gsub(/[^a-z0-9-]/, '-')
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def build_dnsmasq_conf(domain, interface, server_ip, records, ttl)
|
|
141
|
+
lines = []
|
|
142
|
+
lines << '# Managed by messhy'
|
|
143
|
+
lines << 'domain-needed'
|
|
144
|
+
lines << 'bogus-priv'
|
|
145
|
+
lines << "local=/#{domain}/"
|
|
146
|
+
lines << "domain=#{domain}"
|
|
147
|
+
lines << 'expand-hosts'
|
|
148
|
+
lines << 'cache-size=1000'
|
|
149
|
+
lines << "local-ttl=#{ttl}"
|
|
150
|
+
lines << "interface=#{interface}"
|
|
151
|
+
lines << 'bind-interfaces'
|
|
152
|
+
lines << 'listen-address=127.0.0.1'
|
|
153
|
+
lines << "listen-address=#{server_ip}"
|
|
154
|
+
lines << ''
|
|
155
|
+
|
|
156
|
+
records.sort.each do |hostname, ips|
|
|
157
|
+
Array(ips).each do |ip|
|
|
158
|
+
lines << "address=/#{hostname}/#{ip}"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
lines.join("\n") + "\n"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -25,10 +25,16 @@ module Messhy
|
|
|
25
25
|
show_node_status(node_name)
|
|
26
26
|
puts
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
show_dns_status if config.dns_enabled?
|
|
30
|
+
|
|
31
|
+
show_latency_matrix
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
def show_node_status(node_name)
|
|
31
35
|
node_config = config.node_config(node_name)
|
|
36
|
+
label = node_config['label']
|
|
37
|
+
label_display = label.to_s.strip.empty? ? '' : " (#{label})"
|
|
32
38
|
|
|
33
39
|
begin
|
|
34
40
|
status = @ssh_executor.get_wireguard_status(node_name)
|
|
@@ -37,7 +43,7 @@ module Messhy
|
|
|
37
43
|
peers = status.scan(/peer: (.+?)$/).flatten
|
|
38
44
|
|
|
39
45
|
if peers.any?
|
|
40
|
-
puts "✓ #{node_name} (#{node_config['private_ip']}) - connected to #{peers.size} peers"
|
|
46
|
+
puts "✓ #{node_name} (#{node_config['private_ip']})#{label_display} - connected to #{peers.size} peers"
|
|
41
47
|
|
|
42
48
|
# Show basic peer info
|
|
43
49
|
status.split('peer:').drop(1).each do |peer_block|
|
|
@@ -48,10 +54,10 @@ module Messhy
|
|
|
48
54
|
puts " └─ Peer: #{endpoint} - #{stats[:received]} rx, #{stats[:sent]} tx"
|
|
49
55
|
end
|
|
50
56
|
else
|
|
51
|
-
puts "✗ #{node_name} (#{node_config['private_ip']}) - 0 peers (DOWN)"
|
|
57
|
+
puts "✗ #{node_name} (#{node_config['private_ip']})#{label_display} - 0 peers (DOWN)"
|
|
52
58
|
end
|
|
53
59
|
rescue StandardError => e
|
|
54
|
-
puts "✗ #{node_name} (#{node_config['private_ip']}) - Error: #{e.message}"
|
|
60
|
+
puts "✗ #{node_name} (#{node_config['private_ip']})#{label_display} - Error: #{e.message}"
|
|
55
61
|
end
|
|
56
62
|
end
|
|
57
63
|
|
|
@@ -155,8 +161,88 @@ module Messhy
|
|
|
155
161
|
end
|
|
156
162
|
end
|
|
157
163
|
|
|
164
|
+
def show_dns_status
|
|
165
|
+
puts '==> Mesh DNS Status'
|
|
166
|
+
puts "Domain: #{config.dns_domain}"
|
|
167
|
+
puts "Servers: #{config.dns_server_nodes.join(', ')}"
|
|
168
|
+
puts
|
|
169
|
+
|
|
170
|
+
config.dns_server_nodes.each do |node_name|
|
|
171
|
+
node_config = config.node_config(node_name)
|
|
172
|
+
next unless node_config
|
|
173
|
+
|
|
174
|
+
label = node_config['label']
|
|
175
|
+
label_display = label.to_s.strip.empty? ? '' : " (#{label})"
|
|
176
|
+
|
|
177
|
+
begin
|
|
178
|
+
@ssh_executor.execute_on_node(node_name) do
|
|
179
|
+
service = capture(:systemctl, 'is-active', 'dnsmasq', raise_on_non_zero_exit: false).strip
|
|
180
|
+
messhy_records = capture(:bash, '-c',
|
|
181
|
+
"sudo awk 'BEGIN{c=0} /^address=\\//{c++} END{print c}' " \
|
|
182
|
+
"/etc/dnsmasq.d/messhy.conf 2>/dev/null || true").strip
|
|
183
|
+
ap_records = capture(:bash, '-c',
|
|
184
|
+
"sudo awk 'BEGIN{c=0} /^address=\\//{c++} END{print c}' " \
|
|
185
|
+
"/etc/dnsmasq.d/active_postgres.conf 2>/dev/null || true").strip
|
|
186
|
+
|
|
187
|
+
status_icon = service == 'active' ? '✓' : '✗'
|
|
188
|
+
puts "#{status_icon} #{node_name} (#{node_config['private_ip']})#{label_display} - dnsmasq #{service}"
|
|
189
|
+
puts " └─ records: messhy=#{messhy_records.to_i} active_postgres=#{ap_records.to_i}"
|
|
190
|
+
end
|
|
191
|
+
rescue StandardError => e
|
|
192
|
+
puts "✗ #{node_name} (#{node_config['private_ip']})#{label_display} - DNS check failed: #{e.message}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
puts
|
|
197
|
+
end
|
|
198
|
+
|
|
158
199
|
private
|
|
159
200
|
|
|
201
|
+
def show_latency_matrix
|
|
202
|
+
node_names = config.node_names
|
|
203
|
+
return if node_names.size < 2
|
|
204
|
+
|
|
205
|
+
puts '==> Latency Matrix (ms)'
|
|
206
|
+
puts
|
|
207
|
+
|
|
208
|
+
latencies = {}
|
|
209
|
+
tested_pairs = Set.new
|
|
210
|
+
|
|
211
|
+
node_names.each do |source|
|
|
212
|
+
node_names.each do |target|
|
|
213
|
+
next if source == target
|
|
214
|
+
|
|
215
|
+
pair_key = [source, target].sort.join('-')
|
|
216
|
+
next if tested_pairs.include?(pair_key)
|
|
217
|
+
|
|
218
|
+
tested_pairs.add(pair_key)
|
|
219
|
+
target_ip = config.node_config(target)['private_ip']
|
|
220
|
+
latency = @ssh_executor.measure_latency(source, target_ip)
|
|
221
|
+
latencies[[source, target]] = latency
|
|
222
|
+
latencies[[target, source]] = latency
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
max_name_len = node_names.map(&:length).max
|
|
227
|
+
header = ' ' * (max_name_len + 2) + node_names.map { |n| n[0..7].rjust(8) }.join(' ')
|
|
228
|
+
puts header
|
|
229
|
+
puts '-' * header.length
|
|
230
|
+
|
|
231
|
+
node_names.each do |source|
|
|
232
|
+
row = node_names.map do |target|
|
|
233
|
+
if source == target
|
|
234
|
+
' - '
|
|
235
|
+
else
|
|
236
|
+
lat = latencies[[source, target]]
|
|
237
|
+
lat ? format('%7.1f ', lat) : ' N/A '
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
puts "#{source.ljust(max_name_len)} #{row.join(' ')}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
puts
|
|
244
|
+
end
|
|
245
|
+
|
|
160
246
|
def handshake_recent?(source_name, target_ip, status_cache)
|
|
161
247
|
status = status_cache[source_name] ||= @ssh_executor.get_wireguard_status(source_name)
|
|
162
248
|
peer_block = WireguardStatusParser.extract_peer_block(status, target_ip)
|
data/lib/messhy/installer.rb
CHANGED
|
@@ -57,6 +57,11 @@ module Messhy
|
|
|
57
57
|
puts "\n==> Verifying mesh connectivity..."
|
|
58
58
|
verify_mesh(skip: skip)
|
|
59
59
|
|
|
60
|
+
if config.dns_enabled?
|
|
61
|
+
puts "\n==> Setting up mesh DNS..."
|
|
62
|
+
DnsManager.new(config, ssh_executor: ssh_executor, dry_run: dry_run, skip: skip).setup
|
|
63
|
+
end
|
|
64
|
+
|
|
60
65
|
puts "\n✓ WireGuard mesh setup complete!"
|
|
61
66
|
end
|
|
62
67
|
|
data/lib/messhy/ssh_executor.rb
CHANGED
|
@@ -172,6 +172,19 @@ module Messhy
|
|
|
172
172
|
false
|
|
173
173
|
end
|
|
174
174
|
|
|
175
|
+
def measure_latency(source_node, target_ip)
|
|
176
|
+
latency = nil
|
|
177
|
+
execute_on_node(source_node) do
|
|
178
|
+
output = capture(:ping, '-c', '3', '-W', '2', '-I', 'wg0', target_ip, raise_on_non_zero_exit: false)
|
|
179
|
+
if output =~ %r{rtt min/avg/max/mdev = [\d.]+/([\d.]+)/}
|
|
180
|
+
latency = ::Regexp.last_match(1).to_f
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
latency
|
|
184
|
+
rescue StandardError
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
|
|
175
188
|
def test_tcp_connectivity(source_node, target_ip, port = 22)
|
|
176
189
|
success = false
|
|
177
190
|
execute_on_node(source_node) do
|
data/lib/messhy/version.rb
CHANGED
data/lib/messhy.rb
CHANGED
data/lib/tasks/messhy.rake
CHANGED
|
@@ -9,11 +9,6 @@ namespace :messhy do
|
|
|
9
9
|
system('bundle exec messhy setup')
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
desc 'Check VPN mesh connectivity'
|
|
13
|
-
task :health do
|
|
14
|
-
system('bundle exec messhy health')
|
|
15
|
-
end
|
|
16
|
-
|
|
17
12
|
desc 'Generate new WireGuard keys'
|
|
18
13
|
task :keygen do
|
|
19
14
|
system('bundle exec messhy keygen')
|
|
@@ -29,4 +24,9 @@ namespace :messhy do
|
|
|
29
24
|
task :trust_hosts do
|
|
30
25
|
system('bundle exec messhy trust-hosts')
|
|
31
26
|
end
|
|
27
|
+
|
|
28
|
+
desc 'Setup mesh DNS (dnsmasq)'
|
|
29
|
+
task :dns do
|
|
30
|
+
system('bundle exec messhy dns')
|
|
31
|
+
end
|
|
32
32
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: messhy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BoringCache
|
|
@@ -122,6 +122,7 @@ files:
|
|
|
122
122
|
- lib/messhy.rb
|
|
123
123
|
- lib/messhy/cli.rb
|
|
124
124
|
- lib/messhy/configuration.rb
|
|
125
|
+
- lib/messhy/dns_manager.rb
|
|
125
126
|
- lib/messhy/generators/messhy/install_generator.rb
|
|
126
127
|
- lib/messhy/health_checker.rb
|
|
127
128
|
- lib/messhy/host_trust_manager.rb
|
|
@@ -156,7 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
156
157
|
- !ruby/object:Gem::Version
|
|
157
158
|
version: '0'
|
|
158
159
|
requirements: []
|
|
159
|
-
rubygems_version: 3.6.
|
|
160
|
+
rubygems_version: 3.6.9
|
|
160
161
|
specification_version: 4
|
|
161
162
|
summary: WireGuard VPN mesh for Ruby & Rails apps
|
|
162
163
|
test_files: []
|