messhy 0.4.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.
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sshkit'
4
+ require 'sshkit/dsl'
5
+ require 'stringio'
6
+
7
+ module Messhy
8
+ class SSHExecutor
9
+ include SSHKit::DSL
10
+
11
+ attr_reader :config
12
+
13
+ def initialize(config)
14
+ @config = config
15
+ setup_sshkit
16
+ end
17
+
18
+ def execute_on_node(node_name, &)
19
+ node_config = config.node_config(node_name)
20
+ raise Error, "Node not found: #{node_name}" unless node_config
21
+
22
+ host = host_for(node_name, node_config)
23
+ on(host, &)
24
+ end
25
+
26
+ def execute_on_all_nodes(skip: nil, &)
27
+ hosts = config.each_node.with_object([]) do |(node_name, node_config), collection|
28
+ next if skip && node_name == skip
29
+
30
+ collection << host_for(node_name, node_config)
31
+ end
32
+
33
+ return if hosts.empty?
34
+
35
+ on(hosts, in: :parallel, &)
36
+ end
37
+
38
+ def install_wireguard(node_name)
39
+ execute_on_node(node_name) do
40
+ # Check if WireGuard is already installed
41
+ if test('[ -f /usr/bin/wg ]')
42
+ info 'WireGuard already installed'
43
+ else
44
+ info 'Installing WireGuard...'
45
+ execute :sudo, 'apt-get', 'update', '-qq'
46
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq', 'wireguard',
47
+ 'iputils-ping'
48
+ end
49
+
50
+ # Install ping if not available
51
+ unless test('which', 'ping', raise_on_error: false)
52
+ info 'Installing ping utility...'
53
+ execute :sudo, 'apt-get', 'update', '-qq', raise_on_error: false
54
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq', 'iputils-ping',
55
+ raise_on_error: false
56
+ end
57
+ end
58
+ end
59
+
60
+ def generate_keypair_on_node(node_name)
61
+ keypair = {}
62
+ execute_on_node(node_name) do
63
+ keypair[:private_key] = capture('wg', 'genkey').strip
64
+ keypair[:public_key] = capture(:echo, keypair[:private_key], '|', 'wg', 'pubkey').strip
65
+ end
66
+ keypair
67
+ end
68
+
69
+ def generate_psk_on_node(node_name)
70
+ psk = nil
71
+ execute_on_node(node_name) do
72
+ psk = capture('wg', 'genpsk').strip
73
+ end
74
+ psk
75
+ end
76
+
77
+ def install_wireguard_on_all_nodes(skip: nil)
78
+ execute_on_all_nodes(skip: skip) do
79
+ # Check if WireGuard is already installed
80
+ if test('[ -f /usr/bin/wg ]')
81
+ info 'WireGuard already installed'
82
+ else
83
+ info 'Installing WireGuard...'
84
+ execute :sudo, 'apt-get', 'update', '-qq'
85
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq', 'wireguard',
86
+ 'iputils-ping'
87
+ end
88
+
89
+ # Install ping if not available
90
+ unless test('which', 'ping', raise_on_error: false)
91
+ info 'Installing ping utility...'
92
+ execute :sudo, 'apt-get', 'update', '-qq', raise_on_error: false
93
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq', 'iputils-ping',
94
+ raise_on_error: false
95
+ end
96
+ end
97
+ end
98
+
99
+ def upload_config(node_name, config_content)
100
+ execute_on_node(node_name) do
101
+ # Create temporary file
102
+ temp_file = '/tmp/wg0.conf'
103
+ upload! StringIO.new(config_content), temp_file
104
+
105
+ # Move to /etc/wireguard with proper permissions
106
+ execute :sudo, 'mv', temp_file, '/etc/wireguard/wg0.conf'
107
+ execute :sudo, 'chmod', '600', '/etc/wireguard/wg0.conf'
108
+ end
109
+ end
110
+
111
+ def upload_and_start_configs(configs)
112
+ hosts = configs.filter_map do |node_name, config_content|
113
+ node_config = config.node_config(node_name)
114
+ next unless node_config
115
+
116
+ host = host_for(node_name, node_config)
117
+ manage_property(host.properties, :config_content, config_content)
118
+ host
119
+ end
120
+
121
+ return if hosts.empty?
122
+
123
+ executor = self
124
+
125
+ on hosts, in: :parallel do |host|
126
+ properties = host.properties
127
+ config_content = executor.send(:manage_property, properties, :config_content)
128
+ temp_file = '/tmp/wg0.conf'
129
+ upload! StringIO.new(config_content), temp_file
130
+ execute :sudo, 'mv', temp_file, '/etc/wireguard/wg0.conf'
131
+ execute :sudo, 'chmod', '600', '/etc/wireguard/wg0.conf'
132
+ execute :sudo, 'systemctl', 'enable', 'wg-quick@wg0'
133
+ if test('systemctl is-active wg-quick@wg0')
134
+ execute :sudo, 'systemctl', 'restart', 'wg-quick@wg0'
135
+ else
136
+ execute :sudo, 'systemctl', 'start', 'wg-quick@wg0'
137
+ end
138
+ end
139
+ end
140
+
141
+ def enable_and_start_wireguard(node_name)
142
+ execute_on_node(node_name) do
143
+ # Enable systemd service
144
+ execute :sudo, 'systemctl', 'enable', 'wg-quick@wg0'
145
+
146
+ # Restart WireGuard
147
+ if test('systemctl is-active wg-quick@wg0')
148
+ execute :sudo, 'systemctl', 'restart', 'wg-quick@wg0'
149
+ else
150
+ execute :sudo, 'systemctl', 'start', 'wg-quick@wg0'
151
+ end
152
+ end
153
+ end
154
+
155
+ def get_wireguard_status(node_name)
156
+ result = nil
157
+ execute_on_node(node_name) do
158
+ result = capture(:sudo, 'wg', 'show', 'wg0')
159
+ end
160
+ result
161
+ end
162
+
163
+ def ping_node_from(source_node, target_ip)
164
+ success = false
165
+ execute_on_node(source_node) do
166
+ if test('which', 'ping', raise_on_error: false)
167
+ success = test('timeout', '3', 'ping', '-c', '1', '-W', '1', '-I', 'wg0', target_ip, raise_on_error: false)
168
+ end
169
+ end
170
+ success
171
+ rescue StandardError
172
+ false
173
+ end
174
+
175
+ def test_tcp_connectivity(source_node, target_ip, port = 22)
176
+ success = false
177
+ execute_on_node(source_node) do
178
+ success = test('timeout', '2', 'bash', '-c',
179
+ "exec 3<>/dev/tcp/#{target_ip}/#{port} 2>&1 && exec 3<&- && exec 3>&-", raise_on_error: false)
180
+ end
181
+ success
182
+ rescue StandardError
183
+ false
184
+ end
185
+
186
+ def restart_wireguard(node_name)
187
+ execute_on_node(node_name) do
188
+ # Stop service first
189
+ if test('systemctl is-active wg-quick@wg0', raise_on_error: false)
190
+ execute :sudo, 'systemctl', 'stop', 'wg-quick@wg0'
191
+ end
192
+
193
+ # Remove interface if it exists
194
+ if test('[ -d /sys/class/net/wg0 ]', raise_on_error: false)
195
+ execute :sudo, 'ip', 'link', 'delete', 'wg0', raise_on_error: false
196
+ end
197
+
198
+ # Start fresh
199
+ execute :sudo, 'systemctl', 'start', 'wg-quick@wg0'
200
+ end
201
+ end
202
+
203
+ def stop_wireguard(node_name)
204
+ execute_on_node(node_name) do
205
+ execute :sudo, 'systemctl', 'stop', 'wg-quick@wg0' if test('systemctl is-active wg-quick@wg0')
206
+ end
207
+ end
208
+
209
+ def purge_wireguard(node_name)
210
+ execute_on_node(node_name) do
211
+ # Stop and disable service
212
+ if test('systemctl is-active wg-quick@wg0', raise_on_error: false)
213
+ execute :sudo, 'systemctl', 'stop', 'wg-quick@wg0', raise_on_error: false
214
+ end
215
+ if test('systemctl is-enabled wg-quick@wg0', raise_on_error: false)
216
+ execute :sudo, 'systemctl', 'disable', 'wg-quick@wg0', raise_on_error: false
217
+ end
218
+
219
+ # Remove interface
220
+ if test('[ -d /sys/class/net/wg0 ]', raise_on_error: false)
221
+ execute :sudo, 'ip', 'link', 'delete', 'wg0', raise_on_error: false
222
+ end
223
+
224
+ # Remove config
225
+ if test('[ -f /etc/wireguard/wg0.conf ]', raise_on_error: false)
226
+ execute :sudo, 'rm', '-f', '/etc/wireguard/wg0.conf', raise_on_error: false
227
+ end
228
+ end
229
+ end
230
+
231
+ private
232
+
233
+ def install_wireguard_packages
234
+ # Check if WireGuard is already installed
235
+ if test('[ -f /usr/bin/wg ]')
236
+ info 'WireGuard already installed'
237
+ else
238
+ info 'Installing WireGuard...'
239
+ execute :sudo, 'apt-get', 'update', '-qq'
240
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq', 'wireguard',
241
+ 'iputils-ping'
242
+ end
243
+
244
+ # Install ping if not available
245
+ return if test('which', 'ping', raise_on_error: false)
246
+
247
+ info 'Installing ping utility...'
248
+ execute :sudo, 'apt-get', 'update', '-qq', raise_on_error: false
249
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq', 'iputils-ping',
250
+ raise_on_error: false
251
+ end
252
+
253
+ def setup_sshkit
254
+ SSHKit.config.output_verbosity = Logger::INFO
255
+ SSHKit.config.use_format :pretty
256
+
257
+ SSHKit::Backend::Netssh.configure do |ssh|
258
+ ssh.ssh_options = build_ssh_options
259
+ end
260
+ end
261
+
262
+ def build_ssh_options
263
+ options = {
264
+ forward_agent: false,
265
+ auth_methods: ['publickey'],
266
+ verify_host_key: config.verify_host_key_mode
267
+ }
268
+
269
+ if File.exist?(config.ssh_key)
270
+ options[:keys] = [config.ssh_key]
271
+ options[:keys_only] = true
272
+ end
273
+
274
+ options
275
+ end
276
+
277
+ def host_for(node_name, node_config)
278
+ host = SSHKit::Host.new(node_config['host'])
279
+ ssh_user = node_config['ssh_user'] || node_config['user'] || config.user
280
+ host.user = ssh_user if ssh_user
281
+
282
+ ssh_port = node_config['ssh_port'] || node_config['port']
283
+ host.port = ssh_port if ssh_port
284
+
285
+ if node_config['ssh_key']
286
+ keys = Array(node_config['ssh_key']).map { |path| File.expand_path(path) }
287
+ merged = (host.ssh_options || {}).merge(keys: keys, keys_only: true)
288
+ host.ssh_options = merged
289
+ end
290
+
291
+ manage_property(host.properties, :node_name, node_name)
292
+ host
293
+ end
294
+
295
+ def manage_property(properties, key, value = nil)
296
+ if value.nil?
297
+ # Fetch mode
298
+ properties.respond_to?(:fetch) ? properties.fetch(key) : properties[key]
299
+ else
300
+ # Assign mode
301
+ properties.respond_to?(:set) ? properties.set(key, value) : properties[key] = value
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Messhy
4
+ VERSION = '0.4.0'
5
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Messhy
4
+ module WireguardStatusParser
5
+ TIME_UNITS_IN_SECONDS = {
6
+ 'second' => 1,
7
+ 'minute' => 60,
8
+ 'hour' => 3_600,
9
+ 'day' => 86_400
10
+ }.freeze
11
+
12
+ module_function
13
+
14
+ def extract_peer_block(status, target_ip)
15
+ status.split('peer:').drop(1).find do |block|
16
+ block.include?("allowed ips: #{target_ip}/32")
17
+ end
18
+ end
19
+
20
+ def parse_handshake_seconds(desc)
21
+ return nil if desc.strip.casecmp('(none)').zero?
22
+
23
+ matches = desc.scan(/(\d+)\s+(second|minute|hour|day)s?/i)
24
+ return nil if matches.empty?
25
+
26
+ matches.sum do |value, unit|
27
+ TIME_UNITS_IN_SECONDS[unit.downcase] * value.to_i
28
+ end
29
+ end
30
+
31
+ def extract_handshake_time(peer_block)
32
+ return nil unless peer_block
33
+
34
+ desc = peer_block[/latest handshake:\s*(.+)/, 1]
35
+ return nil unless desc&.include?('ago')
36
+
37
+ parse_handshake_seconds(desc)
38
+ end
39
+
40
+ def extract_transfer_stats(peer_block)
41
+ rx = peer_block.match(/transfer: (.+?) received/)&.[](1) || '0 B'
42
+ tx = peer_block.match(/received, (.+?) sent/)&.[](1) || '0 B'
43
+ { received: rx, sent: tx }
44
+ end
45
+
46
+ def extract_endpoint(peer_block)
47
+ peer_block[/endpoint: (.+?)$/, 1]
48
+ end
49
+
50
+ def extract_allowed_ips(peer_block)
51
+ peer_block[/allowed ips: (.+?)$/, 1]
52
+ end
53
+ end
54
+ end
data/lib/messhy.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'messhy/version'
4
+ require_relative 'messhy/configuration'
5
+ require_relative 'messhy/wireguard_status_parser'
6
+ require_relative 'messhy/installer'
7
+ require_relative 'messhy/mesh_builder'
8
+ require_relative 'messhy/ssh_executor'
9
+ require_relative 'messhy/health_checker'
10
+ require_relative 'messhy/host_trust_manager'
11
+ require_relative 'messhy/cli'
12
+
13
+ module Messhy
14
+ class Error < StandardError; end
15
+
16
+ def self.root
17
+ File.expand_path('..', __dir__)
18
+ end
19
+ end
20
+
21
+ require_relative 'messhy/railtie'
@@ -0,0 +1,32 @@
1
+ namespace :messhy do
2
+ desc 'Install Messhy configuration'
3
+ task :install do
4
+ system('rails generate messhy:install')
5
+ end
6
+
7
+ desc 'Deploy WireGuard VPN mesh to all nodes'
8
+ task :setup do
9
+ system('bundle exec messhy setup')
10
+ end
11
+
12
+ desc 'Check VPN mesh connectivity'
13
+ task :health do
14
+ system('bundle exec messhy health')
15
+ end
16
+
17
+ desc 'Generate new WireGuard keys'
18
+ task :keygen do
19
+ system('bundle exec messhy keygen')
20
+ end
21
+
22
+ desc 'Show mesh status'
23
+ task :status do
24
+ puts "\n🔒 WireGuard VPN Mesh Status\n\n"
25
+ system('bundle exec messhy status')
26
+ end
27
+
28
+ desc 'Trust SSH host keys for all nodes'
29
+ task :trust_hosts do
30
+ system('bundle exec messhy trust-hosts')
31
+ end
32
+ end
@@ -0,0 +1,24 @@
1
+ # WireGuard configuration for <%= node_name %>
2
+ # Generated by messhy
3
+
4
+ [Interface]
5
+ Address = <%= interface_ip %>/<%= prefix_length %>
6
+ PrivateKey = <%= private_key %>
7
+ ListenPort = <%= listen_port %>
8
+ MTU = <%= mtu %>
9
+
10
+ # Enable IP forwarding
11
+ PostUp = sysctl -w net.ipv4.ip_forward=1
12
+ PostDown = sysctl -w net.ipv4.ip_forward=0
13
+
14
+ <% peers.each do |peer| %>
15
+ # Peer: <%= peer[:name] %>
16
+ [Peer]
17
+ PublicKey = <%= peer[:public_key] %>
18
+ PresharedKey = <%= peer[:preshared_key] %>
19
+ AllowedIPs = <%= peer[:allowed_ips] %>
20
+ Endpoint = <%= peer[:endpoint] %>
21
+ PersistentKeepalive = <%= peer[:keepalive] %>
22
+
23
+ <% end %>
24
+
metadata ADDED
@@ -0,0 +1,162 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: messhy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - BoringCache
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bcrypt_pbkdf
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ed25519
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.3'
40
+ - !ruby/object:Gem::Dependency
41
+ name: sshkit
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.21'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.21'
54
+ - !ruby/object:Gem::Dependency
55
+ name: thor
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.3'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.3'
68
+ - !ruby/object:Gem::Dependency
69
+ name: minitest
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '5.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '5.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rubocop
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.50'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.50'
96
+ - !ruby/object:Gem::Dependency
97
+ name: simplecov
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.22'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.22'
110
+ description: Sets up a full WireGuard VPN mesh across any VMs. Every node connects
111
+ directly to every other node for secure private networking.
112
+ email:
113
+ - oss@boringcache.com
114
+ executables:
115
+ - messhy
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - LICENSE
120
+ - README.md
121
+ - exe/messhy
122
+ - lib/messhy.rb
123
+ - lib/messhy/cli.rb
124
+ - lib/messhy/configuration.rb
125
+ - lib/messhy/generators/messhy/install_generator.rb
126
+ - lib/messhy/health_checker.rb
127
+ - lib/messhy/host_trust_manager.rb
128
+ - lib/messhy/installer.rb
129
+ - lib/messhy/mesh_builder.rb
130
+ - lib/messhy/railtie.rb
131
+ - lib/messhy/ssh_executor.rb
132
+ - lib/messhy/version.rb
133
+ - lib/messhy/wireguard_status_parser.rb
134
+ - lib/tasks/messhy.rake
135
+ - templates/wg0.conf.erb
136
+ homepage: https://github.com/boringcache/messhy
137
+ licenses:
138
+ - MIT
139
+ metadata:
140
+ homepage_uri: https://github.com/boringcache/messhy
141
+ source_code_uri: https://github.com/boringcache/messhy
142
+ documentation_uri: https://github.com/boringcache/messhy/blob/main/README.md
143
+ changelog_uri: https://github.com/boringcache/messhy/blob/main/CHANGELOG.md
144
+ rubygems_mfa_required: 'true'
145
+ rdoc_options: []
146
+ require_paths:
147
+ - lib
148
+ required_ruby_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: 3.4.0
153
+ required_rubygems_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ requirements: []
159
+ rubygems_version: 3.6.7
160
+ specification_version: 4
161
+ summary: WireGuard VPN mesh for Ruby & Rails apps
162
+ test_files: []