nub 0.0.103 → 0.0.119

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -14
  3. data/lib/nub/:w +374 -0
  4. data/lib/nub/net.rb +170 -59
  5. metadata +5 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e03ab8b435d9eccad62fe0d1a515377dddf39cdd752e6ae643dd4337bb3a606e
4
- data.tar.gz: 4795f2c20f7eeaf1da565f41c783d15b20007307f3cd926bbe28860419511cdc
3
+ metadata.gz: d7c15f5f66c73221685a124f105c3cca8205d41155603111823e8d580783aea7
4
+ data.tar.gz: 5d1bbe7937d9befa92a78237d6554993490e3e492b2dd75c5721e4457c2e630e
5
5
  SHA512:
6
- metadata.gz: 69825e1f9cb11e189c77ab8ca30f9668300759f30f7fa1805153994c5ffb24eb017b7a3f12abd2067cdf1b04110a64b3558c7ff220d1211172060985ea52f7fc
7
- data.tar.gz: cbdb16ed3fb4eb6b9f219492d60279d6035c233d1c21802b745079ea3e1d78fe12e0610bcfc0cc9fcfcaf2c18925c2ceff61434df84727f562287e246566da6b
6
+ metadata.gz: 4e2d6a2b5344de24c51ddb4913dd1225de0da8e6aeead3a527138bf124bfbe96a309f50ce5cd4b5b572a33bcdaf048da9cde1197c7a4dfb76f9a037e80b740d9
7
+ data.tar.gz: 11bb6954b62eb0326ac9015cd517808edaaabfeed0ec3d220448a80bd91b5ad84a03c8a94deed1c63f29d00a80eae5e3b1fe87fcc43290d902566de35393f0c9
data/README.md CHANGED
@@ -25,6 +25,7 @@ Collection of ruby utils I've used in several of my projects and wanted re-usabl
25
25
  * [Examples](#examples)
26
26
  * [Indicators](#indicators)
27
27
  * [Config Module](#config-module)
28
+ * [ERB Resolution](#erb-resolution)
28
29
  * [Core Module](#core-module)
29
30
  * [FileUtils Extensions](#fileutils-extensions)
30
31
  * [Hash Module](#hash-module)
@@ -350,6 +351,18 @@ Config.init("openvpn.yml")
350
351
  ```
351
352
 
352
353
  ## Core Module <a name="core-module"></a>
354
+ The core module provides a few extensions to common ruby types.
355
+
356
+ ### ERB Resolution <a name="erb-resolution"></a>
357
+ The ***String*** class has been extended to include ***.erb*** and ***.erb!*** to easily resolve
358
+ template variables. Additionally the ***Array*** and ***Hash*** classes have been extended to
359
+ recursively resolve template variables with ***.erb*** and ***.erb!*** functions.
360
+
361
+ Examples:
362
+ ```ruby
363
+ puts("This is a template example <%=foo%>.".erb({'foo': 'foobar'}))
364
+ # outputs: This is a template example foobar.
365
+ ```
353
366
 
354
367
  ## FileUtils Extensions <a name="fileutils-module"></a>
355
368
 
@@ -367,11 +380,11 @@ encapsulate functionality into reusable components.
367
380
  Linux by default shares a single set of network interfaces and routing table entries, such that an
368
381
  installed application can bind to all interfaces and has access to all other services currently
369
382
  running on the system. Network namespaces implemented in the kernel provide a way to isolate
370
- networks and services from each other. This is the technology that docker uses to isolate docker
371
- apps from the host and other docker apps.
383
+ networks, interfaces and services from each other. This is the technology that docker uses to
384
+ isolate networking in docker apps from the host and other docker apps.
372
385
 
373
- Network namespaces provide a way to have different separate virtual interfaces and routing tables
374
- that operate independent of each other. They can be easily manipulated via the ***ip netns*** command.
386
+ Network namespaces provide a way to have separate virtual interfaces and routing tables that operate
387
+ independent of each other. They can be manipulated via the `ip netns` command.
375
388
 
376
389
  ```bash
377
390
  # Create a new network namespace
@@ -382,8 +395,8 @@ sudo ip netns add foo
382
395
  ip netns list
383
396
  ```
384
397
 
385
- Once a network namespace has been created you need to configure how it connected to the host. This
386
- can be done by creating a pair of virtual Ethernet ***veth*** interfaces and assigning them to the
398
+ Once a network namespace has been created you need to configure how/if it connects to the host. This
399
+ can be done by creating a pair of virtual Ethernet (***veth***) interfaces and assigning them to the
387
400
  new network namespace as by default they will be assigned to the root namespace. The root namespace
388
401
  or global namespace is the default namespace used by the host for regular networking.
389
402
 
@@ -429,7 +442,7 @@ sudo ip netns exec foo ping 192.168.100.1
429
442
  # 64 bytes from 192.168.100.1: icmp_seq=1 ttl=64 time=0.080 ms
430
443
  ```
431
444
 
432
- Now we have connectivity between our veth pair across network namespaces. Because
445
+ Now we have connectivity between our veth pair across network namespaces. Because the
433
446
  ***192.168.100.0/24*** network, that we configured the veth pair on, is a separate network from the
434
447
  host any applications running within the new network namespace will not have connectivity to
435
448
  anything else on your host or networks currently including external access to the internet.
@@ -443,18 +456,38 @@ sudo ip netns exec foo ping www.google.com
443
456
  # connect: Network is unreachable
444
457
  ```
445
458
 
446
- In the following sub sections I'll show you how to automated this compliated setup using the
459
+ In the following sub sections I'll show you how to automated this complicated setup using the
447
460
  ***Net*** ruby module.
448
461
 
449
462
  #### TeamViewer Example <a name="teamviewer-example"></a>
450
463
  In this example I'll be showing you how to isolate Teamviewer such that Teamviewer is only able to
451
- bind to the veth2 IPv4 address that we create for it rather than all network interfaces on the host.
452
- This will allow you to have Teamviewer running and accessible from your network facing IP but also
453
- to be able to SSH port forward other Teamviewer instances to your loopback interface or other veth
454
- addresses.
464
+ bind to the veth IPv4 address that we create for it rather than all network interfaces on the host.
465
+ This will allow you to have a local Teamviewer instance running and accessible from your network
466
+ facing IP but also to be able to SSH port forward other Teamviewer instances to your veth addresses.
455
467
 
456
- ```ruby
457
- WIP
468
+ ```bash
469
+ require_relative '../lib/nub/net'
470
+ require_relative '../lib/nub/user'
471
+ !puts("Must be root to execute") and exit if not User.root?
472
+
473
+ if ARGV.size > 0
474
+ cmd = ARGV[0]
475
+ app = ARGV[1]
476
+ namespace = "foo"
477
+ host_veth = Net::Veth.new("veth1", "192.168.100.1")
478
+ guest_veth = Net::Veth.new("veth2", "192.168.100.2")
479
+ network = Net::Network.new("192.168.100.0", "24")
480
+ if cmd == "isolate"
481
+ Net.create_namespace(namespace, host_veth, guest_veth, network, "enp+")
482
+ Net.namespace_connectivity?(namespace, "google.com")
483
+ Net.namespace_exec(namespace, "lxterminal")
484
+ elsif cmd == "destroy"
485
+ Net.delete_namespace(namespace, host_veth, network, "enp+")
486
+ end
487
+ else
488
+ puts("Isolate: #{$0} isolate <app>")
489
+ puts("Destroy: #{$0} destroy")
490
+ end
458
491
  ```
459
492
 
460
493
  #### PIA VPN Example <a name="pia-vpn-example"></a>
@@ -488,6 +521,11 @@ Net.proxy?
488
521
  Net.proxy_export
489
522
  ```
490
523
 
524
+ ```bash
525
+ # Ping type equivalent that works behind a proxy
526
+ curl -m 3 -sL -w "%{http_code}" google.com -o /dev/null
527
+ ```
528
+
491
529
  ## Pacman Module <a name="pacman-module"></a>
492
530
 
493
531
  ## Process Module <a name="process-module"></a>
@@ -498,6 +536,19 @@ Net.proxy_export
498
536
 
499
537
  ## User Module <a name="user-module"></a>
500
538
 
539
+ ```ruby
540
+ require 'nub'
541
+
542
+ # Check if the user is root
543
+ User.root?
544
+
545
+ # Drop root privileges to regular user that invoked sudo
546
+ User.drop_privileges
547
+
548
+ # Raise privileges back to sudo user if previously dropped
549
+ User.raise_privileges
550
+ ```
551
+
501
552
  ## Ruby Gem Creation <a name="ruby-gem-creation"></a>
502
553
  http://guides.rubygems.org/make-your-own-gem/
503
554
 
data/lib/nub/:w ADDED
@@ -0,0 +1,374 @@
1
+ #!/usr/bin/env ruby
2
+ #MIT License
3
+ #Copyright (c) 2018 phR0ze
4
+ #
5
+ #Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ #of this software and associated documentation files (the "Software"), to deal
7
+ #in the Software without restriction, including without limitation the rights
8
+ #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ #copies of the Software, and to permit persons to whom the Software is
10
+ #furnished to do so, subject to the following conditions:
11
+ #
12
+ #The above copyright notice and this permission notice shall be included in all
13
+ #copies or substantial portions of the Software.
14
+ #
15
+ #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ #SOFTWARE.
22
+
23
+ require 'ostruct'
24
+ require 'ipaddr'
25
+ require_relative 'log'
26
+ require_relative 'sys'
27
+ require_relative 'module'
28
+
29
+ # Collection of network related helpers
30
+ module Net
31
+ extend self
32
+ mattr_accessor(:agents)
33
+ mattr_accessor(:namespace_subnet, :namespce_cidr)
34
+
35
+ @@namespace_cidr = "24"
36
+ @@namespace_subnet = "192.168.100.0"
37
+
38
+ @@agents = OpenStruct.new({
39
+ windows_ie_6: 'Windows IE 6',
40
+ windows_ie_7: 'Windows IE 7',
41
+ windows_mozilla: 'Windows Mozilla',
42
+ mac_safari: 'Mac Safari',
43
+ mac_firefox: 'Mac FireFox',
44
+ mac_mozilla: 'Mac Mozilla',
45
+ linux_mozilla: 'Linux Mozilla',
46
+ linux_firefox: 'Linux Firefox',
47
+ linux_konqueror: 'Linux Konqueror',
48
+ iphone: 'iPhone'
49
+ })
50
+
51
+ # Get fresh proxy from environment
52
+ def proxy
53
+ return OpenStruct.new({
54
+ ftp: ENV['ftp_proxy'],
55
+ http: ENV['http_proxy'],
56
+ https: ENV['https_proxy'],
57
+ no: ENV['no_proxy'],
58
+ uri: ENV['http_proxy'] ? ENV['http_proxy'].split(':')[0..-2] * ":" : nil,
59
+ port: ENV['http_proxy'] ? ENV['http_proxy'].split(':').last : nil
60
+ })
61
+ end
62
+
63
+ # Check if a proxy is set
64
+ def proxy?
65
+ return !self.proxy.http.nil?
66
+ end
67
+
68
+ # Get a shell export string for proxies
69
+ # @param proxy [String] to use rather than default
70
+ def proxy_export(*args)
71
+ proxy = args.any? ? args.first.to_s : nil
72
+ if proxy
73
+ ({'ftp_proxy' => proxy,
74
+ 'http_proxy' => proxy,
75
+ 'https_proxy' => proxy
76
+ }.map{|k,v| "export #{k}=#{v}"} * ';') + ";"
77
+ elsif self.proxy?
78
+ (self.proxy.to_h.map{|k,v| (![:uri, :port].include?(k) && v) ? "export #{k}_proxy=#{v}" : nil}.compact * ';') + ";"
79
+ else
80
+ return nil
81
+ end
82
+ end
83
+
84
+ # Check if the system is configured for the kernel to forward ip traffic
85
+ def ip_forward?
86
+ return File.read('/proc/sys/net/ipv4/ip_forward').include?('1')
87
+ end
88
+
89
+ # Determine the primary nic on the machine
90
+ # based off default routing to google.com
91
+ # @returns [String] nic identified as primary
92
+ def primary_nic
93
+ out = `ip route`
94
+ return out[/default via.*dev (.*) proto/, 1]
95
+ end
96
+
97
+ # Increment the given ip address
98
+ # @param ip [String] ip address to increment
99
+ # @param i [int] optionally increment by given number
100
+ # @returns [String] incremented ip
101
+ def ipinc(ip, *args)
102
+ i = args.any? ? args.first.to_i : 1
103
+ ip_i = IPAddr.new(ip).to_i + i
104
+ return [24, 16, 8, 0].collect{|x| (ip_i >> x) & 255}.join('.')
105
+ end
106
+
107
+ # Decrement the given ip address
108
+ # @param ip [String] ip address to decrement
109
+ # @param i [int] optionally decrement by given number
110
+ # @returns [String] decremented ip
111
+ def ipdec(ip, *args)
112
+ i = args.any? ? args.first.to_i : 1
113
+ ip_i = IPAddr.new(ip).to_i - i
114
+ return [24, 16, 8, 0].collect{|x| (ip_i >> x) & 255}.join('.')
115
+ end
116
+
117
+ # ----------------------------------------------------------------------------
118
+ # Namespace related helpers
119
+ # ----------------------------------------------------------------------------
120
+
121
+ # Virtual Ethernet NIC object
122
+ # @param name [String] of the veth e.g. veth1
123
+ # @param ip [String] of the veth e.g. 192.168.100.1
124
+ Veth = Struct.new(:name, :ip)
125
+
126
+ # Network object
127
+ # @param subnet [String] of the network e.g. 192.168.100.0
128
+ # @param cidr [String] of the network
129
+ # @param nic [String] to use for NAT etc...
130
+ # @param nameservers [Array[String]] to optionally use for new network else uses hosts
131
+ Network = Struct.new(:subnet, :cidr, :nic, :nameservers)
132
+
133
+ # Get all namespaces
134
+ # @returns [Array] of namespace names
135
+ def namespaces
136
+ return Dir[File.join("/var/run/netns", "*")].map{|x| File.basename(x)}
137
+ end
138
+
139
+ # Get the current nameservers in use
140
+ # @param filename [String] to use instead of /etc/resolv.conf
141
+ # @returns [Array] of name server ips
142
+ def nameservers(*args)
143
+ filename = args.any? ? args.first.to_s : '/etc/resolv.conf'
144
+
145
+ result = []
146
+ if File.file?(filename)
147
+ File.readlines(filename).each{|line|
148
+ if line[/nameserver/]
149
+ result << line[/nameserver\s+(.*)/, 1]
150
+ end
151
+ }
152
+ end
153
+ return result
154
+ end
155
+
156
+ # Check that the namespace has connectivity to the outside world
157
+ # using a simple curl on google
158
+ # @param namespace [String] name to use when creating it
159
+ # @param target [String] ip or dns name to use for check
160
+ # @param proxy [String] to use rather than default
161
+ def namespace_connectivity?(namespace, target, *args)
162
+ success = false
163
+ proxy = args.any? ? args.first.to_s : nil
164
+ Log.info("Checking namespace #{namespace.colorize(:cyan)} for connectivity to #{target}", newline:false)
165
+
166
+ if self.namespaces.include?(namespace)
167
+ ping = "curl -m 3 -sL -w \"%{http_code}\" #{target} -o /dev/null"
168
+ return Sys.exec_status("ip netns exec #{namespace} bash -c '#{self.proxy_export(proxy)}#{ping}'", die:false, check:"200")
169
+ else
170
+ Sys.exec_status(":", die:false, check:"200")
171
+ Log.warn("Namespace #{namespace} doesn't exist!")
172
+ end
173
+ end
174
+
175
+ # Get namespace details using defaults for missing arguments
176
+ # veth names are generated incrementally using the '<ns>_vethN' naming pattern
177
+ # veth ips are generated based off @@namespace_subnet/@@namespace_cidr incrementally
178
+ # network subnet and cidr default and namespaces and nic are looked up
179
+ # @param namespace [String] name to use when creating it
180
+ # @returns [host_veth, guest_veth, network]
181
+ def namespace_details(namespace, *args)
182
+ host_veth, guest_veth = Veth.new, Veth.new
183
+ network = Network.new(@@namespace_subnet, @@namespace_cidr, true)
184
+
185
+ # Rebuild namespace objects from disk where possible
186
+ if self.namespaces.include?(namespace)
187
+
188
+ # Rebuild veths from guest
189
+ out = `ip netns exec #{namespace} ip a show type veth`
190
+ if hostif = out[/([\d]+):\s+.*@if[\d]+/, 1] ? "if" + out[/([\d]+):\s+.*@if[\d]+/, 1] : nil
191
+ guest_veth.name = out[/ (.*)@if[\d]+/, 1]
192
+ guest_veth.ip = out[/inet\s+([\d]+\.[\d]+\.[\d]+\.[\d]+).*/, 1]
193
+ host_veth.name = out[/ (.*)@#{hostif}/, 1]
194
+ host_veth.ip = self.ipdec(guest_veth.ip)
195
+
196
+ # Rebuild veths from host
197
+ else
198
+ # out = `ip a show type veth`
199
+ # host_str = out[/inet(.*)#{host_veth.name}/, 1]
200
+ # host_ip = host_str[/\s*([\d]+\.[\d]+\.[\d]+\.[\d]+\/[\d]+).*/, 1] if host_str
201
+ # host_veth.ip = host_ip[/(.*)\/[\d]+/, 1] if host_ip
202
+ end
203
+
204
+ # Rebuild network object
205
+ namespace_conf = File.join("/etc/netns", namespace, "resolv.conf")
206
+ network.nameservers = self.nameservers(namespace_conf) if File.exists?(namespace_conf)
207
+ network.cidr = host_ip[/\/([\d]+)/, 1] if host_ip
208
+ network.subnet = IPAddr.new(host_veth.ip).mask(network.cidr).to_s
209
+ out = `iptables -S`
210
+ if out.include?(host_veth.name)
211
+ network.nic = out[/-A FORWARD -i #{host_veth.name} -o (.*) -j ACCEPT/, 1]
212
+ end
213
+
214
+ # Handle args as either as positional or named
215
+ else
216
+ if args.size == 1 && args.first.is_a?(Hash)
217
+ network = args.first[:network] if args.first.key?(:network)
218
+ host_veth = args.first[:host_veth] if args.first.key?(:host_veth)
219
+ guest_veth = args.first[:guest_veth] if args.first.key?(:guest_veth)
220
+ elsif args.size > 1
221
+ host_veth = args.shift
222
+ guest_veth = args.shift if args.any?
223
+ network = args.shift if args.any?
224
+ end
225
+ end
226
+
227
+ # Populate missing information
228
+ nss = self.namespaces
229
+ i = (nss.include?(namespace) ? nss.size - 1 : nss.size) * 2 + 1
230
+ host_veth.name = "#{namespace}_veth#{i}" if !host_veth.name
231
+ host_veth.ip = self.ipinc(@@namespace_subnet, i) if !host_veth.ip
232
+ guest_veth.name = "#{namespace}_veth#{i + 1}" if !guest_veth.name
233
+ guest_veth.ip = "#{self.ipinc(host_veth.ip)}" if !guest_veth.ip
234
+ network.nic = self.primary_nic if network.nic == true
235
+ network.nameservers = self.nameservers if not network.nameservers
236
+
237
+ return host_veth, guest_veth, network
238
+ end
239
+
240
+ # Create a network namespace with the given name
241
+ # Params can be given as ordered positional args or named args
242
+ #
243
+ # @param namespace [String] name to use when creating it
244
+ # @param host_veth [Veth] describes the veth to create for the host side
245
+ # @param guest_veth [Veth] describes the veth to create for the guest side
246
+ # @param network [Network] describes the network to share.
247
+ # If the nic param is nil NAT is not enabled. If nic is true then the primary nic is dynamically
248
+ # looked up else use user given If nameservers are not given the host nameservers will be used
249
+ #def create_namespace(namespace, host_veth, guest_veth, network)
250
+ def create_namespace(namespace, *args)
251
+ host_veth, guest_veth, network = self.namespace_details(namespace, args)
252
+
253
+ # Ensure namespace i.e. /var/run/netns/<namespace> exists
254
+ if !self.namespaces.include?(namespace)
255
+ Log.info("Creating Network Namespace #{namespace.colorize(:cyan)}", newline:false)
256
+ Sys.exec_status("ip netns add #{namespace}")
257
+ end
258
+
259
+ # Ensure loopback device is running inside the pnamespace
260
+ if `ip netns exec #{namespace} ip a`.include?("state DOWN")
261
+ Log.info("Start loopback interface in namespace", newline:false)
262
+ Sys.exec_status("ip netns exec #{namespace} ip link set lo up")
263
+ end
264
+
265
+ # Create a virtual ethernet pair to communicate across namespaces
266
+ # by default they will both be in the root namespace until one is assigned to another
267
+ # e.g. host:192.168.100.1 and guest:192.168.100.2 communicating in network:192.168.100.0
268
+ if !`ip a`.include?(host_veth.name)
269
+ msg = "Create namespace veths #{host_veth.name.colorize(:cyan)} for #{'root'.colorize(:cyan)} "
270
+ msg += "and #{guest_veth.name.colorize(:cyan)} for #{namespace.colorize(:cyan)}"
271
+ Log.info(msg, newline:false)
272
+ Sys.exec_status("ip link add #{host_veth.name} type veth peer name #{guest_veth.name}")
273
+ Log.info("Assign veth #{guest_veth.name.colorize(:cyan)} to namespace #{namespace.colorize(:cyan)}", newline:false)
274
+ Sys.exec_status("ip link set #{guest_veth.name} netns #{namespace}")
275
+ end
276
+
277
+ # Assign IPv4 addresses and start up the new veth interfaces
278
+ # sudo ping #{host_veth.ip} and sudo netns exec #{namespace} ping #{guest_veth.ip} should work now
279
+ if !`ip a`.include?(host_veth.ip)
280
+ Log.info("Assign ip #{host_veth.ip.colorize(:cyan)} and start #{host_veth.name.colorize(:cyan)}", newline:false)
281
+ Sys.exec_status("ifconfig #{host_veth.name} #{File.join(host_veth.ip, network.cidr)} up")
282
+ end
283
+ if !`ip netns exec #{namespace} ip a`.include?(guest_veth.ip)
284
+ Log.info("Assign ip #{guest_veth.ip.colorize(:cyan)} and start #{guest_veth.name.colorize(:cyan)}", newline:false)
285
+ Sys.exec_status("ip netns exec #{namespace} ifconfig #{guest_veth.name} #{File.join(guest_veth.ip, network.cidr)} up")
286
+ end
287
+
288
+ # Configure host veth as guest's default route leaving namespace
289
+ if !`ip netns exec #{namespace} ip route`.include?('default')
290
+ Log.info("Set default route for traffic leaving namespace #{namespace.colorize(:cyan)} to #{host_veth.ip.colorize(:cyan)}", newline:false)
291
+ Sys.exec_status("ip netns exec #{namespace} ip route add default via #{host_veth.ip} dev #{guest_veth.name}")
292
+ end
293
+
294
+ # NAT guest veth behind host veth to share internet access on host with guest
295
+ # Note: to see current forward rules use: sudo iptables -S
296
+ if network.nic
297
+ if !`iptables -t nat -S`.include?(File.join(network.subnet, network.cidr))
298
+ Log.info("Enable NAT on host for namespace #{File.join(network.subnet, network.cidr).colorize(:cyan)}", newline:false)
299
+ Sys.exec_status("iptables -t nat -A POSTROUTING -s #{File.join(network.subnet, network.cidr)} -o #{network.nic} -j MASQUERADE")
300
+ end
301
+ if !`iptables -S`.include?("-A FORWARD -i #{network.nic}")
302
+ Log.info("Allow forwarding to #{namespace.colorize(:cyan)} from #{network.nic.colorize(:cyan)}", newline:false)
303
+ Sys.exec_status("iptables -A FORWARD -i #{network.nic} -o #{host_veth.name} -j ACCEPT")
304
+ end
305
+ if !`iptables -S`.include?("-A FORWARD -i #{host_veth.name}")
306
+ Log.info("Allow forwarding from #{namespace.colorize(:cyan)} to #{network.nic.colorize(:cyan)}", newline:false)
307
+ Sys.exec_status("iptables -A FORWARD -i #{host_veth.name} -o #{network.nic} -j ACCEPT")
308
+ end
309
+ end
310
+
311
+ # Configure nameserver to use in Network namespace
312
+ namespace_conf = File.join("/etc/netns", namespace)
313
+ if !File.exists?(namespace_conf) && network.nameservers
314
+ Log.info("Creating nameserver config #{namespace_conf}", newline:false)
315
+ Sys.exec_status("mkdir -p #{namespace_conf}")
316
+ network.nameservers.each{|x|
317
+ Log.info("Adding nameserver #{x.colorize(:cyan)} to config", newline:false)
318
+ Sys.exec_status("echo 'nameserver #{x}' >> /etc/netns/#{namespace}/resolv.conf")
319
+ }
320
+ end
321
+ end
322
+
323
+ # Delete the given network namespace
324
+ # @param namespace [String] name to use when creating it
325
+ def delete_namespace(namespace)
326
+ host_veth, guest_veth, network = self.namespace_details(namespace)
327
+
328
+ # Remove nameserver config for network namespace
329
+ namespace_conf = File.join("/etc/netns", namespace)
330
+ if File.exists?(namespace_conf)
331
+ Log.info("Removing nameserver config #{namespace_conf.colorize(:cyan)}", newline:false)
332
+ Sys.exec_status("rm -rf #{namespace_conf}")
333
+ end
334
+
335
+ # Remove NAT and iptables forwarding allowances
336
+ if network.nic
337
+ if `iptables -t nat -S`.include?(File.join(network.subnet, network.cidr))
338
+ Log.info("Removing NAT on host for namespace #{File.join(network.subnet, network.cidr).colorize(:cyan)}", newline:false)
339
+ Sys.exec_status("iptables -t nat -D POSTROUTING -s #{File.join(network.subnet, network.cidr)} -o #{network.nic} -j MASQUERADE")
340
+ end
341
+ if `iptables -S`.include?("-A FORWARD -i #{network.nic}")
342
+ Log.info("Remove forwarding to #{namespace.colorize(:cyan)} from #{host_veth.ip.colorize(:cyan)}", newline:false)
343
+ Sys.exec_status("iptables -D FORWARD -i #{network.nic} -o #{host_veth.name} -j ACCEPT")
344
+ end
345
+ if `iptables -S`.include?("-A FORWARD -i #{host_veth.name}")
346
+ Log.info("Remove forwarding from #{namespace.colorize(:cyan)} to #{host_veth.ip.colorize(:cyan)}", newline:false)
347
+ Sys.exec_status("iptables -D FORWARD -i #{host_veth.name} -o #{network.nic} -j ACCEPT")
348
+ end
349
+ end
350
+
351
+ # Remove veths (virtual ethernet interfaces)
352
+ if `ip a`.include?(host_veth.name)
353
+ Log.info("Removing veth interface #{host_veth.name.colorize(:cyan)} for namespace", newline:false)
354
+ Sys.exec_status("ip link delete #{host_veth.name}")
355
+ end
356
+
357
+ # Remove namespace
358
+ if self.namespaces.include?(namespace)
359
+ Log.info("Removing namespace #{namespace.colorize(:cyan)}", newline:false)
360
+ Sys.exec_status("ip netns delete #{namespace}")
361
+ end
362
+ end
363
+
364
+ # Execute in the namespace
365
+ # @param namespace [String] to execut within
366
+ # @param cmd [String] command to execute
367
+ # @param proxy [String] to use rather than default
368
+ def namespace_exec(namespace, cmd, *args)
369
+ proxy = args.any? ? args.first.to_s : nil
370
+ return `ip netns exec #{namespace} bash -c '#{self.proxy_export(proxy)}#{cmd}'`
371
+ end
372
+ end
373
+
374
+ # vim: ft=ruby:ts=2:sw=2:sts=2
data/lib/nub/net.rb CHANGED
@@ -21,6 +21,7 @@
21
21
  #SOFTWARE.
22
22
 
23
23
  require 'ostruct'
24
+ require 'ipaddr'
24
25
  require_relative 'log'
25
26
  require_relative 'sys'
26
27
  require_relative 'module'
@@ -29,6 +30,10 @@ require_relative 'module'
29
30
  module Net
30
31
  extend self
31
32
  mattr_accessor(:agents)
33
+ mattr_accessor(:namespace_subnet, :namespce_cidr)
34
+
35
+ @@namespace_cidr = "24"
36
+ @@namespace_subnet = "192.168.100.0"
32
37
 
33
38
  @@agents = OpenStruct.new({
34
39
  windows_ie_6: 'Windows IE 6',
@@ -62,7 +67,8 @@ module Net
62
67
 
63
68
  # Get a shell export string for proxies
64
69
  # @param proxy [String] to use rather than default
65
- def proxy_export(proxy:nil)
70
+ def proxy_export(*args)
71
+ proxy = args.any? ? args.first.to_s : nil
66
72
  if proxy
67
73
  ({'ftp_proxy' => proxy,
68
74
  'http_proxy' => proxy,
@@ -80,32 +86,85 @@ module Net
80
86
  return File.read('/proc/sys/net/ipv4/ip_forward').include?('1')
81
87
  end
82
88
 
89
+ # Determine the primary nic on the machine
90
+ # based off default routing to google.com
91
+ # @returns [String] nic identified as primary
92
+ def primary_nic
93
+ out = `ip route`
94
+ return out[/default via.*dev (.*) proto/, 1]
95
+ end
96
+
97
+ # Increment the given ip address
98
+ # @param ip [String] ip address to increment
99
+ # @param i [int] optionally increment by given number
100
+ # @returns [String] incremented ip
101
+ def ipinc(ip, *args)
102
+ i = args.any? ? args.first.to_i : 1
103
+ ip_i = IPAddr.new(ip).to_i + i
104
+ return [24, 16, 8, 0].collect{|x| (ip_i >> x) & 255}.join('.')
105
+ end
106
+
107
+ # Decrement the given ip address
108
+ # @param ip [String] ip address to decrement
109
+ # @param i [int] optionally decrement by given number
110
+ # @returns [String] decremented ip
111
+ def ipdec(ip, *args)
112
+ i = args.any? ? args.first.to_i : 1
113
+ ip_i = IPAddr.new(ip).to_i - i
114
+ return [24, 16, 8, 0].collect{|x| (ip_i >> x) & 255}.join('.')
115
+ end
116
+
83
117
  # ----------------------------------------------------------------------------
84
118
  # Namespace related helpers
85
119
  # ----------------------------------------------------------------------------
86
120
 
87
121
  # Virtual Ethernet NIC object
88
- # @param name [String] of the veth
89
- # @param ip [String] of the veth
90
- # @param nic [String] pattern matching nic e.g. en+
91
- Veth = Struct.new(:name, :ip, :nic)
122
+ # @param name [String] of the veth e.g. veth1
123
+ # @param ip [String] of the veth e.g. 192.168.100.1
124
+ Veth = Struct.new(:name, :ip)
92
125
 
93
126
  # Network object
94
- # @param ip [String] of the network
127
+ # @param subnet [String] of the network e.g. 192.168.100.0
95
128
  # @param cidr [String] of the network
96
- # @param nameservers [Array[String]] to use for new network
97
- Network = Struct.new(:ip, :cidr, :nameservers)
129
+ # @param nic [String] to use for NAT etc...
130
+ # @param nameservers [Array[String]] to optionally use for new network else uses hosts
131
+ Network = Struct.new(:subnet, :cidr, :nic, :nameservers)
132
+
133
+ # Get all namespaces
134
+ # @returns [Array] of namespace names
135
+ def namespaces
136
+ return Dir[File.join("/var/run/netns", "*")].map{|x| File.basename(x)}
137
+ end
138
+
139
+ # Get the current nameservers in use
140
+ # @param filename [String] to use instead of /etc/resolv.conf
141
+ # @returns [Array] of name server ips
142
+ def nameservers(*args)
143
+ filename = args.any? ? args.first.to_s : '/etc/resolv.conf'
144
+
145
+ result = []
146
+ if File.file?(filename)
147
+ File.readlines(filename).each{|line|
148
+ if line[/nameserver/]
149
+ result << line[/nameserver\s+(.*)/, 1]
150
+ end
151
+ }
152
+ end
153
+ return result
154
+ end
98
155
 
99
156
  # Check that the namespace has connectivity to the outside world
100
157
  # using a simple curl on google
101
158
  # @param namespace [String] name to use when creating it
159
+ # @param target [String] ip or dns name to use for check
102
160
  # @param proxy [String] to use rather than default
103
- def namespace_connectivity?(namespace, proxy:nil)
161
+ def namespace_connectivity?(namespace, target, *args)
104
162
  success = false
105
- Log.info("Checking namespace #{namespace.colorize(:cyan)} for connectivity to google.com", newline:false)
163
+ proxy = args.any? ? args.first.to_s : nil
164
+ Log.info("Checking namespace #{namespace.colorize(:cyan)} for connectivity to #{target}", newline:false)
106
165
 
107
- if File.exists?(File.join("/var/run/netns", namespace))
108
- ping = 'curl -sL -w "%{http_code}" http://www.google.com -o /dev/null'
166
+ if self.namespaces.include?(namespace)
167
+ ping = "curl -m 3 -sL -w \"%{http_code}\" #{target} -o /dev/null"
109
168
  return Sys.exec_status("ip netns exec #{namespace} bash -c '#{self.proxy_export(proxy)}#{ping}'", die:false, check:"200")
110
169
  else
111
170
  Sys.exec_status(":", die:false, check:"200")
@@ -113,17 +172,55 @@ module Net
113
172
  end
114
173
  end
115
174
 
175
+ # Get namespace details using defaults for missing arguments
176
+ # veth names are generated using the '<ns>_<type>' naming pattern
177
+ # veth ips are generated based off @@namespace_subnet/@@namespace_cidr incrementally
178
+ # network subnet and cidr default and namespaces and nic are looked up
179
+ # @param namespace [String] name to use when creating it
180
+ # @returns [host_veth, guest_veth, network]
181
+ def namespace_details(namespace, *args)
182
+ host_veth, guest_veth = Veth.new, Veth.new
183
+ network = Network.new(@@namespace_subnet, @@namespace_cidr, true)
184
+
185
+ # Handle args as either as positional or named
186
+ if args.size == 1 && args.first.is_a?(Hash)
187
+ network = args.first[:network] if args.first.key?(:network)
188
+ host_veth = args.first[:host_veth] if args.first.key?(:host_veth)
189
+ guest_veth = args.first[:guest_veth] if args.first.key?(:guest_veth)
190
+ elsif args.size > 1
191
+ host_veth = args.shift
192
+ guest_veth = args.shift if args.any?
193
+ network = args.shift if args.any?
194
+ end
195
+
196
+ # Populate missing information
197
+ nss = self.namespaces
198
+ i = (nss.include?(namespace) ? nss.size - 1 : nss.size) * 2 + 1
199
+ host_veth.name = "#{namespace}_host" if !host_veth.name
200
+ host_veth.ip = self.ipinc(@@namespace_subnet, i) if !host_veth.ip
201
+ guest_veth.name = "#{namespace}_guest" if !guest_veth.name
202
+ guest_veth.ip = "#{self.ipinc(host_veth.ip)}" if !guest_veth.ip
203
+ network.nic = self.primary_nic if network.nic == true
204
+ network.nameservers = self.nameservers if not network.nameservers
205
+
206
+ return host_veth, guest_veth, network
207
+ end
208
+
116
209
  # Create a network namespace with the given name
210
+ # Params can be given as ordered positional args or named args
211
+ #
117
212
  # @param namespace [String] name to use when creating it
118
213
  # @param host_veth [Veth] describes the veth to create for the host side
119
214
  # @param guest_veth [Veth] describes the veth to create for the guest side
120
- # @param network [Network] describes the network to share
121
- def create_namespace(namespace, host_veth, guest_veth, network)
122
- namespace_conf = File.join("/etc/netns", namespace)
215
+ # @param network [Network] describes the network to share.
216
+ # If the nic param is nil NAT is not enabled. If nic is true then the primary nic is dynamically
217
+ # looked up else use user given If nameservers are not given the host nameservers will be used
218
+ def create_namespace(namespace, *args)
219
+ host_veth, guest_veth, network = self.namespace_details(namespace, args)
123
220
 
124
221
  # Ensure namespace i.e. /var/run/netns/<namespace> exists
125
- if !File.exists?(File.join("/var/run/netns", namespace))
126
- Log.info("Creating VPN Namespace #{namespace.colorize(:cyan)}", newline:false)
222
+ if !self.namespaces.include?(namespace)
223
+ Log.info("Creating Network Namespace #{namespace.colorize(:cyan)}", newline:false)
127
224
  Sys.exec_status("ip netns add #{namespace}")
128
225
  end
129
226
 
@@ -137,8 +234,9 @@ module Net
137
234
  # by default they will both be in the root namespace until one is assigned to another
138
235
  # e.g. host:192.168.100.1 and guest:192.168.100.2 communicating in network:192.168.100.0
139
236
  if !`ip a`.include?(host_veth.name)
140
- Log.info("Create vpn veths #{host_veth.name.colorize(:cyan)} for #{'root'.colorize(:cyan)}
141
- and #{guest_veth.name.colorize(:cyan)} for #{namespace.colorize(:cyan)}", newline:false)
237
+ msg = "Create namespace veths #{host_veth.name.colorize(:cyan)} for #{'root'.colorize(:cyan)} "
238
+ msg += "and #{guest_veth.name.colorize(:cyan)} for #{namespace.colorize(:cyan)}"
239
+ Log.info(msg, newline:false)
142
240
  Sys.exec_status("ip link add #{host_veth.name} type veth peer name #{guest_veth.name}")
143
241
  Log.info("Assign veth #{guest_veth.name.colorize(:cyan)} to namespace #{namespace.colorize(:cyan)}", newline:false)
144
242
  Sys.exec_status("ip link set #{guest_veth.name} netns #{namespace}")
@@ -147,34 +245,38 @@ module Net
147
245
  # Assign IPv4 addresses and start up the new veth interfaces
148
246
  # sudo ping #{host_veth.ip} and sudo netns exec #{namespace} ping #{guest_veth.ip} should work now
149
247
  if !`ip a`.include?(host_veth.ip)
150
- Log.info("Assign ip #{host_veth.ip.colorize(:cyan)} and start #{host_veth.ip.colorize(:cyan)}", newline:false)
151
- Sys.exec_status("ifconfig #{host_veth.name} #{File.join(host_veth.ip, nework.cidr)} up")
248
+ Log.info("Assign ip #{host_veth.ip.colorize(:cyan)} and start #{host_veth.name.colorize(:cyan)}", newline:false)
249
+ Sys.exec_status("ifconfig #{host_veth.name} #{File.join(host_veth.ip, network.cidr)} up")
152
250
  end
153
251
  if !`ip netns exec #{namespace} ip a`.include?(guest_veth.ip)
154
- Log.info("Assign ip #{guest_veth.ip.colorize(:cyan)} and start #{guest_veth.ip.colorize(:cyan)}", newline:false)
155
- Sys.exec_status("ip netns exec #{namespace} ifconfig #{guest_veth.name} #{File.join(guest_veth.ip, nework.cidr)} up")
252
+ Log.info("Assign ip #{guest_veth.ip.colorize(:cyan)} and start #{guest_veth.name.colorize(:cyan)}", newline:false)
253
+ Sys.exec_status("ip netns exec #{namespace} ifconfig #{guest_veth.name} #{File.join(guest_veth.ip, network.cidr)} up")
156
254
  end
157
255
 
158
- # Share internet access on host with namespace
159
- # Note: to see current forward rules use: iptables -S
256
+ # Configure host veth as guest's default route leaving namespace
160
257
  if !`ip netns exec #{namespace} ip route`.include?('default')
161
- Log.info("Set default route for traffic leaving namespace to #{host_veth.ip.colorize(:cyan)}", newline:false)
258
+ Log.info("Set default route for traffic leaving namespace #{namespace.colorize(:cyan)} to #{host_veth.ip.colorize(:cyan)}", newline:false)
162
259
  Sys.exec_status("ip netns exec #{namespace} ip route add default via #{host_veth.ip} dev #{guest_veth.name}")
163
260
  end
164
- if !`iptables -t nat -S`.include?(File.join(nework.ip, nework.cidr))
165
- Log.info("Enable NAT on host for vpn net #{File.join(nework.ip, nework.cidr).colorize(:cyan)}", newline:false)
166
- Sys.exec_status("iptables -t nat -A POSTROUTING -s #{File.join(nework.ip, nework.cidr)} -o #{host_veth.nic} -j MASQUERADE")
167
- end
168
- if !`iptables -S`.include?("-A FORWARD -i #{host_veth.nic}")
169
- Log.info("Allow forwarding to #{namespace.colorize(:cyan)}", newline:false)
170
- Sys.exec_status("iptables -A FORWARD -i #{host_veth.nic} -o #{host_veth.name} -j ACCEPT")
171
- end
172
- if !`iptables -S`.include?("-A FORWARD -i #{host_veth.name}")
173
- Log.info("Allow forwarding from #{namespace.colorize(:cyan)}", newline:false)
174
- Sys.exec_status("iptables -A FORWARD -i #{host_veth.name} -o #{host_veth.nic} -j ACCEPT")
261
+
262
+ # NAT guest veth behind host veth to share internet access on host with guest
263
+ # Note: to see current forward rules use: sudo iptables -S
264
+ if network.nic
265
+ if !`iptables -t nat -S`.include?(File.join(network.subnet, network.cidr))
266
+ Log.info("Enable NAT on host for namespace #{File.join(network.subnet, network.cidr).colorize(:cyan)}", newline:false)
267
+ Sys.exec_status("iptables -t nat -A POSTROUTING -s #{File.join(network.subnet, network.cidr)} -o #{network.nic} -j MASQUERADE")
268
+ end
269
+ if !`iptables -S`.include?("-A FORWARD -i #{network.nic}")
270
+ Log.info("Allow forwarding to #{namespace.colorize(:cyan)} from #{network.nic.colorize(:cyan)}", newline:false)
271
+ Sys.exec_status("iptables -A FORWARD -i #{network.nic} -o #{host_veth.name} -j ACCEPT")
272
+ end
273
+ if !`iptables -S`.include?("-A FORWARD -i #{host_veth.name}")
274
+ Log.info("Allow forwarding from #{namespace.colorize(:cyan)} to #{network.nic.colorize(:cyan)}", newline:false)
275
+ Sys.exec_status("iptables -A FORWARD -i #{host_veth.name} -o #{network.nic} -j ACCEPT")
276
+ end
175
277
  end
176
278
 
177
- # Configure nameserver to use in VPN namespace
279
+ # Configure nameserver to use in Network namespace
178
280
  namespace_conf = File.join("/etc/netns", namespace)
179
281
  if !File.exists?(namespace_conf) && network.nameservers
180
282
  Log.info("Creating nameserver config #{namespace_conf}", newline:false)
@@ -187,40 +289,48 @@ module Net
187
289
  end
188
290
 
189
291
  # Delete the given network namespace
190
- # @param namespace [String] name to use when creating it
292
+ # Params can be given as ordered positional args or named args
293
+ #
294
+ # @param namespace [String] name to use when deleting it
191
295
  # @param host_veth [Veth] describes the veth to create for the host side
192
- # @param network [Network] describes the network to share
193
- def delete_namespace(namespace, host_veth, network)
296
+ # @param guest_veth [Veth] describes the veth to create for the guest side
297
+ # @param network [Network] describes the network to share.
298
+ # If the nic param is nil NAT is not enabled. If nic is true then the primary nic is dynamically
299
+ # looked up else use user given If nameservers are not given the host nameservers will be used
300
+ def delete_namespace(namespace, *args)
301
+ host_veth, guest_veth, network = self.namespace_details(namespace, args)
194
302
 
195
- # Remove nameserver config for vpn
303
+ # Remove nameserver config for network namespace
196
304
  namespace_conf = File.join("/etc/netns", namespace)
197
305
  if File.exists?(namespace_conf)
198
- Log.info("Removing nameserver config #{namespace_conf}", newline:false)
306
+ Log.info("Removing nameserver config #{namespace_conf.colorize(:cyan)}", newline:false)
199
307
  Sys.exec_status("rm -rf #{namespace_conf}")
200
308
  end
201
309
 
202
310
  # Remove NAT and iptables forwarding allowances
203
- if `iptables -t nat -S`.include?(File.join(network.ip, network.cidr))
204
- Log.info("Removing NAT on host for vpn net #{File.join(network.ip, network.cidr).colorize(:cyan)}", newline:false)
205
- Sys.exec_status("iptables -t nat -D POSTROUTING -s #{File.join(network.ip, network.cidr)} -o #{host_veth.nic} -j MASQUERADE")
206
- end
207
- if `iptables -S`.include?("-A FORWARD -i #{host_veth.nic}")
208
- Log.info("Allow forwarding to #{namespace.colorize(:cyan)}", newline:false)
209
- Sys.exec_status("iptables -D FORWARD -i #{host_veth.nic} -o #{host_veth.name} -j ACCEPT")
210
- end
211
- if `iptables -S`.include?("-A FORWARD -i #{host_veth.name}")
212
- Log.info("Allow forwarding from #{namespace.colorize(:cyan)}", newline:false)
213
- Sys.exec_status("iptables -D FORWARD -i #{host_veth.name} -o #{host_veth.nic} -j ACCEPT")
311
+ if network.nic
312
+ if `iptables -t nat -S`.include?(File.join(network.subnet, network.cidr))
313
+ Log.info("Removing NAT on host for namespace #{File.join(network.subnet, network.cidr).colorize(:cyan)}", newline:false)
314
+ Sys.exec_status("iptables -t nat -D POSTROUTING -s #{File.join(network.subnet, network.cidr)} -o #{network.nic} -j MASQUERADE")
315
+ end
316
+ if `iptables -S`.include?("-A FORWARD -i #{network.nic}")
317
+ Log.info("Remove forwarding to #{namespace.colorize(:cyan)} from #{host_veth.ip.colorize(:cyan)}", newline:false)
318
+ Sys.exec_status("iptables -D FORWARD -i #{network.nic} -o #{host_veth.name} -j ACCEPT")
319
+ end
320
+ if `iptables -S`.include?("-A FORWARD -i #{host_veth.name}")
321
+ Log.info("Remove forwarding from #{namespace.colorize(:cyan)} to #{host_veth.ip.colorize(:cyan)}", newline:false)
322
+ Sys.exec_status("iptables -D FORWARD -i #{host_veth.name} -o #{network.nic} -j ACCEPT")
323
+ end
214
324
  end
215
325
 
216
- # Remove virtual ethernet interfaces
326
+ # Remove veths (virtual ethernet interfaces)
217
327
  if `ip a`.include?(host_veth.name)
218
- Log.info("Removing veth interface #{host_veth.name.colorize(:cyan)} for vpn", newline:false)
328
+ Log.info("Removing veth interface #{host_veth.name.colorize(:cyan)} for namespace", newline:false)
219
329
  Sys.exec_status("ip link delete #{host_veth.name}")
220
330
  end
221
331
 
222
- # Remove namespace for vpn
223
- if File.exists?(File.join("/var/run/netns", namespace))
332
+ # Remove namespace
333
+ if self.namespaces.include?(namespace)
224
334
  Log.info("Removing namespace #{namespace.colorize(:cyan)}", newline:false)
225
335
  Sys.exec_status("ip netns delete #{namespace}")
226
336
  end
@@ -230,7 +340,8 @@ module Net
230
340
  # @param namespace [String] to execut within
231
341
  # @param cmd [String] command to execute
232
342
  # @param proxy [String] to use rather than default
233
- def namespace_exec(namespace, cmd, proxy:nil)
343
+ def namespace_exec(namespace, cmd, *args)
344
+ proxy = args.any? ? args.first.to_s : nil
234
345
  return `ip netns exec #{namespace} bash -c '#{self.proxy_export(proxy)}#{cmd}'`
235
346
  end
236
347
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nub
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.103
4
+ version: 0.0.119
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick Crummett
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-11 00:00:00.000000000 Z
11
+ date: 2018-08-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0.8'
47
+ version: 0.8.22
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0.8'
54
+ version: 0.8.22
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: bundler
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -89,6 +89,7 @@ files:
89
89
  - LICENSE
90
90
  - README.md
91
91
  - lib/nub.rb
92
+ - lib/nub/:w
92
93
  - lib/nub/commander.rb
93
94
  - lib/nub/config.rb
94
95
  - lib/nub/core.rb