getting exotic socket options in python

note: this post appeared here because of me not reading the documentation properly.

more specifically, after reading python's socket module documentation i understood that you can't get to read socket options that are not defined in the libc headers. more, i concluded that you can't get to read anything else than integers either, once i also missed the explanation for bufflen argument. ofcourse, this is totally false but by the time i figured i'm stupid i already had two other ways of getting one of the exotic socket options installed by third party system software. specifically, the SO_ORIGINAL_DST option defined by linux netfilter.  one way was by writing a C extension (not gonna talk about it here as i don't have anything new on the subject) and the other was by using ctypes module to load and call libc' s getsockopt. however i will provide in the end the correct one-liner that does it right without any headache... if you know your weapon.

part 1: an example on how to use ctypes to call C functions and pass C structures as arguments.
SO_ORIGINAL_DST requires optval to be a sockaddr_in. so we need to load libc, get a handle for getsockopt and call it with (ofcourse) the signature as defined in libc header.

sockaddr_in is defined like this in netinet/in.h header:
struct sockaddr_in
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;                 /* Port number.  */
    struct in_addr sin_addr;            /* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
                           __SOCKADDR_COMMON_SIZE -
                           sizeof (in_port_t) -
                           sizeof (struct in_addr)];

to expand the preprocessor macros and see what's really inside you can run

gcc -E /usr/include/netinet/in.h

the output we're interested on is:
struct sockaddr_in
    sa_family_t sin_family;
    in_port_t sin_port;
    struct in_addr sin_addr;

    unsigned char sin_zero[sizeof (struct sockaddr) -
      (sizeof (unsigned short int)) -
      sizeof (in_port_t) -
      sizeof (struct in_addr)];

we need to emulate this structure in python:

class sin_addr(ctypes.Structure):

class sockaddr_in(ctypes.Structure):

and compute the length of sockaddr_in struct. it's 16. save it in a variable. get a handle on libc's getsockopt and call it properly (with pointers for our sockaddr_in structure and the var holding its length (sk_new is a newly created socket, normally returned by socket.accept() in our case: netfilter and SO_ORIGINAL_DST) :
libc_handle = cdll.LoadLibrary('')
rc=libc_handle.getsockopt(c_int32(sk_new.fileno()), socket.SOL_IP,80, pointer(sadd), pointer(sockaddr_in_len))

don't forget port and addr are both in network byte order so you need to make them look like little endians first:

 ipaddr = socket.inet_ntoa(pack('BBBB',(ipint>>24)&0xff,(ipint>>16)&0xff,(ipint>>8)&0xff,ipint&0xff))

you now have the ip address as  string in ipaddr and the port properly formatted in port.

part 2: the one-liner or the correct way to get a socket option in python

getsockopt will return a buffer of bytes if you specify a bufflen to getsockopt. knowing the internals of sockaddr_in structure you can easily unpack it and extract the info you are looking for.
the pack format explained:

  • ! we have big endians coming
  • 2x this is padding and we don't care about it (well it in fact contains AF_INET but we don't care) 
  • H there's an unsigned short we're interested in here, the port
  • 4s a 4char array containing the ip address bytes
  • 8x this is some padding. really ;)

getsockopt doesn't really care about what socket option you ask for. it's just an integer and if you pass it like that it will use it blindly to make the system call. its true that socket module isn't aware of SO_ORIGINAL_DST definition, not being in libc's headers but in netfilter headers. but also it's true that socket module wouldn't mind of using the integer you want.

No comments:

Post a Comment