Basic NETLINK_FIREWALL support. Highlights need for struct alignment

This commit is contained in:
Brian Candler
2011-05-01 12:32:22 +01:00
parent aefd94093c
commit b78327c3b6
7 changed files with 297 additions and 7 deletions

119
examples/c_firewall.c Normal file
View File

@@ -0,0 +1,119 @@
/* Adapted from http://people.redhat.com/nhorman/papers/netlink.pdf */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <linux/types.h>
#include <linux/netlink.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4/ip_queue.h>
/*
* 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 dont 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, dont 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, dont 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);
}
}
}

18
examples/firewall.rb Normal file
View File

@@ -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

View File

@@ -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

80
lib/netlink/firewall.rb Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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
@@ -148,6 +152,21 @@ module Netlink
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
# includes multiple netlink messages). Raise an exception if no

View File

@@ -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
# [#<Netlink::IFInfo {:family=>0, :pad=>0, :type=>772, :index=>1,
# [#<Netlink::IFInfo {:family=>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",