wg-admin 0.0.2

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,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module WireGuard
6
+ module Admin
7
+ #
8
+ # Commands for working with clients
9
+ #
10
+ class Clients < Thor
11
+ extend ClassHelpers
12
+ include InstanceHelpers
13
+
14
+ # rubocop:disable Metrics/AbcSize
15
+ desc 'add NAME', 'Adds a new client with the given NAME'
16
+ long_desc 'Adds a new client to the configuration database.'
17
+ method_option :network, desc: 'network', aliases: '-n', default: default_network
18
+ method_option :ip, desc: 'the IP address of the new client', aliases: '-i', required: false
19
+ def add(name)
20
+ warn "Using database #{repository.path}" if options[:verbose]
21
+ client = Client.new(name: name, ip: ip)
22
+ repository.add_peer(network, client)
23
+ if options[:verbose]
24
+ warn 'New client was successfully added:'
25
+ warn ''
26
+ warn client
27
+ end
28
+ rescue StandardError => e
29
+ raise Thor::Error, "Error: #{e.message}"
30
+ end
31
+
32
+ desc 'list', 'Lists all clients'
33
+ long_desc 'For a given network, lists all clients in the configuration database.'
34
+ method_option :network, desc: 'network', aliases: '-n', default: default_network
35
+ def list
36
+ if options[:verbose]
37
+ warn "Using database #{repository.path}"
38
+ warn "No clients in network #{network}." if repository.networks.empty?
39
+ end
40
+ repository.clients(network).each do |client|
41
+ puts client
42
+ end
43
+ rescue StandardError => e
44
+ raise Thor::Error, "Error: #{e.message}"
45
+ end
46
+ # rubocop:enable Metrics/AbcSize
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO: Check ~/workspace/rtc-cli/test/test_helper.rb on how to improve this
4
+
5
+ module WireGuard
6
+ module Admin
7
+ #
8
+ # Shared class methods
9
+ #
10
+ module ClassHelpers
11
+ def default_network
12
+ if repository.networks.size != 1
13
+ ENV['WG_ADMIN_NETWORK']
14
+ else
15
+ nw = repository.networks.first
16
+ ENV.fetch('WG_ADMIN_NETWORK', "#{nw}/#{nw.prefix}")
17
+ end
18
+ end
19
+
20
+ def path
21
+ ENV['WG_ADMIN_STORE'] || File.expand_path('~/.wg-admin.pstore')
22
+ end
23
+
24
+ def repository
25
+ @repository ||= Repository.new(path)
26
+ end
27
+
28
+ Thor.class_option :verbose, type: :boolean, aliases: '-v'
29
+ end
30
+
31
+ #
32
+ # Shared instance methods
33
+ #
34
+ module InstanceHelpers
35
+ def repository
36
+ self.class.repository
37
+ end
38
+
39
+ def ip
40
+ if options[:ip]
41
+ IPAddr.new(options[:ip])
42
+ else
43
+ repository.next_address(network)
44
+ end
45
+ end
46
+
47
+ def network
48
+ IPAddr.new(options[:network])
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'wire_guard/admin/cli/helpers'
5
+
6
+ module WireGuard
7
+ module Admin
8
+ #
9
+ # Commands for working with networks
10
+ #
11
+ class Networks < Thor
12
+ extend ClassHelpers
13
+ include InstanceHelpers
14
+
15
+ # rubocop:disable Metrics/AbcSize
16
+ desc 'list', 'Lists all known networks'
17
+ long_desc 'List the networks in the configuration database.'
18
+ def list
19
+ if options[:verbose]
20
+ warn "Using database #{repository.path}"
21
+ warn 'No networks defined.' if repository.networks.empty?
22
+ end
23
+
24
+ repository.networks.each do |network|
25
+ puts "#{network}/#{network.prefix}"
26
+ end
27
+ rescue StandardError => e
28
+ raise Thor::Error, "Error: #{e.message}"
29
+ end
30
+ # rubocop:enable Metrics/AbcSize
31
+
32
+ desc 'add NETWORK', 'Adds a new network'
33
+ long_desc 'Adds a new network to the configuration database.'
34
+ def add(network)
35
+ warn "Using database #{repository.path}" if options[:verbose]
36
+ nw = IPAddr.new(network)
37
+ repository.add_network(nw)
38
+ warn "Network #{nw}/#{nw.prefix} was successfully added." if options[:verbose]
39
+ rescue Repository::NetworkAlreadyExists => e
40
+ raise Thor::Error, "Error: #{e.message}"
41
+ end
42
+
43
+ desc 'delete NETWORK', 'Deletes a network'
44
+ long_desc 'Deletes an existingnetwork from the configuration database.'
45
+ def delete(network)
46
+ warn "Using database #{repository.path}" if options[:verbose]
47
+ nw = IPAddr.new(network)
48
+ repository.delete_network(nw)
49
+ warn "Network #{nw}/#{nw.prefix} was successfully deleted." if options[:verbose]
50
+ rescue StandardError => e
51
+ raise Thor::Error, "Error: #{e.message}"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module WireGuard
6
+ module Admin
7
+ #
8
+ # Commands for working with peers (servers and clients)
9
+ #
10
+ class Peers < Thor
11
+ extend ClassHelpers
12
+ include InstanceHelpers
13
+
14
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
15
+ desc 'list', 'Lists all peers'
16
+ long_desc 'For a given network, lists all peers (servers and clients) in the configuration database.'
17
+ method_option :network, desc: 'network', aliases: '-n', default: default_network
18
+ def list
19
+ if options[:verbose]
20
+ warn "Using database #{repository.path}"
21
+ warn "No clients in network #{network}." if repository.networks.empty?
22
+ end
23
+ repository.peers(network).each do |peer|
24
+ if STDOUT.tty?
25
+ puts peer
26
+ else
27
+ puts peer.name
28
+ end
29
+ end
30
+ rescue StandardError => e
31
+ raise Thor::Error, "Error: #{e.message}"
32
+ end
33
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module WireGuard
6
+ module Admin
7
+ #
8
+ # Commands for working with servers
9
+ #
10
+ class Servers < Thor
11
+ extend ClassHelpers
12
+ include InstanceHelpers
13
+
14
+ # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
15
+ desc 'add NAME', 'Adds a new server'
16
+ long_desc 'Adds a new server with the given public DNS NAME to the configuration database.'
17
+ method_option :network, desc: 'network', aliases: '-n', default: default_network
18
+ method_option :ip, desc: 'the (private) IP address of the new server (within the VPN)', aliases: '-i', required: false
19
+ method_option :port, desc: 'port to listen on', aliases: '-p', required: false
20
+ method_option :allowed_ips, desc: 'The range of allowed IP addresses that this server is routing', aliases: '-a', required: false
21
+ method_option :device, desc: 'The network device used for forwarding traffic', aliases: '-d', required: false
22
+ def add(name)
23
+ warn "Using database #{repository.path}" if options[:verbose]
24
+ server = Server.new(name: name, ip: ip, allowed_ips: options[:allowed_ips] || repository.find_network(network))
25
+ server.device = options[:device] if options[:device]
26
+ server.port = options[:port] if options[:port]
27
+ repository.add_peer(network, server)
28
+ if options[:verbose]
29
+ warn 'New server was successfully added:'
30
+ warn ''
31
+ warn server
32
+ end
33
+ rescue StandardError => e
34
+ raise Thor::Error, "Error: #{e.message}"
35
+ end
36
+
37
+ desc 'list', 'Lists all servers'
38
+ long_desc 'For a given network, lists all servers in the configuration database.'
39
+ method_option :network, desc: 'network', aliases: '-n', default: default_network
40
+ def list
41
+ if options[:verbose]
42
+ warn "Using database #{repository.path}"
43
+ warn "No servers in network #{network}." if repository.networks.empty?
44
+ end
45
+ repository.servers(network).each do |server|
46
+ puts server
47
+ end
48
+ rescue StandardError => e
49
+ raise Thor::Error, "Error: #{e.message}"
50
+ end
51
+ # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module WireGuard
6
+ module Admin
7
+ InvocationError = Class.new(StandardError) do
8
+ def initialize(lines)
9
+ super(lines.first)
10
+ end
11
+ end
12
+
13
+ ProgramNotFoundError = Class.new(StandardError) do
14
+ def initialize
15
+ super('wg was not found in the PATH. Perhaps it is not installed?')
16
+ end
17
+ end
18
+
19
+ #
20
+ # A host that connects to the VPN and registers a VPN subnet address such as 192.0.2.3 for itself.
21
+ #
22
+ # @see https://github.com/pirate/wireguard-docs#peernodedevice
23
+ #
24
+ class Client
25
+ attr_reader :name, :ip, :private_key, :public_key
26
+
27
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
28
+ def initialize(name:, ip:, private_key: nil, public_key: nil)
29
+ raise ArgumentError, 'name must be present' if name.nil?
30
+ raise ArgumentError, 'name must not be empty' if name.empty?
31
+ raise ArgumentError, 'ip must be present' if ip.nil?
32
+ raise ArgumentError, 'private_key must not be empty' if private_key&.empty?
33
+ raise ArgumentError, 'public_key must not be empty' if public_key&.empty?
34
+
35
+ @name = name
36
+ @ip = ip
37
+ @private_key = private_key || generate_private_key
38
+ @public_key = public_key || generate_public_key
39
+ end
40
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
41
+
42
+ def to_s
43
+ "#{self.class.name.split('::').last} #{name}: #{ip}"
44
+ end
45
+
46
+ def hash
47
+ name.hash
48
+ end
49
+
50
+ def eql?(other)
51
+ hash == other.hash
52
+ end
53
+
54
+ def ==(other)
55
+ name == other.name
56
+ end
57
+
58
+ private
59
+
60
+ def generate_public_key
61
+ Open3.popen3('wg pubkey') do |stdin, stdout, stderr, waiter|
62
+ stdin.write(private_key)
63
+ stdin.close
64
+ raise InvocationError, stderr.lines unless waiter.value.success?
65
+
66
+ stdout.read.chomp
67
+ end
68
+ rescue SystemCallError => e
69
+ raise ProgramNotFoundError if e.message =~ /No such file or directory/
70
+
71
+ raise
72
+ end
73
+
74
+ def generate_private_key
75
+ Open3.popen3('wg genkey') do |_, stdout, stderr, waiter|
76
+ raise InvocationError, stderr.lines unless waiter.value.success?
77
+
78
+ stdout.read.chomp
79
+ end
80
+ rescue SystemCallError => e
81
+ raise ProgramNotFoundError if e.message =~ /No such file or directory/
82
+
83
+ raise
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pstore'
4
+
5
+ module WireGuard
6
+ module Admin
7
+ #
8
+ # The place where networks, clients and servers can be found and are persisted
9
+ #
10
+ class Repository
11
+ #
12
+ # Raised if the network was not specified
13
+ #
14
+ class NetworkNotSpecified < StandardError
15
+ def initialize
16
+ super('Network not specified')
17
+ end
18
+ end
19
+
20
+ #
21
+ # Raised if the network is not known
22
+ #
23
+ class UnknownNetwork < StandardError
24
+ def initialize(unknown)
25
+ super("Network #{unknown} is unknown")
26
+ end
27
+ end
28
+
29
+ #
30
+ # Raised if the network already exists
31
+ #
32
+ class NetworkAlreadyExists < StandardError
33
+ def initialize(existing)
34
+ super("Network #{existing} already exists")
35
+ end
36
+ end
37
+
38
+ #
39
+ # Raised if the network already exists
40
+ #
41
+ class PeerAlreadyExists < StandardError
42
+ def initialize(peer, network)
43
+ super("A peer named #{peer.name} already exists in network #{network}.")
44
+ end
45
+ end
46
+
47
+ attr_reader :path
48
+
49
+ def initialize(path)
50
+ @path = path
51
+ @backend = PStore.new(@path)
52
+ end
53
+
54
+ #
55
+ # Get all networks
56
+ #
57
+ def networks
58
+ @backend.transaction do
59
+ @backend.roots
60
+ end
61
+ end
62
+
63
+ #
64
+ # Find a network within all known ones
65
+ #
66
+ def find_network(network)
67
+ raise ArgumentError, 'network must be an IP address range' unless network.is_a?(IPAddr)
68
+
69
+ networks.select { |n| n == network }.first
70
+ end
71
+
72
+ #
73
+ # Find a peer by name within the given network
74
+ #
75
+ def find_peer(network, name)
76
+ raise ArgumentError, 'network must be an IP address range' unless network.is_a?(IPAddr)
77
+
78
+ peers(network).select { |p| p.name == name }.first
79
+ end
80
+
81
+ #
82
+ # Get all peers of the given network
83
+ #
84
+ def peers(network)
85
+ raise ArgumentError, 'network must be an IP address range' unless network.is_a?(IPAddr)
86
+
87
+ @backend.transaction do
88
+ raise UnknownNetwork, network unless @backend.root?(network)
89
+
90
+ @backend[network]
91
+ end
92
+ end
93
+
94
+ #
95
+ # Add a new network
96
+ #
97
+ def add_network(network)
98
+ raise ArgumentError, 'network must be an IP address range' unless network.is_a?(IPAddr)
99
+
100
+ @backend.transaction do
101
+ raise NetworkAlreadyExists, network if @backend.root?(network)
102
+
103
+ @backend[network] = []
104
+ end
105
+ end
106
+
107
+ def delete_network(network)
108
+ raise ArgumentError, 'network must be an IP address range' unless network.is_a?(IPAddr)
109
+
110
+ @backend.transaction do
111
+ raise UnknownNetwork, network unless @backend.root?(network)
112
+
113
+ @backend.delete(network)
114
+ end
115
+ end
116
+
117
+ #
118
+ # Add a peer to the given network
119
+ #
120
+ def add_peer(network, peer)
121
+ raise PeerAlreadyExists.new(peer, network) if find_peer(network, peer.name)
122
+
123
+ @backend.transaction do
124
+ raise UnknownNetwork, network unless @backend.root?(network)
125
+
126
+ @backend[network] << peer
127
+ end
128
+ end
129
+
130
+ #
131
+ # Find the next address within the given network that is not assigned to a peer
132
+ #
133
+ def next_address(network)
134
+ raise ArgumentError, 'network must be an IP address range' unless network.is_a?(IPAddr)
135
+
136
+ peers(network).inject(network.succ) do |candidate, peer|
137
+ candidate == peer.ip ? candidate.succ : peer.ip
138
+ end
139
+ end
140
+
141
+ def servers(network)
142
+ peers(network).select { |p| p.is_a?(Server) }
143
+ end
144
+
145
+ def clients(network)
146
+ peers(network).select { |p| p.is_a?(Client) }
147
+ end
148
+ end
149
+ end
150
+ end