From 0a7297a86ddf2ac33cd6546cbcdfd9e050f76c1c Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Tue, 3 May 2011 12:41:10 +0100 Subject: [PATCH] Reorg to separate different NETLINK_ROUTE functions. Try adding vlan interfaces --- examples/add_addr.rb | 17 +- examples/add_vlan.rb | 30 +++ examples/route_hi.rb | 6 +- examples/route_lo.rb | 6 +- lib/netlink/c_struct.rb | 9 +- lib/netlink/constants.rb | 29 ++- lib/netlink/message.rb | 55 ++--- lib/netlink/nlsocket.rb | 6 +- lib/netlink/route.rb | 334 ++---------------------------- lib/netlink/route/if_handler.rb | 355 ++++++++++++++++++++++++++++++++ lib/netlink/route/rt_handler.rb | 106 ++++++++++ 11 files changed, 585 insertions(+), 368 deletions(-) create mode 100644 examples/add_vlan.rb create mode 100644 lib/netlink/route/if_handler.rb create mode 100644 lib/netlink/route/rt_handler.rb diff --git a/examples/add_addr.rb b/examples/add_addr.rb index d54c3a5..c08ff04 100644 --- a/examples/add_addr.rb +++ b/examples/add_addr.rb @@ -5,16 +5,17 @@ require 'netlink/route' nl = Netlink::Route::Socket.new puts "\n*** Before adding address" -nl.addrs["lo"][Socket::AF_INET].each { |x| puts x.address } +nl.if.addrs["lo"][Socket::AF_INET].each { |x| puts x.address } -begin - nl.add_addr(:index=>"lo", :local=>"1.2.3.4", :prefixlen=>32) -rescue Errno::EEXIST -end puts "\n*** After adding address" -nl.addrs["lo"][Socket::AF_INET].each { |x| puts x.address } +begin + nl.if.add_addr(:index=>"lo", :local=>"1.2.3.4", :prefixlen=>32) +rescue Errno::EEXIST + puts "Already exists" +end +nl.if.addrs["lo"][Socket::AF_INET].each { |x| puts x.address } -nl.delete_addr(:index=>"lo", :local=>"1.2.3.4", :prefixlen=>32) puts "\n*** After deleting address" -nl.addrs["lo"][Socket::AF_INET].each { |x| puts x.address } +nl.if.delete_addr(:index=>"lo", :local=>"1.2.3.4", :prefixlen=>32) +nl.if.addrs["lo"][Socket::AF_INET].each { |x| puts x.address } diff --git a/examples/add_vlan.rb b/examples/add_vlan.rb new file mode 100644 index 0000000..72eb622 --- /dev/null +++ b/examples/add_vlan.rb @@ -0,0 +1,30 @@ +LIBDIR = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) +$LOAD_PATH.unshift LIBDIR + +require 'netlink/route' +require 'pp' + +nl = Netlink::Route::Socket.new +puts "\n*** Before adding VLAN" +pp nl.if.select { |lnk| lnk.kind?("vlan") } + +puts "\n*** After adding VLAN on lo" +begin + nl.if.add_link(:link=>"lo", + :linkinfo=>Netlink::LinkInfo.new( + :kind=>"vlan", :data=>Netlink::VlanInfo.new( + :id=>1234, #:flags => Netlink::VlanFlags.new(:flags=>Netlink::VLAN_FLAG_LOOSE_BINDING, :mask=>0xffffffff) + ))) +rescue Errno::EEXIST + puts "Already present" +end +pp nl.if.select { |lnk| lnk.kind?("vlan") } + +puts "\n*** After deleting VLANs from lo" +nl.if.each do |lnk| + if lnk.kind?('vlan') && nl.if.name(lnk.link) == 'lo' + nl.if.delete_link(lnk.index) + end +end +pp nl.if.select { |lnk| lnk.kind?("vlan") } + diff --git a/examples/route_hi.rb b/examples/route_hi.rb index 074662b..694a261 100644 --- a/examples/route_hi.rb +++ b/examples/route_hi.rb @@ -9,8 +9,8 @@ require 'netlink/route' # and then manipulated internally. nl = Netlink::Route::Socket.new -pp nl.link["eth0"] -pp nl.addrs["eth0"] +pp nl.if["eth0"] +pp nl.if.addrs["eth0"] # Find the route with the shortest prefix len (probably default route) -pp nl.routes[Socket::AF_INET].min_by { |route| route.dst_len } +pp nl.rt[Socket::AF_INET].min_by { |route| route.dst_len } diff --git a/examples/route_lo.rb b/examples/route_lo.rb index 9501bae..faf3eaa 100644 --- a/examples/route_lo.rb +++ b/examples/route_lo.rb @@ -9,8 +9,8 @@ require 'netlink/route' nl = Netlink::Route::Socket.new puts "*** links ***" -pp nl.read_links +pp nl.if.read_links puts "*** addrs ***" -pp nl.read_addrs(:family => Socket::AF_INET) +pp nl.if.read_addrs(:family => Socket::AF_INET) puts "*** routes ***" -pp nl.read_routes(:family => Socket::AF_INET) +pp nl.rt.read_routes(:family => Socket::AF_INET) diff --git a/lib/netlink/c_struct.rb b/lib/netlink/c_struct.rb index eb40eaa..291c571 100644 --- a/lib/netlink/c_struct.rb +++ b/lib/netlink/c_struct.rb @@ -24,7 +24,7 @@ module Netlink # # msg = Foo.new(:bar => 123) # msg.bar = 456 # accessor methods -# str = msg.to_s # convert to binary +# str = msg.to_str # convert to binary # msg2 = Foo.parse(str) # convert from binary # msg2 = Foo.new(msg) # copy an existing object class CStruct @@ -169,7 +169,7 @@ class CStruct end # Returns the packed binary representation of this structure - def to_s + def to_str self.class::FIELDS.map { |key| self[key] }.pack(self.class::FORMAT) end @@ -177,11 +177,14 @@ class CStruct "#<#{self.class} #{@attrs.inspect}>" end - # Convert a binary representation of this structure into an object instance + # Convert a binary representation of this structure into an object instance. + # If a block is given, the object is yielded to that block. Finally the + # after_parse hook is called. def self.parse(data, obj=new) data.unpack(self::FORMAT).zip(self::FIELDS).each do |val, key| obj[key] = val end + yield obj if block_given? obj.after_parse obj end diff --git a/lib/netlink/constants.rb b/lib/netlink/constants.rb index 8841763..72c606c 100644 --- a/lib/netlink/constants.rb +++ b/lib/netlink/constants.rb @@ -219,7 +219,7 @@ module Netlink IFLA_WEIGHT = 15 IFLA_OPERSTATE = 16 IFLA_LINKMODE = 17 - IFLA_LINKINFO = 18 + IFLA_LINKINFO = 18 # Nested IFLA_INFO_* IFLA_NET_NS_PID = 19 IFLA_IFALIAS = 20 IFLA_NUM_VF = 21 @@ -228,6 +228,33 @@ module Netlink IFLA_VF_PORTS = 24 IFLA_PORT_SELF = 25 + IFLA_INFO_UNSPEC = 0 + IFLA_INFO_KIND = 1 # "vlan", "gre" etc + IFLA_INFO_DATA = 2 # packed rtattrs specific to type, e.g. vlan + IFLA_INFO_XSTATS = 3 + + # INFO_DATA for INFO_KIND == "vlan" + IFLA_VLAN_UNSPEC = 0 + IFLA_VLAN_ID = 1 # ushort + IFLA_VLAN_FLAGS = 2 # struct ifla_vlan_flags + IFLA_VLAN_EGRESS_QOS = 3 # followed by instance of IFLA_VLAN_QOS_* + IFLA_VLAN_INGRESS_QOS = 4 # followed by instance of IFLA_VLAN_QOS_* + + IFLA_VLAN_QOS_UNSPEC = 0 + IFLA_VLAN_QOS_MAPPING = 1 + + IFLA_MACVLAN_UNSPEC = 0 + IFLA_MACVLAN_MODE = 1 + + MACVLAN_MODE_PRIVATE = 1 + MACVLAN_MODE_VEPA = 2 + MACVLAN_MODE_BRIDGE = 4 + + # linux/if_vlan.h + VLAN_FLAG_REORDER_HDR = 0x1 + VLAN_FLAG_GVRP = 0x2 + VLAN_FLAG_LOOSE_BINDING = 0x4 + # from linux/if_addr.h IFA_UNSPEC = 0 IFA_ADDRESS = 1 diff --git a/lib/netlink/message.rb b/lib/netlink/message.rb index 6293dfd..8e6ae0e 100644 --- a/lib/netlink/message.rb +++ b/lib/netlink/message.rb @@ -61,7 +61,7 @@ module Netlink # colons, hyphens or dots. # IFInfo.new(:address => "00:11:22:33:44:55") # this is OK define_type :l2addr, - :pack => lambda { |val,obj| [val.delete(":-.")].pack("H*") }, + :pack => lambda { |val,obj| [val.delete(":.-")].pack("H*") }, # hyphen last, otherwise it's a character range :unpack => lambda { |val,obj| val.unpack("H*").first } # L3 addresses are presented as IPAddr objects where possible. When @@ -72,10 +72,10 @@ module Netlink # IFAddr.new(:family=>Socket::AF_INET, :address=>0x01020304) # Furthermore, the 'family' will be set automatically if it is unset # at the time the message is encoded: - # IFAddr.new(:address=>IPAddr.new("1.2.3.4")).to_s # ok - # IFAddr.new(:address=>"1.2.3.4").to_s # ok - # IFAddr.new(:address=>0x01020304).to_s # error, unknown family - # IFAddr.new(:address=>"1.2.3.4", :local=>"::1").to_s # error, mismatched families + # IFAddr.new(:address=>IPAddr.new("1.2.3.4")).to_str # ok + # IFAddr.new(:address=>"1.2.3.4").to_str # ok + # IFAddr.new(:address=>0x01020304).to_str # error, unknown family + # IFAddr.new(:address=>"1.2.3.4", :local=>"::1").to_str # error, mismatched families define_type :l3addr, :pack => lambda { |val,obj| case obj.family @@ -147,15 +147,19 @@ module Netlink # The main message is processed *after* the rtattrs; this is so that # the address family can be set automatically while processing any # optional l3 address rtattrs. - def to_s + def to_str data = "" self.class::RTATTRS.each do |code, (name, info)| if val = @attrs[name] Message.nlmsg_pad(data) # assume NLMSG_ALIGNTO == NLA_ALIGNTO - if pack = info[:pack] + if !info + val = val.to_str # raw binary or nested structure + elsif pack = info[:pack] val = pack[val,self] elsif pattern = info[:pattern] val = Array(val).pack(pattern) + else + val = val.to_str end data << [val.bytesize+RTATTR_SIZE, code].pack(RTATTR_PACK) << val end @@ -166,27 +170,28 @@ module Netlink # Convert a binary representation of this message into an object instance. # The main message is processed *before* the rtattrs, so that the # address family is available for l3 address rtattrs. - def self.parse(data) - res = super - attrs = res.to_hash - unpack_rtattr(data, attr_offset) do |code, val| - name, info = self::RTATTRS[code] - if name - if !info - # skip - elsif unpack = info[:unpack] - val = unpack[val,res] - elsif pattern = info[:pattern] - val = val.unpack(pattern).first + def self.parse(data,*rest) + super(data,*rest) do |res| + attrs = res.to_hash + unpack_rtattr(data, attr_offset) do |code, val| + name, info = self::RTATTRS[code] + if name + if !info + # skip + elsif unpack = info[:unpack] + val = unpack[val,res] + elsif pattern = info[:pattern] + val = val.unpack(pattern).first + end + warn "Duplicate attribute #{name} (#{code}): #{attrs[name].inspect} -> #{val.inspect}" if attrs[name] + attrs[name] = val + else + warn "Unknown attribute #{code}, value #{val.inspect}" + attrs[code] = val end - warn "Duplicate attribute #{name} (#{code}): #{attrs[name].inspect} -> #{val.inspect}" if attrs[name] - attrs[name] = val - else - warn "Unknown attribute #{code}, value #{val.inspect}" - attrs[code] = val end + yield res if block_given? end - res end # Unpack a string containing a sequence of rtattrs, yielding each in turn. diff --git a/lib/netlink/nlsocket.rb b/lib/netlink/nlsocket.rb index ced2b3b..99bca53 100644 --- a/lib/netlink/nlsocket.rb +++ b/lib/netlink/nlsocket.rb @@ -90,7 +90,7 @@ module Netlink # pid:: pid, defaults to $$ # vflags:: sendmsg flags, defaults to 0 # Normally 'msg' would be an instance of a Netlink::Message subclass, - # although in fact any object which respond to #to_s will do (if you + # although in fact any object which respond to #to_str will do (if you # want to pack the message body yourself). def send_request(type, msg, flags=NLM_F_REQUEST, sockaddr=SOCKADDR_DEFAULT, seq=next_seq, pid=@pid, vflags=0, controls=[]) @socket.sendmsg( @@ -101,7 +101,7 @@ module Netlink # Build a message comprising header+body. It is not padded at the end. def build_message(type, body, flags=NLM_F_REQUEST, seq=next_seq, pid=@pid) - body = body.to_s + body = body.to_str header = [ body.bytesize + NLMSGHDR_SIZE, type, flags, seq, pid @@ -190,7 +190,7 @@ module Netlink loop do parse_yield(recvmsg(timeout)) do |type, flags, seq, pid, msg| if !check_pid_seq || (pid == @pid && seq == @seq) - self.class.check_error(msg.error) if type == NLMSG_ERROR + Netlink.check_error(msg.error) if type == NLMSG_ERROR res = yield type, msg next unless res == false end diff --git a/lib/netlink/route.rb b/lib/netlink/route.rb index a3409a6..ae30bae 100644 --- a/lib/netlink/route.rb +++ b/lib/netlink/route.rb @@ -4,337 +4,27 @@ require 'netlink/nlsocket' require 'netlink/message' module Netlink - # struct rtnl_link_stats / rtnl_link_stats64 - LinkStats = Struct.new :rx_packets, :tx_packets, - :rx_bytes, :tx_bytes, - :rx_errors, :tx_errors, - :rx_dropped, :tx_dropped, - :multicast, :collisions, - :rx_length_errors, :rx_over_errors, - :rx_crc_errors, :rx_frame_errors, - :rx_fifo_errors, :rx_missed_errors, - :tx_aborted_errorsr, :tx_carrier_errors, - :tx_fifo_errors, :tx_heartbeat_errors, - :tx_window_errors, - :rx_compressed, :tx_compressed - - # struct ifmap - IFMap = Struct.new :mem_start, :mem_end, :base_addr, :irq, :dma, :port - - # struct ifinfomsg - class IFInfo < RtattrMessage - code RTM_NEWLINK, RTM_DELLINK, RTM_GETLINK - - field :family, :uchar # Socket::AF_* - field :type, :ushort # ARPHRD_* - field :index, :int - field :flags, :uint # IFF_* - field :change, :uint, :default=>0xffffffff - rtattr :address, IFLA_ADDRESS, :l2addr - rtattr :broadcast, IFLA_BROADCAST, :l2addr - rtattr :ifname, IFLA_IFNAME, :cstring - rtattr :mtu, IFLA_MTU, :uint32 - rtattr :link, IFLA_LINK, :int32 - rtattr :qdisc, IFLA_QDISC, :cstring - rtattr :stats32, IFLA_STATS, - :pack => lambda { |val,obj| val.to_a.pack("L23") }, - :unpack => lambda { |str,obj| LinkStats.new(*(str.unpack("L23"))) } - rtattr :cost, IFLA_COST - rtattr :master, IFLA_MASTER, :uint32 - rtattr :wireless, IFLA_WIRELESS - rtattr :protinfo, IFLA_PROTINFO, :uchar - rtattr :txqlen, IFLA_TXQLEN, :uint32 - IFMAP_PACK = "QQQSCC".freeze #:nodoc: - rtattr :map, IFLA_MAP, - :pack => lambda { |val,obj| val.to_a.pack(IFMAP_PACK) }, - :unpack => lambda { |str,obj| IFMap.new(*(str.unpack(IFMAP_PACK))) } - rtattr :weight, IFLA_WEIGHT, :uint32 - rtattr :operstate, IFLA_OPERSTATE, :uchar - rtattr :linkmode, IFLA_LINKMODE, :uchar - rtattr :linkinfo, IFLA_LINKINFO # nested - rtattr :net_ns_pid, IFLA_NET_NS_PID, :uint32 - rtattr :ifalias, IFLA_IFALIAS, :cstring - rtattr :num_vf, IFLA_NUM_VF, :uint32 - rtattr :vfinfo_list, IFLA_VFINFO_LIST - rtattr :stats64, IFLA_STATS64, - :pack => lambda { |val,obj| val.to_a.pack("Q23") }, - :unpack => lambda { |str,obj| LinkStats.new(*(str.unpack("Q23"))) } - rtattr :vf_ports, IFLA_VF_PORTS - rtattr :port_self, IFLA_PORT_SELF - - # Return the best stats available (64bit or 32bit) - def stats - stats64 || stats32 - end - end - - # struct ifa_cacheinfo - IFACacheInfo = Struct.new :prefered, :valid, :cstamp, :tstamp - - # struct ifaddrmsg - class IFAddr < RtattrMessage - code RTM_NEWADDR, RTM_DELADDR, RTM_GETADDR - - field :family, :uchar # Socket::AF_* - field :prefixlen, :uchar - field :flags, :uchar # IFA_F_* - field :scope, :uchar # RT_SCOPE_* - field :index, :int - rtattr :address, IFA_ADDRESS, :l3addr - rtattr :local, IFA_LOCAL, :l3addr - rtattr :label, IFA_LABEL, :cstring - rtattr :broadcast, IFA_BROADCAST, :l3addr - rtattr :anycast, IFA_ANYCAST, :l3addr - rtattr :cacheinfo, IFA_CACHEINFO, - :pack => lambda { |val,obj| val.to_a.pack("L*") }, - :unpack => lambda { |str,obj| IFACacheInfo.new(*(str.unpack("L*"))) } - rtattr :multicast, IFA_MULTICAST, :l3addr - end - - # struct rta_cacheinfo - RTACacheInfo = Struct.new :clntref, :lastuse, :expires, :error, :used, :id, :ts, :tsage - - # struct rtmsg - class RT < RtattrMessage - code RTM_NEWROUTE, RTM_DELROUTE, RTM_GETROUTE - - field :family, :uchar # Socket::AF_* - field :dst_len, :uchar - field :src_len, :uchar - field :tos, :uchar - field :table, :uchar # table id or RT_TABLE_* - field :protocol, :uchar # RTPROT_* - field :scope, :uchar # RT_SCOPE_* - field :type, :uchar # RTN_* - field :flags, :uint # RTM_F_* - rtattr :dst, RTA_DST, :l3addr - rtattr :src, RTA_SRC, :l3addr - rtattr :iif, RTA_IIF, :uint32 - rtattr :oif, RTA_OIF, :uint32 - rtattr :gateway, RTA_GATEWAY, :l3addr - rtattr :priority, RTA_PRIORITY, :uint32 - rtattr :prefsrc, RTA_PREFSRC, :l3addr - # Route metrics are themselves packed using the rtattr format. - # In the kernel, the dst.metrics structure is an array of u32. - METRIC_PACK = "SSL".freeze #:nodoc: - METRIC_SIZE = [0,0,0].pack(METRIC_PACK).bytesize #:nodoc: - rtattr :metrics, RTA_METRICS, # {RTAX_* => Integer} - :pack => lambda { |metrics,obj| - metrics.map { |code,val| [METRIC_SIZE,code,val].pack(METRIC_PACK) }.join - }, - :unpack => lambda { |str,obj| - res = {} - RtattrMessage.unpack_rtattr(str) { |code,val| res[code] = val.unpack("L").first } - res - } - rtattr :multipath, RTA_MULTIPATH - rtattr :flow, RTA_FLOW - rtattr :cacheinfo, RTA_CACHEINFO, - :pack => lambda { |val,obj| val.to_a.pack("L*") }, - :unpack => lambda { |str,obj| RTACacheInfo.new(*(str.unpack("L*"))) } - rtattr :table2, RTA_TABLE, :uint32 # NOTE: table in two places! - end - module Route + autoload :IFHandler, 'netlink/route/if_handler' + autoload :RTHandler, 'netlink/route/rt_handler' + # This class formats and receives messages using NETLINK_ROUTE protocol class Socket < NLSocket def initialize(opt={}) super(opt.merge(:protocol => Netlink::NETLINK_ROUTE)) - clear_cache end - # Download a list of links (interfaces). Either returns an array of - # Netlink::IFInfo objects, or yields them to the supplied block. - # - # res = nl.read_links - # p res - # [#0, :type=>772, :index=>1, - # :flags=>65609, :change=>0, :ifname=>"lo", :txqlen=>0, :operstate=>0, - # :linkmode=>0, :mtu=>16436, :qdisc=>"noqueue", :map=>"...", - # :address=>"\x00\x00\x00\x00\x00\x00", :broadcast=>"\x00\x00\x00\x00\x00\x00", - # :stats32=>#, - # :stats64=>#}>, ...] - def read_links(opt=nil, &blk) - send_request RTM_GETLINK, IFInfo.new(opt), - NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST - receive_until_done(RTM_NEWLINK, &blk) - end - - # Download a list of routes. Either returns an array of - # Netlink::RT objects, or yields them to the supplied block. - # - # A hash of kernel options may be supplied, but you might also have - # to perform your own filtering. e.g. - # rt.read_routes(:family=>Socket::AF_INET) # works - # rt.read_routes(:protocol=>Netlink::RTPROT_STATIC) # ignored - # - # res = nl.read_routes(:family => Socket::AF_INET) - # p res - # [#2, :dst_len=>32, :src_len=>0, :tos=>0, - # :table=>255, :protocol=>2, :scope=>253, :type=>3, :flags=>0, :table2=>255, - # :dst=>#, - # :prefsrc=>#, :oif=>1}>, ...] - # - # Note that not all attributes will always be present. In particular, - # a defaultroute (dst_len=0) misses out the dst address completely: - # - # [#2, :dst_len=>0, :src_len=>0, :tos=>0, - # :table=>254, :protocol=>4, :scope=>0, :type=>1, :flags=>0, :table2=>254, - # :gateway=>#, :oif=>2}>, ...] - def read_routes(opt=nil, &blk) - send_request RTM_GETROUTE, RT.new(opt), - NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST - receive_until_done(RTM_NEWROUTE, &blk) + # Return a Netlink::Route::IF object for manipulating interfaces + # and interface addresses + def if(reload=false) + @if = nil if reload + @if ||= Netlink::Route::IFHandler.new(self) end - # Download a list of link addresses. Either returns an array of - # Netlink::IFAddr objects, or yields them to the supplied block. - # You will need to use the 'index' to cross reference to the interface. - # - # A hash of kernel options may be supplied, but likely only :family - # is honoured. - # - # res = nl.read_addrs(:family => Socket::AF_INET) - # p res - # [#2, :prefixlen=>8, :flags=>128, :scope=>254, - # :index=>1, :address=>#, - # :local=>#, :label=>"lo"}>, ...] - def read_addrs(opt=nil, &blk) - send_request RTM_GETADDR, IFAddr.new(opt), - NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST - receive_until_done(RTM_NEWADDR, &blk) - end - - # Download a list of addresses, grouped as {index=>[addr,addr], index=>[addr,addr]} - def read_addrs_by_ifindex(opt=nil) - res = read_addrs(opt).group_by { |obj| obj.index } - res.default = EMPTY_ARRAY - res - end - - # Add an IP address to an interface - # - # require 'netlink/route' - # rt = Netlink::Route::Socket.new - # rt.add_ipaddr(:index=>"eth0", :local=>"1.2.3.4", :prefixlen=>24) - def add_addr(opt) - ipaddr_modify(RTM_NEWADDR, NLM_F_CREATE|NLM_F_EXCL, opt) - end - - def change_addr(opt) - ipaddr_modify(RTM_NEWADDR, NLM_F_REPLACE, opt) - end - - def replace_addr(opt) - ipaddr_modify(RTM_NEWADDR, NLM_F_CREATE|NLM_F_REPLACE, opt) - end - - # Delete an IP address from an interface. Pass in either a hash of - # parameters, or an existing IFAddr object. - def delete_addr(opt) - ipaddr_modify(RTM_DELADDR, 0, opt) - end - - def ipaddr_modify(code, flags, msg) #:nodoc: - msg = IFAddr.new(msg) - case msg.index - when nil - raise "Device index must be specified" - when String - msg.index = linkindex(msg.index) - end - msg.address ||= msg.local - # Note: IPAddr doesn't support addresses off the subnet base, - # so there's no point trying to set msg.prefixlen from the IPAddr mask - cmd code, msg, flags|NLM_F_REQUEST - clear_cache - end - - # Clear the memoization cache - def clear_cache - @links = nil - @link = nil - @addrs = nil - @routes = nil - end - - # Return the memoized interface table as a flat array, suitable for - # iteration. e.g. - # rt.links.each { |link| puts link.ifname } - def links - @links ||= read_links - end - - # Return the memoized interface table, keyed by both ifname and ifindex. e.g. - # puts rt.link["eth0"].index - # puts rt.link[1].ifname - def link - @link ||= ( - h = {} - links.each { |link| h[link.index] = h[link.ifname] = link } - h - ) - end - - # Convert a link index to a (String) name, or nil. - # - # rt.routes[Socket::AF_INET].each do |route| - # puts "iif=#{rt.linkname(route.iif)}" - # puts "oif=#{rt.linkname(route.oif)}" - # end - def linkname(x) - link[x] && link[x].ifname - end - - # Convert a link name to an (Integer) index, or nil. - def linkindex(x) - link[x] && link[x].index - end - - # Return the memoized address table, keyed by interface name and - # address family, containing an array of addresses for each - # interface/family combination. i.e. - # - # # {ifname=>{family=>[addr,addr,...], ...}, ...} - # puts rt.addrs["eth0"][Socket::AF_INET][0].address - # - # If there are no addresses for a particular family then it will - # return a (frozen) empty array, to make iteration eaiser. - def addrs - @addrs ||= ( - h = {} - links.each do |link| - h[link.ifname] = {} - end - read_addrs.each do |addr| - ifname = link[addr.index].ifname - h[ifname] ||= Hash.new(EMPTY_ARRAY) - (h[ifname][addr.family] ||= []) << addr - end - h - ) - end - - # Return the memoized route table, keyed by address family, containing - # an array of routes for each address family. i.e. - # family combination. i.e. - # - # # {family=>[route,route,...], ...}, ...} - # puts rt.routes[Socket::AF_INET].first.dst - # - # If there are no routes for a particular family then it will - # return a (frozen) empty array. - def routes - @routes ||= ( - h = {} - links.each do |link| - h[link.ifname] = Hash.new(EMPTY_ARRAY) - end - read_routes.each do |route| - (h[route.family] ||= []) << route - end - h - ) + # Return a Netlink::Route::RT object for manipulating routes + def rt(reload=false) + @rt = nil if reload + @rt ||= Netlink::Route::RTHandler.new(self) end end end diff --git a/lib/netlink/route/if_handler.rb b/lib/netlink/route/if_handler.rb new file mode 100644 index 0000000..9a3bcc9 --- /dev/null +++ b/lib/netlink/route/if_handler.rb @@ -0,0 +1,355 @@ +require 'netlink/message' + +module Netlink + # struct rtnl_link_stats / rtnl_link_stats64 + LinkStats = Struct.new :rx_packets, :tx_packets, + :rx_bytes, :tx_bytes, + :rx_errors, :tx_errors, + :rx_dropped, :tx_dropped, + :multicast, :collisions, + :rx_length_errors, :rx_over_errors, + :rx_crc_errors, :rx_frame_errors, + :rx_fifo_errors, :rx_missed_errors, + :tx_aborted_errorsr, :tx_carrier_errors, + :tx_fifo_errors, :tx_heartbeat_errors, + :tx_window_errors, + :rx_compressed, :tx_compressed + + # struct ifmap + IFMap = Struct.new :mem_start, :mem_end, :base_addr, :irq, :dma, :port + + # struct ifinfomsg + class IFInfo < RtattrMessage + code RTM_NEWLINK, RTM_DELLINK, RTM_GETLINK + + field :family, :uchar # Socket::AF_* + field :type, :ushort # ARPHRD_* + field :index, :int + field :flags, :uint # IFF_* + field :change, :uint, :default=>0xffffffff + rtattr :address, IFLA_ADDRESS, :l2addr + rtattr :broadcast, IFLA_BROADCAST, :l2addr + rtattr :ifname, IFLA_IFNAME, :cstring + rtattr :mtu, IFLA_MTU, :uint32 + rtattr :link, IFLA_LINK, :int32 + rtattr :qdisc, IFLA_QDISC, :cstring + rtattr :stats32, IFLA_STATS, + :pack => lambda { |val,obj| val.to_a.pack("L23") }, + :unpack => lambda { |str,obj| LinkStats.new(*(str.unpack("L23"))) } + rtattr :cost, IFLA_COST + rtattr :master, IFLA_MASTER, :uint32 + rtattr :wireless, IFLA_WIRELESS + rtattr :protinfo, IFLA_PROTINFO, :uchar + rtattr :txqlen, IFLA_TXQLEN, :uint32 + IFMAP_PACK = "QQQSCC".freeze #:nodoc: + rtattr :map, IFLA_MAP, + :pack => lambda { |val,obj| val.to_a.pack(IFMAP_PACK) }, + :unpack => lambda { |str,obj| IFMap.new(*(str.unpack(IFMAP_PACK))) } + rtattr :weight, IFLA_WEIGHT, :uint32 + rtattr :operstate, IFLA_OPERSTATE, :uchar + rtattr :linkmode, IFLA_LINKMODE, :uchar + rtattr :linkinfo, IFLA_LINKINFO # nested + rtattr :net_ns_pid, IFLA_NET_NS_PID, :uint32 + rtattr :ifalias, IFLA_IFALIAS, :cstring + rtattr :num_vf, IFLA_NUM_VF, :uint32 + rtattr :vfinfo_list, IFLA_VFINFO_LIST + rtattr :stats64, IFLA_STATS64, + :pack => lambda { |val,obj| val.to_a.pack("Q23") }, + :unpack => lambda { |str,obj| LinkStats.new(*(str.unpack("Q23"))) } + rtattr :vf_ports, IFLA_VF_PORTS + rtattr :port_self, IFLA_PORT_SELF + + # Return the best stats available (64bit or 32bit) + def stats + stats64 || stats32 + end + + # Link kind for special links, e.g. "vlan" or "gre" + def kind + linkinfo && linkinfo.kind + end + + # Set link kind, creating a linkinfo member if necessary. e.g. + # i = IFAddr.new + # i.kind = "vlan" + # i.linkinfo.data = VlanInfo.new(...) + def kind=(str) + self.linkinfo ||= LinkInfo.new + linkinfo.kind = str + end + + def kind?(str) + kind == str + end + + def after_parse #:nodoc: + self.linkinfo = LinkInfo.parse(linkinfo) if linkinfo + end + end + + class LinkInfo < RtattrMessage + rtattr :kind, IFLA_INFO_KIND, :cstring + rtattr :data, IFLA_INFO_DATA # rtattr packed, see below + rtattr :xstats, :IFLA_INFO_XSTATS # don't know + + def after_parse #:nodoc: + case kind + when "vlan" + self.data = VlanInfo.parse(data) + end + end + end + + class VlanFlags < CStruct + field :flags, :uint32 + field :mask, :uint32, :default => 0xffffffff + end + + # VLAN information is packed in rtattr format (there is no corresponding 'struct') + class VlanInfo < RtattrMessage + rtattr :id, IFLA_VLAN_ID, :ushort + rtattr :flags, IFLA_VLAN_FLAGS, + :unpack => lambda { |str,obj| VlanFlags.parse(str) } + rtattr :egress_qos, IFLA_VLAN_EGRESS_QOS + rtattr :ingress_qos, IFLA_VLAN_INGRESS_QOS + end + + # struct ifa_cacheinfo + IFACacheInfo = Struct.new :prefered, :valid, :cstamp, :tstamp + + # struct ifaddrmsg + class IFAddr < RtattrMessage + code RTM_NEWADDR, RTM_DELADDR, RTM_GETADDR + + field :family, :uchar # Socket::AF_* + field :prefixlen, :uchar + field :flags, :uchar # IFA_F_* + field :scope, :uchar # RT_SCOPE_* + field :index, :int + rtattr :address, IFA_ADDRESS, :l3addr + rtattr :local, IFA_LOCAL, :l3addr + rtattr :label, IFA_LABEL, :cstring + rtattr :broadcast, IFA_BROADCAST, :l3addr + rtattr :anycast, IFA_ANYCAST, :l3addr + rtattr :cacheinfo, IFA_CACHEINFO, + :pack => lambda { |val,obj| val.to_a.pack("L*") }, + :unpack => lambda { |str,obj| IFACacheInfo.new(*(str.unpack("L*"))) } + rtattr :multicast, IFA_MULTICAST, :l3addr + end + + module Route + # This class provides an API for manipulating interfaces and addresses. + # Since we frequently need to map ifname to ifindex, or vice versa, + # we keep a memoized list of interfaces. If the interface list changes, + # you should create a new instance of this object. + # + # The object is Enumerable, so you can iterate over it directly + # (which will iterate over the interfaces, but not the addresses) + class IFHandler + include Enumerable + + def initialize(nlsocket = Netlink::Route::Socket.new) + @nlsocket = nlsocket + clear_link_cache + clear_addr_cache + end + + def clear_link_cache + @links = nil + @linkmap = nil + end + + def clear_addr_cache + @addrs = nil + end + + # Download a list of links (interfaces). Either returns an array of + # Netlink::IFInfo objects, or yields them to the supplied block. + # + # res = nl.read_links + # p res + # [#0, :type=>772, :index=>1, + # :flags=>65609, :change=>0, :ifname=>"lo", :txqlen=>0, :operstate=>0, + # :linkmode=>0, :mtu=>16436, :qdisc=>"noqueue", :map=>"...", + # :address=>"\x00\x00\x00\x00\x00\x00", :broadcast=>"\x00\x00\x00\x00\x00\x00", + # :stats32=>#, + # :stats64=>#}>, ...] + def read_links(opt=nil, &blk) + @nlsocket.send_request RTM_GETLINK, IFInfo.new(opt), + NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST + @nlsocket.receive_until_done(RTM_NEWLINK, &blk) + end + + # Download a list of link addresses. Either returns an array of + # Netlink::IFAddr objects, or yields them to the supplied block. + # You will need to use the 'index' to cross reference to the interface. + # + # A hash of kernel options may be supplied, but likely only :family + # is honoured. + # + # res = nl.read_addrs(:family => Socket::AF_INET) + # p res + # [#2, :prefixlen=>8, :flags=>128, :scope=>254, + # :index=>1, :address=>#, + # :local=>#, :label=>"lo"}>, ...] + def read_addrs(opt=nil, &blk) + @nlsocket.send_request RTM_GETADDR, IFAddr.new(opt), + NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST + @nlsocket.receive_until_done(RTM_NEWADDR, &blk) + end + + # Download a list of addresses, grouped as {index=>[addr,addr], index=>[addr,addr]} + def read_addrs_by_ifindex(opt=nil) + res = read_addrs(opt).group_by { |obj| obj.index } + res.default = EMPTY_ARRAY + res + end + + # Return memoized list of all interfaces + def links + @links ||= read_links + end + + # Iterate over all interfaces + def each(&blk) + links.each(&blk) + end + + # Return a memoized Hash of interfaces, keyed by both index and name + def linkmap + @linkmap ||= ( + h = {} + each { |link| h[link.index] = h[link.ifname] = link } + h + ) + end + + # Return details of one interface, given its name or index. + # Raises exception if unknown value. + def [](key) + linkmap.fetch(key) + end + + # Convert an interface index into name string, or nil if the + # index is nil or empty string. Raises exception for unknown values. + # + # nl = Netlink::Route::Socket.new + # nl.rt[Socket::AF_INET].each do |route| + # puts "iif=#{nl.if.name(route.iif)}" + # puts "oif=#{nl.if.name(route.oif)}" + # end + def name(index) + return nil if index.nil? || index == 0 + self[index].ifname + end + + # Convert an interface name into index. Returns 0 for nil or empty + # string. Otherwise raises an exception for unknown values. + def index(name) + return 0 if name.nil? || name == EMPTY_STRING + self[name].index + end + + # Add an interface + # + # require 'netlink/route' + # rt = Netlink::Route::Socket.new + # rt.if.add_link(:type=>, :index=>"eth0") + def add_link(opt) + iplink_modify(RTM_NEWLINK, NLM_F_CREATE|NLM_F_EXCL, opt) + end + + def change_link(opt) + iplink_modify(RTM_NEWLINK, NLM_F_REPLACE, opt) + end + + def replace_link(opt) + iplink_modify(RTM_NEWLINK, NLM_F_CREATE|NLM_F_REPLACE, opt) + end + + # Delete an existing link. Pass in ifname or index, or options + # hash {:index=>n} + def delete_link(opt) + case opt + when Integer + opt = {:index=>opt} + when String + opt = {:index=>index(opt)} + end + iplink_modify(RTM_DELLINK, 0, opt) + end + + def iplink_modify(code, flags, msg) #:nodoc: + msg = IFInfo.new(msg) + + if (flags & NLM_F_CREATE) != 0 + raise "Missing :linkinfo" unless msg.linkinfo + raise "Missing :kind" unless msg.linkinfo.kind + end + + msg.index = index(msg.index) if msg.index.is_a?(String) + msg.link = index(msg.link) if msg.link.is_a?(String) + + @nlsocket.cmd code, msg, flags|NLM_F_REQUEST + clear_link_cache + end + + # Return the memoized address table, keyed by interface name and + # address family, containing an array of addresses for each + # interface/family combination. i.e. + # + # # {ifname=>{family=>[addr,addr,...], ...}, ...} + # puts rt.addrs["eth0"][Socket::AF_INET][0].address + # + # If there are no addresses for a particular family then it will + # return a (frozen) empty array, to make iteration eaiser. + def addrs + @addrs ||= ( + h = {} + links.each do |link| + h[link.ifname] = {} + end + read_addrs.each do |addr| + ifname = name(addr.index) + h[ifname] ||= Hash.new(EMPTY_ARRAY) + (h[ifname][addr.family] ||= []) << addr + end + h + ) + end + + # Add an IP address to an interface + # + # require 'netlink/route' + # rt = Netlink::Route::Socket.new + # rt.add_addr(:index=>"eth0", :local=>"1.2.3.4", :prefixlen=>24) + def add_addr(opt) + ipaddr_modify(RTM_NEWADDR, NLM_F_CREATE|NLM_F_EXCL, opt) + end + + def change_addr(opt) + ipaddr_modify(RTM_NEWADDR, NLM_F_REPLACE, opt) + end + + def replace_addr(opt) + ipaddr_modify(RTM_NEWADDR, NLM_F_CREATE|NLM_F_REPLACE, opt) + end + + # Delete an IP address from an interface. Pass in either a hash of + # parameters, or an existing IFAddr object. + def delete_addr(opt) + ipaddr_modify(RTM_DELADDR, 0, opt) + end + + def ipaddr_modify(code, flags, msg) #:nodoc: + msg = IFAddr.new(msg) + msg.index = index(msg.index) unless msg.index.is_a?(Integer) + msg.address ||= msg.local + # Note: IPAddr doesn't support addresses off the subnet base, + # so there's no point trying to set msg.prefixlen from the IPAddr mask + @nlsocket.cmd code, msg, flags|NLM_F_REQUEST + clear_addr_cache + end + end + end +end diff --git a/lib/netlink/route/rt_handler.rb b/lib/netlink/route/rt_handler.rb new file mode 100644 index 0000000..8de278c --- /dev/null +++ b/lib/netlink/route/rt_handler.rb @@ -0,0 +1,106 @@ +require 'netlink/message' + +module Netlink + # struct rta_cacheinfo + RTACacheInfo = Struct.new :clntref, :lastuse, :expires, :error, :used, :id, :ts, :tsage + + # struct rtmsg + class RT < RtattrMessage + code RTM_NEWROUTE, RTM_DELROUTE, RTM_GETROUTE + + field :family, :uchar # Socket::AF_* + field :dst_len, :uchar + field :src_len, :uchar + field :tos, :uchar + field :table, :uchar # table id or RT_TABLE_* + field :protocol, :uchar # RTPROT_* + field :scope, :uchar # RT_SCOPE_* + field :type, :uchar # RTN_* + field :flags, :uint # RTM_F_* + rtattr :dst, RTA_DST, :l3addr + rtattr :src, RTA_SRC, :l3addr + rtattr :iif, RTA_IIF, :uint32 + rtattr :oif, RTA_OIF, :uint32 + rtattr :gateway, RTA_GATEWAY, :l3addr + rtattr :priority, RTA_PRIORITY, :uint32 + rtattr :prefsrc, RTA_PREFSRC, :l3addr + # Route metrics are themselves packed using the rtattr format. + # In the kernel, the dst.metrics structure is an array of u32. + METRIC_PACK = "SSL".freeze #:nodoc: + METRIC_SIZE = [0,0,0].pack(METRIC_PACK).bytesize #:nodoc: + rtattr :metrics, RTA_METRICS, # {RTAX_* => Integer} + :pack => lambda { |metrics,obj| + metrics.map { |code,val| [METRIC_SIZE,code,val].pack(METRIC_PACK) }.join + }, + :unpack => lambda { |str,obj| + res = {} + RtattrMessage.unpack_rtattr(str) { |code,val| res[code] = val.unpack("L").first } + res + } + rtattr :multipath, RTA_MULTIPATH + rtattr :flow, RTA_FLOW + rtattr :cacheinfo, RTA_CACHEINFO, + :pack => lambda { |val,obj| val.to_a.pack("L*") }, + :unpack => lambda { |str,obj| RTACacheInfo.new(*(str.unpack("L*"))) } + rtattr :table2, RTA_TABLE, :uint32 # NOTE: table in two places! + end + + module Route + # This class manipulates the + class RTHandler + def initialize(nlsocket = Netlink::Route::Socket.new) + @nlsocket = nlsocket + clear_cache + end + + # Send message to download the kernel routing table. Either returns an + # array of Netlink::RT objects, or yields them to the supplied block. + # + # A hash of kernel options may be supplied, but you might also have + # to perform your own filtering. e.g. + # read_routes(:family=>Socket::AF_INET) # works + # read_routes(:protocol=>Netlink::RTPROT_STATIC) # ignored + # + # res = nl.rt.read_routes(:family => Socket::AF_INET) + # p res + # [#2, :dst_len=>32, :src_len=>0, :tos=>0, + # :table=>255, :protocol=>2, :scope=>253, :type=>3, :flags=>0, :table2=>255, + # :dst=>#, + # :prefsrc=>#, :oif=>1}>, ...] + # + # Note that not all attributes will always be present. In particular, + # a defaultroute (dst_len=0) misses out the dst address completely: + # + # [#2, :dst_len=>0, :src_len=>0, :tos=>0, + # :table=>254, :protocol=>4, :scope=>0, :type=>1, :flags=>0, :table2=>254, + # :gateway=>#, :oif=>2}>, ...] + def read_routes(opt=nil, &blk) + @nlsocket.send_request RTM_GETROUTE, RT.new(opt), + NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST + @nlsocket.receive_until_done(RTM_NEWROUTE, &blk) + end + + def clear_cache + @all = nil + end + + # Return the complete memoized route table + def all + @all ||= read_routes + end + + # Iterate over the memoized route table + def each(&blk) + all.each(&blk) + end + + # Return just the routes for the given address family + # + # nl = Netlink::Route::Socket.new + # nl.rt[Socket::AF_INET].each { |r| p r } + def [](family) + all.select { |r| r.family == family } + end + end + end +end