diff --git a/examples/add_addr.rb b/examples/add_addr.rb new file mode 100644 index 0000000..d54c3a5 --- /dev/null +++ b/examples/add_addr.rb @@ -0,0 +1,20 @@ +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 address" +nl.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 } + +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 } + diff --git a/lib/netlink/nlsocket.rb b/lib/netlink/nlsocket.rb index 5e861b4..6a9ea30 100644 --- a/lib/netlink/nlsocket.rb +++ b/lib/netlink/nlsocket.rb @@ -48,6 +48,7 @@ module Netlink # :seq => N (override initial sequence number) # :pid => N (override PID) # :timeout => N (seconds, default to DEFAULT_TIMEOUT. Pass nil for no timeout) + # :junk_handler => lambda { ... } for unexpected packets def initialize(opt) @socket ||= opt[:socket] || ::Socket.new( Socket::AF_NETLINK, @@ -58,6 +59,13 @@ module Netlink @seq = opt[:seq] || Time.now.to_i @pid = opt[:pid] || $$ @timeout = opt.has_key?(:timeout) ? opt[:timeout] : DEFAULT_TIMEOUT + if opt.has_key?(:junk_handler) + @junk_handler = opt[:junk_handler] + elsif $VERBOSE + @junk_handler = lambda { |type, flags, seq, pid, msg| + warn "Discarding junk message (#{type}, #{flags}, #{seq}, #{pid}) #{msg.inspect}" + } + end end # Generate the next sequence number @@ -108,6 +116,18 @@ module Netlink end end + # Send a command and wait for an Errno::NOERROR as confirmation. Raise + # an exception if any error message is returned, or on timeout. + # + # (Compare: rtnl_talk in lib/libnetlink.c, with answer=NULL) + def cmd(type, msg, flags=NLM_F_REQUEST, timeout=@timeout, sockaddr=SOCKADDR_DEFAULT) + send_request(type, msg, flags|NLM_F_ACK, sockaddr) + receive_responses(true, timeout) do |type,msg| + return if type == NLMSG_ERROR + false + end + end + # Discard all waiting messages def drain while select([@socket], nil, nil, 0) @@ -117,57 +137,72 @@ module Netlink end # Loop receiving responses until a DONE message is received (or you - # break out of the loop, or a timeout exception occurs). Filters out - # messages with unexpected pid and seq. If you pass an expected_type then - # messages other than this type will be discarded too. + # break out of the loop, or a timeout exception occurs). # # Yields Netlink::Message objects, or if no block is given, returns an - # array of those objects. If you provide a junk_handler then it will be - # called for discarded messages. + # array of those objects. # # (Compare: rtnl_dump_filter_l in lib/libnetlink.c) - def receive_until_done(expected_type=nil, timeout=@timeout, junk_handler=nil, &blk) #:yields: msg + def receive_until_done(expected_type=nil, timeout=@timeout, &blk) #:yields: msg res = [] - blk ||= lambda { |msg| res << msg } - junk_handler ||= lambda { |type, flags, seq, pid, msg| - warn "Discarding junk message (#{type}, #{flags}, #{seq}, #{pid}) #{msg.inspect}" } if $VERBOSE - loop do - receive_response(timeout) do |type, flags, seq, pid, msg| - if pid != @pid || seq != @seq - junk_handler[type, flags, seq, pid, msg] if junk_handler - next - end - case type - when NLMSG_DONE - return res - when NLMSG_ERROR - raise ERRNO_MAP[-msg.error] || "Netlink Error: #{msg.inspect}" - end - if expected_type && type != expected_type - junk_handler[type, flags, seq, pid, msg] if junk_handler - next - end + blk ||= lambda { |obj| res << obj } + receive_responses(true, timeout) do |type,msg| + return res if type == NLMSG_DONE + if expected_type && type != expected_type + false + else blk.call(msg) if msg end end end - # Loop infinitely receiving messages of given type(s), ignoring pid and seq. + # Loop infinitely receiving responses and yielding message objects + # of the given type. + def receive_stream(expected_type=nil) + receive_responses(false, nil) do |type, msg| + if expected_type && type != expected_type + false + else + yield msg + end + end + end + + # This is the main loop for receiving responses. It optionally checks + # the pid/seq of received messages, and discards those which don't match. # Raises an exception on NLMSG_ERROR. - def receive_stream(*expected_types) + # + # Matching messages are yielded to the block. If the block returns + # false then they are treated as junk. + def receive_responses(check_pid_seq=false, timeout=nil) loop do - receive_response(nil) do |type, flags, seq, pid, msg| - if expected_types.include?(type) - yield msg - elsif type == NLMSG_ERROR - raise ERRNO_MAP[-msg.error] || "Netlink Error: #{msg.inspect}" - else - warn "Received unexpected message type #{type}: #{msg.inspect}" + receive_response(timeout) do |type, flags, seq, pid, msg| + if !check_pid_seq || (pid == @pid && seq == @seq) + if type == NLMSG_ERROR && -msg.error != Errno::NOERROR::Errno + raise ERRNO_MAP[-msg.error] || "Netlink Error: #{msg.inspect}" + end + res = yield type, msg + next unless res == false end + @junk_handler[type, flags, seq, pid, msg] if @junk_handler end end end + # Receive one datagram from kernel. Validates the sender, and returns + # the raw binary message. Raises an exception on timeout or if the + # kernel closes the socket. + def recvmsg(timeout=@timeout) + if select([@socket], nil, nil, timeout) + mesg, sender, rflags, controls = @socket.recvmsg + raise EOFError unless mesg + NLSocket.check_sockaddr(sender.to_sockaddr) + mesg + else + raise "Timeout" + end + end + # Receive one datagram from kernel. Yield header fields plus # Netlink::Message objects (maybe multiple times if the datagram # includes multiple netlink messages). Raise an exception if no @@ -176,23 +211,16 @@ module Netlink # # receive_response { |type, flags, seq, pid, msg| p msg } def receive_response(timeout=@timeout, &blk) # :yields: type, flags, seq, pid, Message - if select([@socket], nil, nil, timeout) - mesg, sender, rflags, controls = @socket.recvmsg - raise EOFError unless mesg - NLSocket.check_sockaddr(sender.to_sockaddr) - parse_yield(mesg, &blk) - else - raise "Timeout" - end + parse_yield(recvmsg(timeout), &blk) end # Parse netlink packet in a string buffer. Yield header fields plus # a Netlink::Message-derived object for each message. For unknown message # types it will yield a raw String, or nil if there is no message body. def parse_yield(mesg) # :yields: type, flags, seq, pid, Message-or-nil - dechunk(mesg) do |h_type, h_flags, h_seq, h_pid, data| - klass = Message::CODE_TO_MESSAGE[h_type] - yield h_type, h_flags, h_seq, h_pid, + dechunk(mesg) do |type, flags, seq, pid, data| + klass = Message::CODE_TO_MESSAGE[type] + yield type, flags, seq, pid, if klass klass.parse(data) elsif data && data != EMPTY_STRING diff --git a/lib/netlink/route.rb b/lib/netlink/route.rb index 0acc3c7..a3409a6 100644 --- a/lib/netlink/route.rb +++ b/lib/netlink/route.rb @@ -136,7 +136,7 @@ module Netlink end module Route - # This is the medium and high-level API using a NETLINK_ROUTE protocol socket + # This class formats and receives messages using NETLINK_ROUTE protocol class Socket < NLSocket def initialize(opt={}) super(opt.merge(:protocol => Netlink::NETLINK_ROUTE)) @@ -208,10 +208,48 @@ module Netlink # 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 = [].freeze + 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 @@ -238,6 +276,21 @@ module Netlink ) 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.