1
votes

I'm implementing a simple userspace networking stack for self-learning purposes. I'm writing it in Python, running it in Linux (Ubuntu 16.04.2 LTS). I'm using a Python TAP device to receive Layer 2 frames (e.g. Ethernet). From there, I extract the headers and process frames according to header fields.

Problem: The TAP device receives several types of frames, however not ICMP packets (e.g. ICMP echo requests). I would like it to receive ICMP echo requests too.

Details: To test the behavior of the stack I'm running ping 10.0.0.4 on the same machine. My Ubuntu environment is running on a VM, and so I've also tried running ping 10.0.0.4 from the host machine (after adding the appropriate entry to the routing table). I always get ICMP echo replies, even though the TAP device sees none of the echo requests:

PING 10.0.0.4 (10.0.0.4): 56 data bytes
64 bytes from 10.0.0.4: icmp_seq=0 ttl=64 time=0.451 ms
64 bytes from 10.0.0.4: icmp_seq=1 ttl=64 time=0.530 ms

Here's the packet handling code (simplified for the purposes of this question):

from pytun import TunTapDevice, IFF_TAP, IFF_NO_PI

tap_dev = TunTapDevice(flags = (IFF_TAP | IFF_NO_PI))
tap_dev.persist(True)
tap_dev.addr = '10.0.0.4'
tap_dev.netmask = '255.255.255.0'
tap_dev.up()

while (1):
    frame = tap_dev.read(1500)
    # extract the Ethernet header from the raw frame 
    # (assume this is working correctly)
    eth_frame_hdr = unpack_eth_hdr(frame)

    # check if it is an IPv4 packet
    if eth_frame_hdr.type == 0x0800:
        ipv4_hdr = unpack_ipv4_hdr(frame)

        # check if an icmp packet
        if ipv4_hdr.proto == 0x01:
            process_icmp(frame)

My diagnosis: I think what's happening is that the Linux kernel is handling the ICMP echo requests directly, and either (1) doesn't even put a packet 'on the wire' or (2) doesn't pass the ICMP packets to userspace.

(Failed) resolution attempts: I've tried several things to get ICMP packets on the TAP device, none of them resulted in the TAP device receiving the ICMP echo requests:

  1. Ignoring ICMP echo handling:

    echo 1 | sudo tee /proc/sys/net/ipv4/icmp_echo_ignore_all

  2. Add an iptables rule to drop ICMP echo requests:

    sudo iptables -I INPUT -p icmp --icmp-type echo-request -j DROP

  3. Add an iptables rule which 'jumps' to the QUEUE target (idea was to pass ICMP packets to userspace):

    sudo iptables -I INPUT -p icmp --icmp-type echo-request -j QUEUE

  4. Use a raw socket as a special case to handle ICMP packets:

    from socket import * icmp_listener_sock = socket(AF_PACKET, SOCK_RAW, IPPROTO_ICMP) icmp_listener_sock.bind((tap_dev.name, IPPROTO_ICMP)) (icmp_ipv4_dgram, snd_addr) = icmp_listener_sock.recvfrom(2048) process_icmp(icmp_ipv4_dgram)

Can you point me to the right way to have the Python TAP device receive the ICMP echo requests?

1
Are you really using python-pytun 0.2? I see they have releases with much more attractive version numbers.phd
Hi, no: I'm using python-pytun 2.2.1, that was just the first link I've found on Google (corrected now).fortune_pickle

1 Answers

0
votes

I reviewed solution attempt 4, and made it work by changing AF_PACKET to AF_INET when creating the raw socket and binding the socket to the address (<ip-address>, 0).

from socket import * 
icmp_listener_sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)
icmp_listener_sock.bind((tap_dev.ip_addr, 0))
(icmp_ipv4_dgram, snd_addr) = icmp_listener_sock.recvfrom(2048)
process_icmp(icmp_ipv4_dgram)

Note that this is a workaround, it doesn't answer the question on how to get ICMP packets with a Python TAP device.

EDIT (and definitive answer):

I've tried a different approach, which is not listed in the original post. Instead of using the python-pytun package, I've directly opened Linux's TUN/TAP device, using Python-like open() and ioctl() system calls.

It works very well, and it doesn't require the raw socket workaround to handle ICMP packets.

In hindsight, this is the approach I should have followed to begin with...

Here's a minimal example of how to do it:

import os
import struct
from fcntl import ioctl

# ioctl constants
TUNSETIFF = 0x400454ca
TUNSETPERSIST = 0x400454cb    
IFF_TUN = 0x0001
IFF_TAP = 0x0002
IFF_NO_PI = 0x1000
SIOCGIFHWADDR = 0x00008927

try:
    # tap device name
    tap_devname = 'tap0'

    # open tap device
    tap_fd = os.open('/dev/net/tun', os.O_RDWR)

    # set tap device flags via ioctl():
    #
    # IFF_TUN   : tun device (no Ethernet headers)
    # IFF_TAP   : tap device
    # IFF_NO_PI : do not provide packet information, otherwise we end 
    #             up with unnecessary packet information prepended to 
    #             the Ethernet frame
    ifr = struct.pack("16sH", ("%s" % (tap_devname)), IFF_TAP | IFF_NO_PI)
    ioctl(tap_fd, TUNSETIFF, ifr)

    # set device to persistent (if needed be, if not, comment the next line)
    ioctl(tap_fd, TUNSETPERSIST, 1)

    print("[INFO] tap device w/ name %s allocated" % (ifr[:16].strip("\x00")))

except Exception as e:
    print("[ERROR] cannot setup tap device (%s)" % (e.message))

Note: after the above, you should do 2 things to make the TAP device operational:

  1. Bring the TAP device up. E.g. in Linux, this could be accomplished with the ip command, as shown below (assumes TAP device name is tap0):

$ ip link set dev tap0 up

  1. Probably you'll want to associate an IP address with your TAP device. You should add a route table entry so that packets directed to that address are forwarded over the tap0 interface (assumes IP address to associate is 10.0.0.4, and a 255.255.255.0 netmask:

$ ip route add dev tap0 10.0.0.4/24

You can do the above in Python too, using Python's subprocess package (see example in my Github).