#!/usr/bin/env ruby # -------------------------------------------------------------------------- # # Copyright 2002-2023, OpenNebula Project, OpenNebula Systems # # # # 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' require 'erb' class Router # Default files FILES = { :resolv_conf => "/etc/resolv.conf", :context => "/mnt/context/context.sh", :dnsmasq_conf => "/etc/dnsmasq.conf", :radvd_conf => "/etc/radvd.conf", :log_file => "/var/log/router.log", :authorized_keys => "/root/.ssh/authorized_keys" } # 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, :root_password] # Context parameters that are XML documents XML_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 }, { :resource => :privnet, :resource_xpath => 'TEMPLATE/FORWARDING' } ], :dhcp => [ { :resource => :context }, { :resource => :privnet, :resource_xpath => 'TEMPLATE/DHCP' } ], :radvd => [ { :resource => :context }, { :resource => :privnet, :resource_xpath => 'TEMPLATE/RADVD' } ], :ntp_server => [ { :resource => :context }, { :resource => :privnet, :resource_xpath => 'TEMPLATE/NTP_SERVER' } ], :ssh_public_key => [ { :resource => :context } ], :root_pubkey => [ { :resource => :context } ], :root_password => [ { :resource => :context } ] } def initialize mount_context if (@context = read_context) unpack get_network_information end 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(:ssh_public_key) || 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_server get_attribute(:ntp_server) end def radvd radvd_raw = get_attribute(:radvd) || "" radvd = !!radvd_raw.downcase.match(/^y(es)?$/) radvd and @privnet[:ipv6] end ############################################################################ # ACTIONS ############################################################################ def mount_context log("mounting context") FileUtils.mkdir_p("/mnt/context") run "mount -t iso9660 /dev/cdrom /mnt/context 2>/dev/null" end 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(FILES[:resolv_conf], 'a') do |resolv_conf| resolv_conf.puts "search #{search}" end end end def configure_network if pubnet ip = @pubnet[:ip] ip6_global = @pubnet[:ip6_global] ip6_site = @pubnet[:ip6_site] 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 addr add #{ip6_global} dev #{nic}" if ip6_global run "ip addr add #{ip6_site} dev #{nic}" if ip6_site run "ip route add default via #{gateway}" end if privnet ip = @privnet[:ip] ip6_global = @privnet[:ip6_global] ip6_site = @privnet[:ip6_site] nic = @privnet[:interface] netmask = @privnet[:netmask] run "ip link set #{nic} up" run "ip addr add #{ip}/#{netmask} dev #{nic}" run "ip addr add #{ip6_global} dev #{nic}" if ip6_global run "ip addr add #{ip6_site} dev #{nic}" if ip6_site end end def configure_dnsmasq File.open(FILES[:dnsmasq_conf],'w') do |conf| dhcp_ip_mac_pairs.collect do |ar| conf.puts "dhcp-range=#{ar[:ip_start]},#{ar[:ip_end]},infinite" end conf.puts "dhcp-option=42,#{ntp_server} # ntp server" if ntp_server conf.puts "dhcp-option=4,#{@privnet[:ip]} # name server" dhcp_ip_mac_pairs.each do |ar| ar[:mac_ip_pairs].each do |mac,ip,_| conf.puts "dhcp-host=#{mac},#{ip}" end 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_radvd prefixes = [@privnet[:ip6_global],@privnet[:ip6_site]].compact privnet_iface = @privnet[:interface] radvd_conf_tpl =<<-EOF.gsub(/^\s{12}/,"") interface <%= privnet_iface %> { AdvSendAdvert on; <% prefixes.each do |p| %> prefix <%= p %>/64 { AdvOnLink on; }; <% end %> }; EOF radvd_conf = ERB.new(radvd_conf_tpl).result(binding) File.open(FILES[:radvd_conf],'w') {|c| c.puts radvd_conf } 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 "echo -n 'root:#{root_password}'|chpasswd -e" end def configure_root_pubkey FileUtils.mkdir_p(File.dirname(FILES[:authorized_keys]),:mode => 0700) File.open(FILES[:authorized_keys], "a", 0600) do |f| f.write(root_pubkey) end end def service(service, action = :start) action = action.to_s run "/etc/init.d/#{service} #{action}" end def log(msg, command = false) msg = "=> #{msg}" unless command File.open(FILES[:log_file],'a') {|f| f.puts msg} end def has_context? !!@context 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_ip6_global = "TEMPLATE/NIC[NETWORK_ID='#{pubnet_id}']/IP6_GLOBAL" xpath_ip6_site = "TEMPLATE/NIC[NETWORK_ID='#{pubnet_id}']/IP6_ULA" xpath_mac = "TEMPLATE/NIC[NETWORK_ID='#{pubnet_id}']/MAC" @pubnet[:ip] = get_element_xpath(:template, xpath_ip) @pubnet[:ip6_global] = get_element_xpath(:template, xpath_ip6_global) @pubnet[:ip6_site] = get_element_xpath(:template, xpath_ip6_site) @pubnet[:mac] = get_element_xpath(:template, xpath_mac) @pubnet[:ipv6] = true if @pubnet[:ip6_global] or @pubnet[:ip6_site] @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_ip6_global = "TEMPLATE/NIC[NETWORK_ID='#{privnet_id}']/IP6_GLOBAL" xpath_ip6_site = "TEMPLATE/NIC[NETWORK_ID='#{privnet_id}']/IP6_ULA" xpath_mac = "TEMPLATE/NIC[NETWORK_ID='#{privnet_id}']/MAC" @privnet[:ip] = get_element_xpath(:template, xpath_ip) @privnet[:ip6_global] = get_element_xpath(:template, xpath_ip6_global) @privnet[:ip6_site] = get_element_xpath(:template, xpath_ip6_site) @privnet[:mac] = get_element_xpath(:template, xpath_mac) @privnet[:ipv6] = true if @privnet[:ip6_global] or @privnet[:ip6_site] @privnet[:interface] = @mac_interfaces[@privnet[:mac]] netmask = get_element_xpath(:privnet, 'TEMPLATE/NETWORK_MASK') @privnet[:netmask] = netmask || DEFAULT_NETMASK end end def run(cmd) log(cmd, true) 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 netxml.elements.each('AR_POOL/AR') do |ar| mac_ip_pairs = Array.new ip_start = ar.elements['IP'].text size = ar.elements['SIZE'].text.to_i ip_start_int = ip_to_int(ip_start) ip_end_int = ip_start_int + size ip_end = int_to_ip(ip_end_int) (ip_start_int..ip_end_int).each do |int_ip| ip = int_to_ip(int_ip) mac = ip2mac(ip) # skip this IP if it's already taken next if ar.elements["LEASES/LEASE[IP='#{ip}']"] mac_ip_pairs << [mac, ip] end pairs << { :ip_start => ip_start, :ip_end => ip_end, :size => size, :mac_ip_pairs => mac_ip_pairs } end pairs 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 @context[key] = Base64::decode64(@context[key]) end end XML_KEYS.each do |key| if @context.include? key @xml[key] = REXML::Document.new(@context[key]).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] 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 return nil if !File.exist?(FILES[: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 if !router.has_context? router.log("ERROR: Context not found. Stopping.") exit 1 end router.log("configure network") router.configure_network if router.pubnet if router.dns || router.search router.log("write resolv.conf") router.write_resolv_conf end # Set masquerade router.log("set masquerade") router.configure_masquerade # Set ipv4 forward router.log("ip forward") router.configure_ip_forward # Set NAT rules if router.nat router.log("configure nat") router.configure_nat end end if router.privnet and router.dhcp router.log("configure dnsmasq") router.configure_dnsmasq router.service("dnsmasq") end if router.radvd router.log("configure radvd") router.configure_radvd router.service("radvd") end if router.root_password router.log("configure root password") router.configure_root_password end if router.root_pubkey router.log("configure root pubkey") router.configure_root_pubkey end