diff --git a/README b/README index 0397761..8338111 100644 --- a/README +++ b/README @@ -15,18 +15,24 @@ themselves are built using class Message or RtattrMessage, which in turn are subclasses of CStruct, which performs the low-level packing and unpacking of the message bodies. - Route Firewall ...etc - | | | - +-------+-------+ - | - v - NLSocket - | - v - Message / RtattrMessage - | - v - CStruct + LinkHandler/ + AddrHandler/ + VlanHandler/ + RouteHandler + | + v + Route Firewall NFLog ...etc + | | | + +-------+-------+ + | + v + NLSocket + | + v + Message / RtattrMessage + | + v + CStruct Useful reference material ========================= diff --git a/examples/add_addr.rb b/examples/add_addr.rb index c08ff04..531edf1 100644 --- a/examples/add_addr.rb +++ b/examples/add_addr.rb @@ -5,17 +5,17 @@ require 'netlink/route' nl = Netlink::Route::Socket.new puts "\n*** Before adding address" -nl.if.addrs["lo"][Socket::AF_INET].each { |x| puts x.address } +nl.addrs.list(:index=>"lo", :family=>Socket::AF_INET) { |x| puts x.address } puts "\n*** After adding address" begin - nl.if.add_addr(:index=>"lo", :local=>"1.2.3.4", :prefixlen=>32) + nl.addrs.add(: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.addrs.list(:index=>"lo", :family=>Socket::AF_INET) { |x| puts x.address } puts "\n*** After deleting 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 } +nl.addrs.delete(:index=>"lo", :local=>"1.2.3.4", :prefixlen=>32) +nl.addrs.list(:index=>"lo", :family=>Socket::AF_INET) { |x| puts x.address } diff --git a/examples/add_route.rb b/examples/add_route.rb new file mode 100644 index 0000000..3dcf17d --- /dev/null +++ b/examples/add_route.rb @@ -0,0 +1,21 @@ +LIBDIR = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) +$LOAD_PATH.unshift LIBDIR + +require 'netlink/route' + +nl = Netlink::Route::Socket.new +puts "\n*** Before adding route" +nl.routes.list(:family=>Socket::AF_INET, :table=>Netlink::RT_TABLE_MAIN) { |x| p x } + +puts "\n*** After adding route" +begin + nl.routes.add(:oif=>"lo", :dst=>"1.2.3.4", :dst_len=>32, :gateway=>"127.0.0.1") +rescue Errno::EEXIST + puts "Already exists" +end +nl.routes.list(:family=>Socket::AF_INET, :table=>Netlink::RT_TABLE_MAIN) { |x| p x } + +puts "\n*** After deleting route" +nl.routes.delete(:oif=>"lo", :dst=>"1.2.3.4", :dst_len=>32, :gateway=>"127.0.0.1") +nl.routes.list(:family=>Socket::AF_INET, :table=>Netlink::RT_TABLE_MAIN) { |x| p x } + diff --git a/examples/add_vlan.rb b/examples/add_vlan.rb index 899445d..0c530fb 100644 --- a/examples/add_vlan.rb +++ b/examples/add_vlan.rb @@ -6,16 +6,16 @@ require 'pp' nl = Netlink::Route::Socket.new puts "\n*** Before adding VLAN" -pp nl.if.links(:kind=>"vlan").to_a +pp nl.vlans.list(:link=>"lo").to_a puts "\n*** After adding VLAN on lo" begin - nl.if.add_vlan(:link=>"lo", :vlan_id=>1234) + nl.vlans.add(:link=>"lo", :vlan_id=>1234) rescue Errno::EEXIST puts "Already present" end -pp nl.if.links(:kind=>"vlan").to_a +pp nl.vlans.list(:link=>"lo").to_a puts "\n*** After deleting VLANs from lo" -nl.if.delete_vlan(:link=>"lo", :vlan_id=>1234) -pp nl.if.links(:kind=>"vlan").to_a +nl.vlans.delete(:link=>"lo", :vlan_id=>1234) +pp nl.vlans.list(:link=>"lo").to_a diff --git a/examples/route_hi.rb b/examples/route_hi.rb index 694a261..8d08a5e 100644 --- a/examples/route_hi.rb +++ b/examples/route_hi.rb @@ -8,9 +8,17 @@ require 'netlink/route' # The data is memoized - that is, it's downloaded from the kernel once # and then manipulated internally. -nl = Netlink::Route::Socket.new -pp nl.if["eth0"] -pp nl.if.addrs["eth0"] +rt = Netlink::Route::Socket.new -# Find the route with the shortest prefix len (probably default route) -pp nl.rt[Socket::AF_INET].min_by { |route| route.dst_len } +puts "\nInterface eth0:" +pp rt.links["eth0"] + +puts "\nAddresses on interface eth0:" +pp rt.addrs.list(:index=>"eth0").to_a + +puts "\nAll routes in main routing table:" +pp rt.routes.list(:family=>Socket::AF_INET, :table=>Netlink::RT_TABLE_MAIN).to_a + +puts "\nDefault route is probably:" +pp rt.routes.list(:family=>Socket::AF_INET, :table=>Netlink::RT_TABLE_MAIN). + min_by { |route| route.dst_len } diff --git a/examples/route_lo.rb b/examples/route_lo.rb index faf3eaa..8d2bf03 100644 --- a/examples/route_lo.rb +++ b/examples/route_lo.rb @@ -7,10 +7,10 @@ require 'netlink/route' # Example of use of low-level API for NETLINK_ROUTE socket. # Each of these method calls performs a netlink protocol exchange. -nl = Netlink::Route::Socket.new +rt = Netlink::Route::Socket.new puts "*** links ***" -pp nl.if.read_links +pp rt.links.read_links puts "*** addrs ***" -pp nl.if.read_addrs(:family => Socket::AF_INET) +pp rt.addrs.read_addrs puts "*** routes ***" -pp nl.rt.read_routes(:family => Socket::AF_INET) +pp rt.routes.read_routes diff --git a/lib/netlink/route.rb b/lib/netlink/route.rb index ae30bae..db575f7 100644 --- a/lib/netlink/route.rb +++ b/lib/netlink/route.rb @@ -1,12 +1,17 @@ -# This file implements the messages and methods for the NETLINK_ROUTE protocol +# This file implements the messages and methods for the NETLINK_ROUTE protocol. +# Apart from a few utility functions for converting ifname to index and vice +# versa, the logic is delegated to separate classes for each entity +# (links, addresses etc) require 'netlink/nlsocket' require 'netlink/message' module Netlink module Route - autoload :IFHandler, 'netlink/route/if_handler' - autoload :RTHandler, 'netlink/route/rt_handler' + autoload :LinkHandler, 'netlink/route/link_handler' + autoload :VlanHandler, 'netlink/route/vlan_handler' + autoload :AddrHandler, 'netlink/route/addr_handler' + autoload :RouteHandler, 'netlink/route/route_handler' # This class formats and receives messages using NETLINK_ROUTE protocol class Socket < NLSocket @@ -14,17 +19,50 @@ module Netlink super(opt.merge(:protocol => Netlink::NETLINK_ROUTE)) end - # 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) + # Return a Netlink::Route::LinkHandler object for manipulating links + def links + @links ||= Netlink::Route::LinkHandler.new(self) + end + + # Return a Netlink::Route::VlanHandler object for manipulating vlans + def vlans + @vlans ||= Netlink::Route::VlanHandler.new(self) + end + + # Return a Netlink::Route::AddrHandler object for manipulating addresses + def addrs + @addrs ||= Netlink::Route::AddrHandler.new(self) end # Return a Netlink::Route::RT object for manipulating routes - def rt(reload=false) - @rt = nil if reload - @rt ||= Netlink::Route::RTHandler.new(self) + def routes + @routes ||= Netlink::Route::RouteHandler.new(self) + 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.routes(:family=>Socket::AF_INET) do |route| + # puts "iif=#{nl.ifname(route.iif)}" + # puts "oif=#{nl.ifname(route.oif)}" + # end + def ifname(index) + return nil if index.nil? || index == 0 + links[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) + case name + when Integer + name + when nil, EMPTY_STRING + 0 + else + links[name].index + end end end end diff --git a/lib/netlink/route/addr_handler.rb b/lib/netlink/route/addr_handler.rb new file mode 100644 index 0000000..a87c029 --- /dev/null +++ b/lib/netlink/route/addr_handler.rb @@ -0,0 +1,141 @@ +require 'netlink/route' + +module Netlink + # 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. + class AddrHandler + def initialize(rtsocket = Netlink::Route::Socket.new) + @rtsocket = rtsocket + clear_cache + end + + def clear_cache + @addrs = nil + end + + def index(v) + @rtsocket.index(v) + end + + def ifname(v) + @rtsocket.ifname(v) + 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) + @rtsocket.send_request RTM_GETADDR, IFAddr.new(opt), + NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST + @rtsocket.receive_until_done(RTM_NEWADDR, &blk) + end + + # Iterate over all addresses, or addressees matching the given + # criteria. Returns an Enumerator if no block given. + # + # The full address list is read once and memoized, so + # it is efficient to call this method multiple times. + # + # nl.addrs.list { |x| p x } + # addrs_eth0 = nl.addrs.list(:index=>"eth0").to_a + # addrs_eth0_v4 = nl.addrs.list(:index=>"eth0", :family=>Socket::AF_INET).to_a + # + # TODO: error on unknown filter conditions + def list(filter=nil, &blk) + @addrs ||= read_addrs + return @addrs.each(&blk) unless filter + return to_enum(:list, filter) unless block_given? + filter[:index] = index(filter[:index]) if filter.has_key?(:index) + @addrs.each do |o| + yield o if (!filter[:family] || o.family == filter[:family]) && + (!filter[:scope] || o.kind?(filter[:scope])) && + (!filter[:flags] || (o.flags & filter[:flags]) == filter[:flags]) && + (!filter[:noflags] || (o.flags & filter[:noflags]) == 0) && + (!filter[:index] || o.index == filter[:index]) + end + end + alias :each :list + + # Return addresses grouped by interface name. e.g. + # addrs_by_interface(:family => Socket::AF_INET).to_a + # #=> {"eth0"=>[addr, addr,...], "lo"=>[addr, addr,...] + # + # The hash has an empty array as its default, so it's safe to do + # addrs_by_interface(...)["eth0"].each { |a| ... } + # even if eth0 has no addresses matching the given filter. + def group_by_interface(*filter) + res = list(*filter).group_by { |a| ifname(a.index) } + res.default = EMPTY_ARRAY + res + end + + # Add an IP address to an interface + # + # require 'netlink/route' + # rt = Netlink::Route::Socket.new + # rt.add(:index=>"eth0", :local=>"1.2.3.4", :prefixlen=>24) + def add(opt) + ipaddr_modify(RTM_NEWADDR, NLM_F_CREATE|NLM_F_EXCL, opt) + end + + def change(opt) + ipaddr_modify(RTM_NEWADDR, NLM_F_REPLACE, opt) + end + + def replace(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(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 + @rtsocket.cmd code, msg, flags|NLM_F_REQUEST + clear_cache + end + end + end +end diff --git a/lib/netlink/route/if_handler.rb b/lib/netlink/route/if_handler.rb deleted file mode 100644 index 30bc742..0000000 --- a/lib/netlink/route/if_handler.rb +++ /dev/null @@ -1,432 +0,0 @@ -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 - field :type, :ushort # ARPHRD_* - field :index, :int - field :flags, :uint # IFF_* - field :change, :uint, :default=>0xffffffff # flags to change - 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. - class IFHandler - 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 - - # Iterate over all interfaces, or interfaces matching the given - # criteria. Returns an Enumerator if no block given. - # - # The full interface list is read once and memoized, so - # it is efficient to call this method multiple times. - # - # if.links { |x| p x } - # ethers = if.links(:type => Netlink::ARPHRD_ETHER).to_a - # vlans = if.links(:kind => "vlan").to_a - # if.links(:flags => Netlink::IFF_RUNNING) - # if.links(:noflags => Netlink::IFF_POINTOPOINT) - # if.links(:link => "lo") # vlan etc attached to this interface - def links(filter=nil, &blk) - return to_enum(:links, filter) unless block_given? - @links ||= read_links - return @links.each(&blk) unless filter - filter[:link] = index(filter[:link]) if filter.has_key?(:link) - @links.each do |l| - yield l if (!filter[:type] || l.type == filter[:type]) && - (!filter[:kind] || l.kind?(filter[:kind])) && - (!filter[:flags] || (l.flags & filter[:flags]) == filter[:flags]) && - (!filter[:noflags] || (l.flags & filter[:noflags]) == 0) && - (!filter[:link] || l.link == filter[:link]) - end - end - - # Return a memoized Hash of interfaces, keyed by both index and name - def linkmap - @linkmap ||= ( - h = {} - links { |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) - case name - when Integer - name - when nil, EMPTY_STRING - 0 - else - self[name].index - end - end - - # Add an interface (low-level) - # - # require 'netlink/route' - # rt = Netlink::Route::Socket.new - # rt.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 - # )))) - - 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 - else - raise "Missing :index" if msg.index.nil? || msg.index == 0 - 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 - - # Higher-level API to manipulate VLAN interface. - # rt.if.add_vlan( - # :link=>"lo", - # :vlan_id=>1234, - # :vlan_flags=>Netlink::VLAN_FLAG_LOOSE_BINDING, - # :vlan_mask=>0xffffffff - # ) - def add_vlan(opt) - add_link(vlan_options(opt)) - end - - def change_vlan(opt) - change_link(vlan_options(opt)) - end - - def replace_vlan(opt) - replace_link(vlan_options(opt)) - end - - # Delete vlan given :link and :vlan_id. If you want to delete - # by :index then call delete_link instead. - def delete_vlan(opt) - raise "Missing vlan_id" unless opt[:vlan_id] - raise "Missing link" unless opt[:link] - link = links(:kind=>"vlan", :link=>opt[:link]).find { |l| - l.linkinfo.data && - l.linkinfo.data.id == opt[:vlan_id] - } - raise Errno::ENODEV unless link - delete_link(link.index) - end - - def vlan_options(orig) #:nodoc: - opt = orig.dup - opt[:link] = index(opt.fetch(:link)) - li = opt[:linkinfo] ||= LinkInfo.new - li.kind = "vlan" - li.data ||= VlanInfo.new - li.data.id = opt.delete(:vlan_id) if opt.has_key?(:vlan_id) - if opt.has_key?(:vlan_flags) - li.data.flags ||= VlanFlags.new(:flags => opt.delete(:vlan_flags)) - li.data.flags.mask = opt.delete(:vlan_mask) if opt.has_key?(:vlan_mask) - end - li.data.egress_qos = opt.delete(:egress_qos) if opt.has_key?(:egress_qos) - li.data.ingress_qos = opt.delete(:ingress_qos) if opt.has_key?(:ingress_qos) - opt - 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 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/link_handler.rb b/lib/netlink/route/link_handler.rb new file mode 100644 index 0000000..ddf5530 --- /dev/null +++ b/lib/netlink/route/link_handler.rb @@ -0,0 +1,253 @@ +require 'netlink/route' + +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 + field :type, :ushort # ARPHRD_* + field :index, :int + field :flags, :uint # IFF_* + field :change, :uint, :default=>0xffffffff # flags to change + 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 + + 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. + class LinkHandler + def initialize(rtsocket = Netlink::Route::Socket.new) + @rtsocket = rtsocket + clear_cache + end + + def clear_cache + @links = nil + @linkmap = nil + end + + def index(v) + @rtsocket.index(v) + end + + # Download a list of links (interfaces). Either returns an array of + # Netlink::IFInfo objects, or yields them to the supplied block. + # + # res = rt.links.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) + @rtsocket.send_request RTM_GETLINK, IFInfo.new(opt), + NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST + @rtsocket.receive_until_done(RTM_NEWLINK, &blk) + end + + # Iterate over all interfaces, or interfaces matching the given + # criteria. Returns an Enumerator if no block given. + # + # The full interface list is read once and memoized, so + # it is efficient to call this method multiple times. + # + # rt.links.list { |x| p x } + # ethers = rt.links.list(:type => Netlink::ARPHRD_ETHER).to_a + # vlans = rt.links.list(:kind => "vlan").to_a + # rt.links.list(:flags => Netlink::IFF_RUNNING) + # rt.links.list(:noflags => Netlink::IFF_POINTOPOINT) + # rt.links.list(:link => "lo") # vlan etc attached to this interface + def list(filter=nil, &blk) + @links ||= read_links + return @links.each(&blk) unless filter + return to_enum(:list, filter) unless block_given? + filter[:link] = index(filter[:link]) if filter.has_key?(:link) + @links.each do |o| + yield o if (!filter[:type] || o.type == filter[:type]) && + (!filter[:kind] || o.kind?(filter[:kind])) && + (!filter[:flags] || (o.flags & filter[:flags]) == filter[:flags]) && + (!filter[:noflags] || (o.flags & filter[:noflags]) == 0) && + (!filter[:link] || o.link == filter[:link]) + end + end + alias :each :list + + # Return a memoized Hash of interfaces, keyed by both index and name + def linkmap + @linkmap ||= ( + h = {} + list { |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 + + # Add an interface (raw). e.g. + # + # require 'netlink/route' + # rt = Netlink::Route::Socket.new + # rt.links.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 + # )))) + + def add(opt) + iplink_modify(RTM_NEWLINK, NLM_F_CREATE|NLM_F_EXCL, opt) + end + + def change(opt) + iplink_modify(RTM_NEWLINK, NLM_F_REPLACE, opt) + end + + def replace(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(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 + else + raise "Missing :index" if msg.index.nil? || msg.index == 0 + end + + msg.index = index(msg.index) if msg.index.is_a?(String) + msg.link = index(msg.link) if msg.link.is_a?(String) + + @rtsocket.cmd code, msg, flags|NLM_F_REQUEST + clear_cache + end + end + end +end diff --git a/lib/netlink/route/rt_handler.rb b/lib/netlink/route/route_handler.rb similarity index 50% rename from lib/netlink/route/rt_handler.rb rename to lib/netlink/route/route_handler.rb index 8de278c..b2794fa 100644 --- a/lib/netlink/route/rt_handler.rb +++ b/lib/netlink/route/route_handler.rb @@ -1,4 +1,4 @@ -require 'netlink/message' +require 'netlink/route' module Netlink # struct rta_cacheinfo @@ -46,13 +46,21 @@ module Netlink end module Route - # This class manipulates the - class RTHandler - def initialize(nlsocket = Netlink::Route::Socket.new) - @nlsocket = nlsocket + # This class manipulates the kernel routing table + class RouteHandler + def initialize(rtsocket = Netlink::Route::Socket.new) + @rtsocket = rtsocket clear_cache end + def clear_cache + @routes = nil + end + + def index(v) + @rtsocket.index(v) + end + # Send message to download the kernel routing table. Either returns an # array of Netlink::RT objects, or yields them to the supplied block. # @@ -61,7 +69,7 @@ module Netlink # read_routes(:family=>Socket::AF_INET) # works # read_routes(:protocol=>Netlink::RTPROT_STATIC) # ignored # - # res = nl.rt.read_routes(:family => Socket::AF_INET) + # res = rt.routes.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, @@ -75,31 +83,89 @@ module Netlink # :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), + @rtsocket.send_request RTM_GETROUTE, RT.new(opt), NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST - @nlsocket.receive_until_done(RTM_NEWROUTE, &blk) + @rtsocket.receive_until_done(RTM_NEWROUTE, &blk) end - def clear_cache - @all = nil + # Return the memoized route table, filtered according to + # the optional criteria. Examples: + # :family => Socket::AF_INET + # :table => Netlink::RT_TABLE_DEFAULT + # :protocol => Netlink::RTPROT_STATIC + # :type => Netlink::RTN_UNICAST + # :scope => Netlink::RT_SCOPE_HOST + # :flags => Netlink::RTM_F_NOTIFY + # :noflags => Netlink::RTM_F_CLONED + # :oif => "eth0" + # :iif => "eth1" + def list(filter=nil, &blk) + @routes = read_routes + return @routes.each(&blk) unless filter + return to_enum(:list, filter) unless block_given? + filter[:oif] = index(filter[:oif]) if filter.has_key?(:oif) + filter[:iif] = index(filter[:iif]) if filter.has_key?(:iif) + @routes.each do |o| + yield o if (!filter[:family] || o.family == filter[:family]) && + (!filter[:table] || o.table == filter[:table]) && + (!filter[:protocol] || o.protocol == filter[:protocol]) && + (!filter[:type] || o.scope == filter[:protocol]) && + (!filter[:scope] || o.type == filter[:type]) && + (!filter[:flags] || (o.flags & filter[:flags]) == filter[:flags]) && + (!filter[:noflags] || (o.flags & filter[:noflags]) == 0) && + (!filter[:oif] || o.oif == filter[:oif]) && + (!filter[:iif] || o.iif == filter[:iif]) + end + end + alias :each :list + + def add(opt) + iproute_modify(RTM_NEWROUTE, NLM_F_CREATE|NLM_F_EXCL, opt) + end + + def change(opt) + iproute_modify(RTM_NEWROUTE, NLM_F_REPLACE, opt) end - # Return the complete memoized route table - def all - @all ||= read_routes + def replace(opt) + iproute_modify(RTM_NEWROUTE, NLM_F_CREATE|NLM_F_REPLACE, opt) + end + + def prepend(opt) + iproute_modify(RTM_NEWROUTE, NLM_F_CREATE, opt) + end + + def append(opt) + iproute_modify(RTM_NEWROUTE, NLM_F_CREATE|NLM_F_APPEND, opt) + end + + def test(opt) + iproute_modify(RTM_NEWROUTE, NLM_F_EXCL, opt) + end + + def delete(opt) + iproute_modify(RTM_DELROUTE, 0, opt) 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 } + def iproute_modify(code, flags, msg) #:nodoc: + msg = RT.new(msg) + + msg.table ||= RT_TABLE_MAIN + msg.metrics ||= [] + if code != RTM_DELROUTE + msg.protocol ||= RTPROT_BOOT + msg.scope ||= RT_SCOPE_UNIVERSE + msg.type ||= RTN_UNICAST + else + msg.scope ||= RT_SCOPE_NOWHERE + end + # Note: there are more complex rules in ip/iproute.c for setting defaults + + msg.iif = index(msg.iif) if msg.iif.is_a?(String) + msg.oif = index(msg.oif) if msg.oif.is_a?(String) + + @rtsocket.cmd code, msg, flags|NLM_F_REQUEST + clear_cache end end end diff --git a/lib/netlink/route/vlan_handler.rb b/lib/netlink/route/vlan_handler.rb new file mode 100644 index 0000000..a9538ab --- /dev/null +++ b/lib/netlink/route/vlan_handler.rb @@ -0,0 +1,68 @@ +require 'netlink/route' + +module Netlink + module Route + class VlanHandler + def initialize(rtsocket = Netlink::Route::Socket.new) + @rtsocket = rtsocket + end + + def index(v) + @rtsocket.index(v) + end + + def list(filter={}, &blk) + @rtsocket.links.list(filter.merge(:kind=>"vlan")) + end + alias :each :list + + # Higher-level API to manipulate VLAN interface. + # nl.vlans.add( + # :link=>"lo", + # :vlan_id=>1234, + # :vlan_flags=>Netlink::VLAN_FLAG_LOOSE_BINDING, + # :vlan_mask=>0xffffffff + # ) + def add(opt) + @rtsocket.links.add(vlan_options(opt)) + end + + def change(opt) + @rtsocket.links.change(vlan_options(opt)) + end + + def replace(opt) + @rtsocket.links.replace(vlan_options(opt)) + end + + # Delete vlan given :link and :vlan_id. If you want to delete + # by :index then call links.delete instead. + def delete(opt) + raise "Missing vlan_id" unless opt[:vlan_id] + raise "Missing link" unless opt[:link] + link = list(:link=>opt[:link]).find { |l| + l.linkinfo.data && + l.linkinfo.data.id == opt[:vlan_id] + } + raise Errno::ENODEV unless link + @rtsocket.links.delete(link.index) + end + + def vlan_options(orig) #:nodoc: + opt = orig.dup + opt[:link] = index(opt.fetch(:link)) + li = opt[:linkinfo] ||= LinkInfo.new + li.kind = "vlan" + li.data ||= VlanInfo.new + li.data.id = opt.delete(:vlan_id) if opt.has_key?(:vlan_id) + if opt.has_key?(:vlan_flags) + li.data.flags ||= VlanFlags.new(:flags => opt.delete(:vlan_flags)) + li.data.flags.mask = opt.delete(:vlan_mask) if opt.has_key?(:vlan_mask) + end + li.data.egress_qos = opt.delete(:egress_qos) if opt.has_key?(:egress_qos) + li.data.ingress_qos = opt.delete(:ingress_qos) if opt.has_key?(:ingress_qos) + opt + end + end + end +end