From b78327c3b6904008ced067fa5c63d57dd81f9998 Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Sun, 1 May 2011 12:32:22 +0100 Subject: [PATCH] Basic NETLINK_FIREWALL support. Highlights need for struct alignment --- examples/c_firewall.c | 119 +++++++++++++++++++++++++++++++++++++++ examples/firewall.rb | 18 ++++++ lib/netlink/constants.rb | 20 +++++++ lib/netlink/firewall.rb | 80 ++++++++++++++++++++++++++ lib/netlink/message.rb | 34 +++++++++++ lib/netlink/nlsocket.rb | 29 ++++++++-- lib/netlink/route.rb | 4 +- 7 files changed, 297 insertions(+), 7 deletions(-) create mode 100644 examples/c_firewall.c create mode 100644 examples/firewall.rb create mode 100644 lib/netlink/firewall.rb diff --git a/examples/c_firewall.c b/examples/c_firewall.c new file mode 100644 index 0000000..17f6a22 --- /dev/null +++ b/examples/c_firewall.c @@ -0,0 +1,119 @@ +/* Adapted from http://people.redhat.com/nhorman/papers/netlink.pdf */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Warning: all messages must be padded to the larger of ipq_verdict_msg + * and ipq_mode_msg (struct ipq_peer_msg), since the kernel enforces this: + * + * // net/ipv4/netfilter/ip_queue.c + * // function ipq_receive_peer + * if (len < sizeof(*pmsg)) + * return -EINVAL; + */ + +int main(void) { + int netlink_socket; + int seq=0; + + struct sockaddr_nl addr; + socklen_t addrlen; + struct nlmsghdr *nl_header = NULL; + struct ipq_mode_msg *mode_data = NULL; + struct ipq_packet_msg *pkt_data = NULL; + struct ipq_verdict_msg *ver_data = NULL; + struct nlmsgerr *nl_error = NULL; + + unsigned char buf1[128]; + unsigned char buf2[128]; + + /*create the socket*/ + netlink_socket = socket(AF_NETLINK,SOCK_RAW,NETLINK_FIREWALL); + + /*set up the socket address structure*/ + memset(&addr,0,sizeof(struct sockaddr_nl)); + addr.nl_family=AF_NETLINK; + addr.nl_pid=0;/*packets are destined for the kernel*/ + addr.nl_groups=0;/*we don’t need any multicast groups*/ + + /* + *we need to send a mode message first, so fill + *out the nlmsghdr structure as such + */ + + nl_header=(struct nlmsghdr *)buf1; + nl_header->nlmsg_type=IPQM_MODE; + nl_header->nlmsg_len=NLMSG_LENGTH(sizeof(struct ipq_peer_msg)); + nl_header->nlmsg_flags=(NLM_F_REQUEST);/*this is a request, don’t ask for an answer*/ + nl_header->nlmsg_pid=getpid(); + nl_header->nlmsg_seq=seq++;/*arbitrary unique value to allow response correlation*/ + + mode_data=NLMSG_DATA(nl_header); + mode_data->value=IPQ_COPY_META; + mode_data->range=0;/*when mode is PACKET, 0 here means copy whole packet*/ + + if(sendto(netlink_socket,(void *)nl_header,nl_header->nlmsg_len,0, + (struct sockaddr *)&addr,sizeof(struct sockaddr_nl)) < 0) { + perror("unable to send mode message"); + exit(0); + } + + /* + *we're ready to filter packets + */ + + for(;;) { + addrlen = sizeof(addr); + if(recvfrom(netlink_socket,buf1,NLMSG_LENGTH(sizeof(struct ipq_packet_msg)), + 0,(struct sockaddr*)&addr,&addrlen) < 0) { + perror("Unable to receive packet message"); + exit(0); + } + + /* + *once we have the packet message, lets extract the header and ancilliary data + */ + + nl_header=(struct nlmsghdr *)buf1; + switch (nl_header->nlmsg_type) { + case IPQM_PACKET: + break; + case NLMSG_ERROR: + nl_error = NLMSG_DATA(nl_header); + fprintf(stderr, "Received error %d\n", nl_error->error); + exit(1); + default: + fprintf(stderr, "Received unexpected packet type %d\n", nl_header->nlmsg_type); + exit(2); + } + + pkt_data=NLMSG_DATA(nl_header); + + /*for the example just forward all packets*/ + + nl_header=(struct nlmsghdr *)buf2; + nl_header->nlmsg_type=IPQM_VERDICT; + nl_header->nlmsg_len=NLMSG_LENGTH(sizeof(struct ipq_verdict_msg)); + nl_header->nlmsg_flags=(NLM_F_REQUEST);/*this is a request, don’t ask for an answer*/ + nl_header->nlmsg_pid=getpid(); + nl_header->nlmsg_seq=seq++;/*arbitrary unique value to allow response correlation*/ + ver_data=(struct ipq_verdict_msg *)NLMSG_DATA(nl_header); + ver_data->value=NF_ACCEPT; + ver_data->id=pkt_data->packet_id; + if(sendto(netlink_socket,(void *)nl_header,nl_header->nlmsg_len,0, + (struct sockaddr *)&addr,sizeof(struct sockaddr_nl)) < 0) { + perror("unable to send mode message"); + exit(0); + } + } +} diff --git a/examples/firewall.rb b/examples/firewall.rb new file mode 100644 index 0000000..e074c79 --- /dev/null +++ b/examples/firewall.rb @@ -0,0 +1,18 @@ +LIBDIR = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) +$LOAD_PATH.unshift LIBDIR + +require 'pp' +require 'netlink/firewall' + +# Example of using Netlink::Firewall to capture all outbound packets +# to TCP port 7551. Use "telnet 127.0.0.1 7551" to test. + +#system("modprobe ip_queue") +#system("modprobe iptable_filter") +#system("iptables -I OUTPUT -j QUEUE -p tcp --destination-port 7551") +nl = Netlink::Firewall::Socket.new +nl.set_mode(Netlink::IPQ_COPY_PACKET, 128) +nl.dequeue_packets do |pkt| + p pkt + Netlink::NF_ACCEPT # Netlink::NF_DROP +end diff --git a/lib/netlink/constants.rb b/lib/netlink/constants.rb index eb1b6f1..aec7952 100644 --- a/lib/netlink/constants.rb +++ b/lib/netlink/constants.rb @@ -268,6 +268,8 @@ module Netlink # ... others to be added as required # linux/if.h + IFNAMSIZ = 16 + IFALIASZ = 256 IFF_UP = 0x1 IFF_BROADCAST = 0x2 IFF_DEBUG = 0x4 @@ -289,4 +291,22 @@ module Netlink IFF_ECHO = 0x40000 IFF_VOLATILE = (IFF_LOOPBACK|IFF_POINTOPOINT|IFF_BROADCAST|IFF_ECHO|\ IFF_MASTER|IFF_SLAVE|IFF_RUNNING|IFF_LOWER_UP|IFF_DORMANT) + + # linux/netfilter.h + NF_DROP = 0 + NF_ACCEPT = 1 + NF_STOLEN = 2 + NF_QUEUE = 3 + NF_REPEAT = 4 + NF_STOP = 5 + + # linux/netfilter_ipv4/ip_queue.h + IPQ_COPY_NONE = 0 + IPQ_COPY_META = 1 + IPQ_COPY_PACKET = 2 + + IPQM_MODE = 17 + IPQM_VERDICT = 18 + IPQM_PACKET = 19 + IPQM_MAX = 20 end diff --git a/lib/netlink/firewall.rb b/lib/netlink/firewall.rb new file mode 100644 index 0000000..a51e5d8 --- /dev/null +++ b/lib/netlink/firewall.rb @@ -0,0 +1,80 @@ +# This file implements the messages and methods for the NETLINK_FIREWALL +# protocol. +# +# TODO: implement multiple queue support (NFQUEUE) + +require 'netlink/nlsocket' +require 'netlink/message' + +module Netlink + # struct ipq_mode_msg + class IPQMode < Message + code IPQM_MODE + + field :value, :uchar # IPQ_* + field_pad 1.size - 1 # FIXME! C structs need aligning + field :range, :size_t + # FIXME! Kernel enforces that IPQM_MODE messages must be at least + # as large as IPQM_VERDICT messages. + field_pad 16 + end + + # struct ipq_packet_msg + class IPQPacket < Message + code IPQM_PACKET + + field :packet_id, :ulong + field :mark, :ulong + field :timestamp_sec, :long + field :timestamp_usec, :long + field :hook, :uint + field :indev_name, :pattern => "Z#{IFNAMSIZ}" + field :outdev_name, :pattern => "Z#{IFNAMSIZ}" + field :hw_protocol, :ns + field :hw_type, :ushort + field :hw_addrlen, :uchar + field :hw_addr, :pattern => "a8" + field_pad 1.size - 1 # FIXME! + field :data_len, :size_t + field :payload, :binary # TODO: clip to data_len + end + + # struct ipq_verdict_msg + class IPQVerdict < Message + code IPQM_VERDICT + + field :value, :uint # NF_* + field_pad 1.size - 4 + field :id, :ulong + field :data_len, :size_t # TODO: auto set from payload.bytesize + field :payload, :binary # optional replacement packet + end + + module Firewall + class Socket < NLSocket + def initialize(opt={}) + super(opt.merge(:protocol => Netlink::NETLINK_FIREWALL)) + end + + # Set mode to IPQ_COPY_META to receive metadata only, IPQ_COPY_PACKET + # to get packet content, or IPQ_COPY_NONE to disable receipt of packets. + # size=0 means copy whole packet, but you can specify a limit instead. + def set_mode(mode, size=0) + send_request IPQM_MODE, IPQMode.new(:value=>mode, :range=>size) + end + + # As packets are received they are yielded to the block. The block + # must return one of the NF_* values, e.g. NF_ACCEPT/NF_DROP. + # nil is treated as NF_ACCEPT. + def dequeue_packets #:yields: pkt + receive_stream(IPQM_PACKET) do |pkt| + verdict = (yield pkt) || NF_ACCEPT + send_request IPQM_VERDICT, IPQVerdict.new( + :value => verdict, + :id => pkt.packet_id + ) + end + end + end + end +end diff --git a/lib/netlink/message.rb b/lib/netlink/message.rb index dc83e8d..bb1e441 100644 --- a/lib/netlink/message.rb +++ b/lib/netlink/message.rb @@ -5,6 +5,9 @@ module Netlink EMPTY_STRING = "".freeze #:nodoc: EMPTY_ARRAY = [].freeze #:nodoc: + NLMSGHDR_PACK = "LSSLL".freeze # :nodoc: + NLMSGHDR_SIZE = [0,0,0,0,0].pack(NLMSGHDR_PACK).bytesize # :nodoc: + # This is the base class from which all Netlink messages are derived. # To define a new Netlink message, make a subclass and then call the # "field" metaprogramming method to define the parts of the message, in @@ -61,6 +64,19 @@ module Netlink define_type :short, :pattern => "s_" define_type :int, :pattern => "i" define_type :long, :pattern => "l_" + define_type :ns, :pattern => "n" + define_type :nl, :pattern => "N" + + SIZE_T_SIZE = Integer(`echo __SIZEOF_SIZE_T__ | gcc -E -P -`) rescue 1.size + define_type :size_t, + case SIZE_T_SIZE + when 8 + {:pattern => "Q"} + when 4 + {:pattern => "L"} + else + raise "Bad size_t" + end define_type :binary, :pattern => "a*", :default => EMPTY_STRING # cstring has \x00 terminator when sent over wire define_type :cstring, :pattern => "Z*", :default => EMPTY_STRING @@ -181,6 +197,11 @@ module Netlink end end + # Skip pad byte(s) - default 1 + def self.field_pad(count=nil) + self::FORMAT << "x#{count}" if count != 0 + end + # Returns the packed binary representation of this message (without # header, and not padded to NLMSG_ALIGNTO bytes) def to_s @@ -308,4 +329,17 @@ module Netlink end end end + + # struct nlmsgerr + class Err < Message + code NLMSG_ERROR + + field :error, :int + #field :msg, :pattern => NLMSGHDR_PACK + field :msg_len, :uint32 + field :msg_type, :uint16 + field :msg_flags, :uint16 + field :msg_seq, :uint32 + field :msg_pid, :uint32 + end end diff --git a/lib/netlink/nlsocket.rb b/lib/netlink/nlsocket.rb index 0450f49..aaeada0 100644 --- a/lib/netlink/nlsocket.rb +++ b/lib/netlink/nlsocket.rb @@ -3,6 +3,13 @@ require 'netlink/constants' require 'netlink/message' module Netlink + ERRNO_MAP = {} #:nodoc: + Errno.constants.each do |k| + klass = Errno.const_get(k) + next unless klass.is_a?(Class) and Class.const_defined?(:Errno) + ERRNO_MAP[klass::Errno] = klass + end + # NLSocket provides low-level sending and receiving of messages across # a netlink socket, adding headers to sent messages and parsing # received messages. @@ -76,9 +83,6 @@ module Netlink ) end - NLMSGHDR_PACK = "LSSLL".freeze # :nodoc: - NLMSGHDR_SIZE = [0,0,0,0,0].pack(NLMSGHDR_PACK).bytesize # :nodoc: - # 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 @@ -126,7 +130,7 @@ module Netlink res = [] blk ||= lambda { |msg| res << msg } junk_handler ||= lambda { |type, flags, seq, pid, msg| - warn "Discarding junk message (#{type}) #{msg}" } if $VERBOSE + 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 @@ -137,7 +141,7 @@ module Netlink when NLMSG_DONE return res when NLMSG_ERROR - raise "Netlink Error received" + 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 @@ -147,6 +151,21 @@ module Netlink end end end + + # Loop infinitely receiving messages of given type(s), ignoring pid and seq. + def receive_stream(*expected_type) + loop do + receive_response(nil) do |type, flags, seq, pid, msg| + if expected_type.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}" + end + end + end + end # Receive one datagram from kernel. Yield header fields plus # Netlink::Message objects (maybe multiple times if the datagram diff --git a/lib/netlink/route.rb b/lib/netlink/route.rb index 589d49d..13d61b5 100644 --- a/lib/netlink/route.rb +++ b/lib/netlink/route.rb @@ -26,7 +26,7 @@ module Netlink code RTM_NEWLINK, RTM_DELLINK, RTM_GETLINK field :family, :uchar # Socket::AF_* - field :pad, :uchar + field_pad field :type, :ushort # ARPHRD_* field :index, :int field :flags, :uint # IFF_* @@ -149,7 +149,7 @@ module Netlink # # res = nl.read_links # p res - # [#0, :pad=>0, :type=>772, :index=>1, + # [#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",