Module: Net

Extended by:
Net
Included in:
Net
Defined in:
lib/nub/net.rb

Overview

Collection of network related helpers

Defined Under Namespace

Classes: Network, Veth

Constant Summary collapse

@@namespace_cidr =
"24"
@@namespace_subnet =
"192.168.100.0"
@@agents =
OpenStruct.new({
  windows_ie_6: 'Windows IE 6',
  windows_ie_7: 'Windows IE 7',
  windows_mozilla: 'Windows Mozilla',
  mac_safari: 'Mac Safari',
  mac_firefox: 'Mac FireFox',
  mac_mozilla: 'Mac Mozilla',
  linux_mozilla: 'Linux Mozilla',
  linux_firefox: 'Linux Firefox',
  linux_konqueror: 'Linux Konqueror',
  iphone: 'iPhone'
})

Instance Method Summary collapse

Instance Method Details

#create_namespace(namespace, *args) ⇒ Object

Create a network namespace with the given name Params can be given as ordered positional args or named args

Parameters:

  • namespace (String)

    name to use when creating it

  • host_veth (Veth)

    describes the veth to create for the host side

  • guest_veth (Veth)

    describes the veth to create for the guest side

  • network (Network)

    describes the network to share. If the nic param is nil NAT is not enabled. If nic is true then the primary nic is dynamically looked up else use user given If nameservers are not given the host nameservers will be used



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/nub/net.rb', line 284

def create_namespace(namespace, *args)
  host_veth, guest_veth, network = self.namespace_details(namespace, *args)

  # Ensure namespace i.e. /var/run/netns/<namespace> exists
  if !self.namespaces.include?(namespace)
    Log.info("Creating Network Namespace #{namespace.colorize(:cyan)}", newline:false)
    Sys.exec_status("ip netns add #{namespace}")
  end

  # Ensure loopback device is running inside the pnamespace
  if `ip netns exec #{namespace} ip a`.include?("state DOWN")
    Log.info("Start loopback interface in namespace", newline:false)
    Sys.exec_status("ip netns exec #{namespace} ip link set lo up")
  end

  # Create a virtual ethernet pair to communicate across namespaces
  # by default they will both be in the root namespace until one is assigned to another
  # e.g. host:192.168.100.1 and guest:192.168.100.2 communicating in network:192.168.100.0
  if !`ip a`.include?(host_veth.name)
    msg = "Create namespace veths #{host_veth.name.colorize(:cyan)} for #{'root'.colorize(:cyan)} "
    msg += "and #{guest_veth.name.colorize(:cyan)} for #{namespace.colorize(:cyan)}"
    Log.info(msg, newline:false)
    Sys.exec_status("ip link add #{host_veth.name} type veth peer name #{guest_veth.name}")
    Log.info("Assign veth #{guest_veth.name.colorize(:cyan)} to namespace #{namespace.colorize(:cyan)}", newline:false)
    Sys.exec_status("ip link set #{guest_veth.name} netns #{namespace}")
  end

  # Assign IPv4 addresses and start up the new veth interfaces
  # sudo ping #{host_veth.ip} and sudo netns exec #{namespace} ping #{guest_veth.ip} should work now
  if !`ip a`.include?(host_veth.ip)
    Log.info("Assign ip #{host_veth.ip.colorize(:cyan)} and start #{host_veth.name.colorize(:cyan)}", newline:false)
    Sys.exec_status("ifconfig #{host_veth.name} #{File.join(host_veth.ip, network.cidr)} up")
  end
  if !`ip netns exec #{namespace} ip a`.include?(guest_veth.ip)
    Log.info("Assign ip #{guest_veth.ip.colorize(:cyan)} and start #{guest_veth.name.colorize(:cyan)}", newline:false)
    Sys.exec_status("ip netns exec #{namespace} ifconfig #{guest_veth.name} #{File.join(guest_veth.ip, network.cidr)} up")
  end

  # Configure host veth as guest's default route leaving namespace
  if !`ip netns exec #{namespace} ip route`.include?('default')
    Log.info("Set default route for traffic leaving namespace #{namespace.colorize(:cyan)} to #{host_veth.ip.colorize(:cyan)}", newline:false)
    Sys.exec_status("ip netns exec #{namespace} ip route add default via #{host_veth.ip} dev #{guest_veth.name}")
  end

  # NAT guest veth behind host veth to share internet access on host with guest
  # Note: to see current forward rules use: sudo iptables -S
  if network.nic
    if !`iptables -t nat -S`.include?(File.join(network.subnet, network.cidr))
      Log.info("Enable NAT on host for namespace #{File.join(network.subnet, network.cidr).colorize(:cyan)}", newline:false)
      Sys.exec_status("iptables -t nat -A POSTROUTING -s #{File.join(network.subnet, network.cidr)} -o #{network.nic} -j MASQUERADE")
    end
    if !`iptables -S`.include?("-A FORWARD -i #{network.nic}")
      Log.info("Allow forwarding to #{namespace.colorize(:cyan)} from #{network.nic.colorize(:cyan)}", newline:false)
      Sys.exec_status("iptables -A FORWARD -i #{network.nic} -o #{host_veth.name} -j ACCEPT")
    end
    if !`iptables -S`.include?("-A FORWARD -i #{host_veth.name}")
      Log.info("Allow forwarding from #{namespace.colorize(:cyan)} to #{network.nic.colorize(:cyan)}", newline:false)
      Sys.exec_status("iptables -A FORWARD -i #{host_veth.name} -o #{network.nic} -j ACCEPT")
    end
  end

  # Configure nameserver to use in Network namespace
  namespace_conf = File.join("/etc/netns", namespace)
  if !File.exists?(namespace_conf) && network.nameservers
    Log.info("Creating nameserver config #{namespace_conf}", newline:false)
    Sys.exec_status("mkdir -p #{namespace_conf}")
    network.nameservers.each{|x|
      Log.info("Adding nameserver #{x.colorize(:cyan)} to config", newline:false)
      Sys.exec_status("echo 'nameserver #{x}' >> /etc/netns/#{namespace}/resolv.conf")
    }
  end
end

#delete_namespace(namespace, *args) ⇒ Object

Delete the given network namespace Params can be given as ordered positional args or named args

Parameters:

  • namespace (String)

    name to use when deleting it

  • host_veth (Veth)

    describes the veth to create for the host side

  • guest_veth (Veth)

    describes the veth to create for the guest side

  • network (Network)

    describes the network to share. If the nic param is nil NAT is not enabled. If nic is true then the primary nic is dynamically looked up else use user given If nameservers are not given the host nameservers will be used



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/nub/net.rb', line 366

def delete_namespace(namespace, *args)
  host_veth, guest_veth, network = self.namespace_details(namespace, *args)

  # Remove nameserver config for network namespace
  namespace_conf = File.join("/etc/netns", namespace)
  if File.exists?(namespace_conf)
    Log.info("Removing nameserver config #{namespace_conf.colorize(:cyan)}", newline:false)
    Sys.exec_status("rm -rf #{namespace_conf}")
  end

  # Remove NAT and iptables forwarding allowances
  if network.nic
    if `iptables -t nat -S`.include?(File.join(network.subnet, network.cidr))
      Log.info("Removing NAT on host for namespace #{File.join(network.subnet, network.cidr).colorize(:cyan)}", newline:false)
      Sys.exec_status("iptables -t nat -D POSTROUTING -s #{File.join(network.subnet, network.cidr)} -o #{network.nic} -j MASQUERADE")
    end
    if `iptables -S`.include?("-A FORWARD -i #{network.nic} -o #{host_veth.name}")
      Log.info("Remove forwarding to #{namespace.colorize(:cyan)} from #{host_veth.ip.colorize(:cyan)}", newline:false)
      Sys.exec_status("iptables -D FORWARD -i #{network.nic} -o #{host_veth.name} -j ACCEPT")
    end
    if `iptables -S`.include?("-A FORWARD -i #{host_veth.name} -o #{network.nic}")
      Log.info("Remove forwarding from #{namespace.colorize(:cyan)} to #{host_veth.ip.colorize(:cyan)}", newline:false)
      Sys.exec_status("iptables -D FORWARD -i #{host_veth.name} -o #{network.nic} -j ACCEPT")
    end
  end

  # Remove veths (virtual ethernet interfaces)
  if `ip a`.include?(host_veth.name)
    Log.info("Removing veth interface #{host_veth.name.colorize(:cyan)} for namespace", newline:false)
    Sys.exec_status("ip link delete #{host_veth.name}")
  end

  # Remove namespace
  if self.namespaces.include?(namespace)
    Log.info("Removing namespace #{namespace.colorize(:cyan)}", newline:false)
    Sys.exec_status("ip netns delete #{namespace}")
  end
end

#ip_forward?Boolean

Check if the system is configured for the kernel to forward ip traffic

Returns:

  • (Boolean)


87
88
89
# File 'lib/nub/net.rb', line 87

def ip_forward?
  return File.read('/proc/sys/net/ipv4/ip_forward').include?('1')
end

#ipdec(ip, *args) ⇒ Object

Decrement the given ip address

Parameters:

  • ip (String)

    ip address to decrement

  • i (int)

    optionally decrement by given number



113
114
115
116
117
# File 'lib/nub/net.rb', line 113

def ipdec(ip, *args)
  i = args.any? ? args.first.to_i : 1
  ip_i = IPAddr.new(ip).to_i - i
  return [24, 16, 8, 0].collect{|x| (ip_i >> x) & 255}.join('.')
end

#ipinc(ip, *args) ⇒ Object

Increment the given ip address

Parameters:

  • ip (String)

    ip address to increment

  • i (int)

    optionally increment by given number



103
104
105
106
107
# File 'lib/nub/net.rb', line 103

def ipinc(ip, *args)
  i = args.any? ? args.first.to_i : 1
  ip_i = IPAddr.new(ip).to_i + i
  return [24, 16, 8, 0].collect{|x| (ip_i >> x) & 255}.join('.')
end

#nameservers(*args) ⇒ Object

Get the current nameservers in use

Parameters:

  • filename (String)

    to use instead of /etc/resolv.conf



162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/nub/net.rb', line 162

def nameservers(*args)
  filename = args.any? ? args.first.to_s : '/etc/resolv.conf'
  filename = '/etc/resolv.conf' if !File.file?(filename)

  result = []
  if File.file?(filename)
    File.readlines(filename).each{|line|
      if line[/nameserver/]
        result << line[/nameserver\s+(.*)/, 1]
      end
    }
  end
  return result
end

#namespace_connectivity?(namespace, target, *args) ⇒ Boolean

Check that the namespace has connectivity to the outside world using a simple curl on google

Parameters:

  • namespace (String)

    name to use when creating it

  • target (String)

    ip or dns name to use for check

  • proxy (String)

    to use rather than default

Returns:

  • (Boolean)


182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/nub/net.rb', line 182

def namespace_connectivity?(namespace, target, *args)
  success = false
  proxy = args.any? ? args.first.to_s : nil
  Log.info("Checking namespace #{namespace.colorize(:cyan)} for connectivity to #{target}", newline:false)

  if self.namespaces.include?(namespace)
    ping = "curl -m 3 -sL -w \"%{http_code}\" #{target} -o /dev/null"
    return Sys.exec_status("ip netns exec #{namespace} bash -c '#{self.proxy_export(proxy)}#{ping}'", die:false, check:"200")
  else
    Sys.exec_status(":", die:false, check:"200")
    Log.warn("Namespace #{namespace} doesn't exist!")
  end
end

#namespace_details(namespace, *args) ⇒ Object

Get namespace details using defaults for missing arguments veth names are generated using the ‘<ns>_<type>’ naming pattern veth ips are generated based off @@namespace_subnet/@@namespace_cidr incrementally network subnet and cidr default and namespaces and nic are looked up

Parameters:

  • namespace (String)

    name to use for details



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/nub/net.rb', line 237

def namespace_details(namespace, *args)
  host_veth, guest_veth = Veth.new, Veth.new
  network = Network.new(@@namespace_subnet, @@namespace_cidr, true)

  # Pull from existing namespace first
  if self.namespaces.include?(namespace)
    network.nameservers = self.nameservers("/etc/netns/#{namespace}/resolv.conf")
    host_veth, guest_veth = self.namespace_veths(namespace)

  # Handle args as either as positional or named
  else
    if args.size == 1 && args.first.is_a?(Hash)
      network = args.first[:network] if args.first.key?(:network)
      host_veth = args.first[:host_veth] if args.first.key?(:host_veth)
      guest_veth = args.first[:guest_veth] if args.first.key?(:guest_veth)
    elsif args.any?
      host_veth = args.shift
      guest_veth = args.shift if args.any?
      network = args.shift if args.any?
    end
  end

  # Populate missing information
  host_veth.name = "#{namespace}_host" if !host_veth.name
  guest_veth.name = "#{namespace}_guest" if !guest_veth.name
  network.subnet = @@namespace_subnet if !network.subnet
  network.cidr = @@namespace_cidr if !network.cidr
  network.nic = self.primary_nic if network.nic.nil? || network.nic == true
  network.nameservers = self.nameservers if !network.nameservers
  if !host_veth.ip or !guest_veth.ip
    host_ip, guest_ip = self.namespace_next_veth_ips
    host_veth.ip = host_ip if !host_veth.ip
    guest_veth.ip = guest_ip if !guest_veth.ip
  end

  return host_veth, guest_veth, network
end

#namespace_exec(namespace, cmd, *args) ⇒ Object

Execute in the namespace

Parameters:

  • namespace (String)

    to execut within

  • cmd (String)

    command to execute

  • proxy (String)

    to use rather than default



409
410
411
412
# File 'lib/nub/net.rb', line 409

def namespace_exec(namespace, cmd, *args)
  proxy = args.any? ? args.first.to_s : nil
  return `ip netns exec #{namespace} bash -c '#{self.proxy_export(proxy)}#{cmd}'`
end

#namespace_next_veth_ipsObject

Get next available pair of veth ips



223
224
225
226
227
228
229
# File 'lib/nub/net.rb', line 223

def namespace_next_veth_ips
  used = []
  self.namespaces.each{|ns|
    used += self.namespace_veths(ns).select{|x| x.ip}.map{|x| x.ip.split('.').last.to_i}
  }
  return ((1..255).to_a - used)[0..1].map{|x| self.ipinc(@@namespace_subnet, x)}
end

#namespace_veths(namespace) ⇒ Object

Get veths for namespace

Parameters:

  • namespace (String)

    name to use for lookup



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/nub/net.rb', line 199

def namespace_veths(namespace)
  host, guest = Veth.new, Veth.new
  if self.namespaces.include?(namespace)

    # Lookup guest side
    out = `ip netns exec #{namespace} ip a show type veth`
    host_i = out[/([\d]+):\s+.*@if[\d]+/, 1]
    if host_i
      guest.name = out[/ (.*)@if[\d]+/, 1]
      guest.ip = out[/inet\s+([\d]+\.[\d]+\.[\d]+\.[\d]+).*/, 1]

      # Lookup host side
      out = `ip a show type veth`
      host.name = out[/ (.*)@if#{host_i}/, 1]
      host_ip = out[/inet(.*)#{host.name}/, 1][/\s*([\d]+\.[\d]+\.[\d]+\.[\d]+\/[\d]+).*/, 1]
      host.ip = host_ip[/(.*)\/[\d]+/, 1]
    end
  end

  return host, guest
end

#namespacesObject

Get all namespaces



155
156
157
# File 'lib/nub/net.rb', line 155

def namespaces
  return Dir[File.join("/var/run/netns", "*")].map{|x| File.basename(x)}
end

#port_open?(ip, port, *args) ⇒ Boolean

Check if the given ip:port is open

Parameters:

  • ip (String)

    to check

  • port (Int)

    to check

  • timeout (Int)

    to wait when dead

Returns:

  • (Boolean)


123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/nub/net.rb', line 123

def port_open?(ip, port, *args)
  sec = args.any? ? args.first.to_i : 0.1
  Timeout::timeout(sec){
    begin
      TCPSocket.new(ip, port).close
      true
    rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
      false
    end
  }
rescue Timeout::Error
  false
end

#primary_nicObject

Determine the primary nic on the machine based off default routing to google.com



94
95
96
97
# File 'lib/nub/net.rb', line 94

def primary_nic
  out = `ip route`
  return out[/default via.*dev (.*) proto/, 1]
end

#proxyObject

Get fresh proxy from environment



54
55
56
57
58
59
60
61
62
63
# File 'lib/nub/net.rb', line 54

def proxy
  return OpenStruct.new({
    ftp: ENV['ftp_proxy'],
    http: ENV['http_proxy'],
    https: ENV['https_proxy'],
    no: ENV['no_proxy'],
    uri: ENV['http_proxy'] ? ENV['http_proxy'].split(':')[0..-2] * ":" : nil,
    port: ENV['http_proxy'] ? ENV['http_proxy'].split(':').last : nil
  })
end

#proxy?Boolean

Check if a proxy is set

Returns:

  • (Boolean)


66
67
68
# File 'lib/nub/net.rb', line 66

def proxy?
  return !self.proxy.http.nil?
end

#proxy_export(*args) ⇒ Object

Get a shell export string for proxies

Parameters:

  • proxy (String)

    to use rather than default



72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/nub/net.rb', line 72

def proxy_export(*args)
  proxy = args.any? ? args.first.to_s : nil
  if proxy
    ({'ftp_proxy' => proxy,
     'http_proxy' => proxy,
     'https_proxy' => proxy
    }.map{|k,v| "export #{k}=#{v}"} * ';') + ";"
  elsif self.proxy?
    (self.proxy.to_h.map{|k,v| (![:uri, :port].include?(k) && v) ? "export #{k}_proxy=#{v}" : nil}.compact * ';') + ";"
  else
    return nil
  end
end