Skip to content

Instantly share code, notes, and snippets.

@jmelis
Created September 17, 2012 12:54
Show Gist options
  • Save jmelis/3737116 to your computer and use it in GitHub Desktop.
Save jmelis/3737116 to your computer and use it in GitHub Desktop.
Virtual Router
#!/usr/bin/env ruby
# -------------------------------------------------------------------------- #
# Copyright 2002-2012, OpenNebula Project Leads (OpenNebula.org) #
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
# not use this file except in compliance with the License. You may obtain #
# a copy of the License at #
# #
# http://www.apache.org/licenses/LICENSE-2.0 #
# #
# Unless required by applicable law or agreed to in writing, software #
# distributed under the License is distributed on an "AS IS" BASIS, #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
# See the License for the specific language governing permissions and #
# limitations under the License. #
#--------------------------------------------------------------------------- #
require 'rexml/document'
require 'base64'
require 'fileutils'
class Router
# Default files
FILES = {
:resolv_conf => "/etc/resolv.conf",
:context => "/mnt/context/context.sh",
:dnsmasq_conf => "/etc/dnsmasq.conf",
:log_file => "/var/log/router.log"
}
# Default MAC prefix
MAC_PREFIX = "02:00"
# Default gateway (last byte)
DEFAULT_GW = "1"
# Default netmask
DEFAULT_NETMASK = "255.255.255.0"
# Context parameters that are base64 encoded
BASE_64_KEYS = [:privnet, :pubnet, :template]
# The specification on how to fetch these attributes.
# The order in the array matters, the first non-empty one is returned.
ATTRIBUTES = {
:dns => [
{
:resource => :context
},
{
:resource => :pubnet,
:resource_xpath => 'TEMPLATE/DNS'
}
],
:search => [
{
:resource => :context
},
{
:resource => :pubnet,
:resource_xpath => 'TEMPLATE/SEARCH'
}
],
:nat => [
{
:resource => :context,
:resource_name => "FORWARDING"
}
],
:dhcp => [
{
:resource => :context
},
{
:resource => :privnet,
:resource_xpath => 'TEMPLATE/DHCP'
}
],
:ntp => [
{
:resource => :context
},
{
:resource => :privnet,
:resource_xpath => 'TEMPLATE/NTP'
}
],
:root_pubkey => [
{
:resource => :context
}
],
:root_password => [
{
:resource => :context
}
]
}
def initialize
@context = read_context
unpack
get_network_information
end
############################################################################
# GETTERS
############################################################################
def pubnet
!@pubnet[:network_id].nil?
end
def privnet
!@privnet[:network_id].nil?
end
def dns
dns_raw = get_attribute(:dns)
dns_raw.split if !dns_raw.nil?
end
def search
get_attribute(:search)
end
def root_password
get_attribute(:root_password)
end
def root_pubkey
get_attribute(:root_pubkey)
end
def nat
nat_raw = get_attribute(:nat)
nat_raw.split if !nat_raw.nil?
end
def dhcp
dhcp_raw = get_attribute(:dhcp) || ""
if dhcp_raw.downcase.match(/^y(es)?$/)
true
else
false
end
end
def ntp
ntp_raw = get_attribute(:ntp) || ""
if ntp_raw.downcase.match(/^y(es)?$/)
true
else
false
end
end
############################################################################
# ACTIONS
############################################################################
def write_resolv_conf
if dns
File.open(FILES[:resolv_conf], 'w') do |resolv_conf|
if search
resolv_conf.puts "search #{search}"
end
dns.each do |nameserver|
resolv_conf.puts "nameserver #{nameserver}"
end
end
elsif search
File.open(FILE[:resolv_conf], 'a') do |resolv_conf|
resolv_conf.puts "search #{search}"
end
end
end
def configure_network
if pubnet
ip = @pubnet[:ip]
nic = @pubnet[:interface]
netmask = @pubnet[:netmask]
gateway = @pubnet[:gateway]
run "ip link set #{nic} up"
run "ip addr add #{ip}/#{netmask} dev #{nic}"
run "ip route add default via #{gateway}"
end
if privnet
ip = @privnet[:ip]
nic = @privnet[:interface]
netmask = @privnet[:netmask]
run "ip link set #{nic} up"
run "ip addr add #{ip}/#{netmask} dev #{nic}"
end
end
def configure_dnsmasq
File.open(FILES[:dnsmasq_conf],'w') do |dnsmasq_conf|
_,ip_start,_ = dhcp_ip_mac_pairs[0]
_,ip_end,_ = dhcp_ip_mac_pairs[-1]
dnsmasq_conf.puts "dhcp-range=#{ip_start},#{ip_end},infinite"
dnsmasq_conf.puts
dnsmasq_conf.puts "dhcp-option=42,#{@privnet[:ip]} # ntp server"
dnsmasq_conf.puts "dhcp-option=4,#{@privnet[:ip]} # name server"
dnsmasq_conf.puts
dhcp_ip_mac_pairs.each do |pair|
mac, ip = pair
dnsmasq_conf.puts "dhcp-host=#{mac},#{ip}"
end
end
end
def configure_nat
nat.each do |nat_rule|
nat_rule = nat_rule.split(":")
if nat_rule.length == 2
ip, inport = nat_rule
outport = inport
elsif nat_rule.length == 3
outport, ip, inport = nat_rule
end
run "iptables -t nat -A PREROUTING -p tcp --dport #{outport} " \
"-j DNAT --to-destination #{ip}:#{inport}"
end
end
def configure_masquerade
run "iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE"
end
def configure_ip_forward
run "sysctl -w net.ipv4.ip_forward=1"
end
def configure_root_password
run "usermod -p #{root_password} root"
end
def configure_root_pubkey
FileUtils.mkdir_p("/root/.ssh",:mode => 0700)
File.open("/root/.ssh/authorized_keys", "a", 0600) do |f|
f.write(root_pubkey)
end
end
def service(service, action = :start)
action = action.to_s
run "/etc/rc.d/#{service} #{action}"
end
############################################################################
# Private methods
############################################################################
private
def get_network_information
@pubnet = Hash.new
@privnet = Hash.new
mac_interfaces = Hash[
Dir["/sys/class/net/*/address"].collect do |f|
[ File.read(f).strip, f.split('/')[4] ]
end
]
if (pubnet_id = get_element_xpath(:pubnet, 'ID'))
@pubnet[:network_id] = pubnet_id
xpath_ip = "TEMPLATE/NIC[NETWORK_ID='#{pubnet_id}']/IP"
xpath_mac = "TEMPLATE/NIC[NETWORK_ID='#{pubnet_id}']/MAC"
@pubnet[:ip] = get_element_xpath(:template, xpath_ip)
@pubnet[:mac] = get_element_xpath(:template, xpath_mac)
@pubnet[:interface] = mac_interfaces[@pubnet[:mac]]
netmask = get_element_xpath(:pubnet, 'TEMPLATE/NETWORK_MASK')
@pubnet[:netmask] = netmask || DEFAULT_NETMASK
gateway = get_element_xpath(:pubnet, 'TEMPLATE/GATEWAY')
if gateway.nil?
gateway = @pubnet[:ip].gsub(/\.\d{1,3}$/,".#{DEFAULT_GW}")
end
@pubnet[:gateway] = gateway
end
if (privnet_id = get_element_xpath(:privnet, 'ID'))
@privnet[:network_id] = privnet_id
xpath_ip = "TEMPLATE/NIC[NETWORK_ID='#{privnet_id}']/IP"
xpath_mac = "TEMPLATE/NIC[NETWORK_ID='#{privnet_id}']/MAC"
@privnet[:ip] = get_element_xpath(:template, xpath_ip)
@privnet[:mac] = get_element_xpath(:template, xpath_mac)
@privnet[:interface] = mac_interfaces[@privnet[:mac]]
netmask = get_element_xpath(:privnet, 'TEMPLATE/NETWORK_MASK')
@privnet[:netmask] = netmask || DEFAULT_NETMASK
end
end
def log(msg)
File.open(FILES[:log_file],'a') {|f| f.puts msg}
end
def run(cmd)
log(cmd)
output = `#{cmd} 2>&1`
exitstatus = $?.exitstatus
log(output) if !output.empty?
log("Error: exit code #{exitstatus}") if exitstatus != 0
end
def dhcp_ip_mac_pairs
netxml = @xml[:privnet]
pairs = Array.new
if netxml.elements['RANGE']
ip_start = netxml.elements['RANGE/IP_START'].text
ip_end = netxml.elements['RANGE/IP_END'].text
(ip_to_int(ip_start)..ip_to_int(ip_end)).each do |int_ip|
ip = int_to_ip(int_ip)
mac = ip2mac(ip)
pairs << [mac, ip, int_ip]
end
elsif netxml.elements['LEASES/LEASE']
netxml.elements.each('LEASES/LEASE') do |lease|
ip = lease.elements['IP'].text
mac = lease.elements['MAC'].text
int_ip = ip_to_int(ip)
pairs << [mac, ip, int_ip]
end
end
pairs.sort{|a,b| a[2] <=> b[2]}
end
def ip_to_int(ip)
num = 0
ip.split(".").each{|i| num *= 256; num = num + i.to_i}
num
end
def int_to_ip(num)
ip = Array.new
(0..3).reverse_each do |i|
ip << (((num>>(8*i)) & 0xff))
end
ip.join('.')
end
def ip2mac(ip)
mac = MAC_PREFIX + ':' \
+ ip.split('.').collect{|i| sprintf("%02X",i)}.join(':')
mac.downcase
end
def mac2ip(mac)
mac.split(':')[2..-1].collect{|i| i.to_i(16)}.join('.')
end
def unpack
@xml = Hash.new
BASE_64_KEYS.each do |key|
if @context.include? key
tpl = Base64::decode64(@context[key])
@xml[key] = REXML::Document.new(tpl).root
end
end
end
def get_attribute(name)
order = ATTRIBUTES[name]
return nil if order.nil?
order.each do |e|
if e[:resource] != :context
resource = e[:resource]
xpath = e[:resource_xpath]
value = get_element_xpath(resource, xpath)
return value if !value.nil?
else
if e[:resource_name]
resource_name = e[:resource_name].downcase.to_sym
else
resource_name = name
end
element = @context[resource_name]
return element if !element.nil?
end
end
return nil
end
def get_element_xpath(resource, xpath)
xml_resource = @xml[resource]
return nil if xml_resource.nil?
element = xml_resource.elements[xpath]
return element.text.to_s if !element.nil?
end
def read_context
context = Hash.new
context_file = File.read(FILES[:context])
context_file.each_line do |line|
next if line.match(/^#/)
if (m = line.match(/^(.*?)="(.*)"$/))
key = m[1].downcase.to_sym
value = m[2]
context[key] = value
end
end
context
end
end
router = Router.new
router.configure_network
if router.pubnet
if router.dns || router.search
router.write_resolv_conf
end
# Set masquerade
router.configure_masquerade
# Set ipv4 forward
router.configure_ip_forward
# Set NAT rules
if router.nat
router.configure_nat
end
end
if router.privnet
if router.dhcp
router.configure_dnsmasq
router.service("dnsmasq")
end
if router.ntp
router.service("ntpd")
end
end
if router.root_password
router.configure_root_password
end
if router.root_pubkey
router.configure_root_pubkey
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment