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:
Ignoring ICMP echo handling:
echo 1 | sudo tee /proc/sys/net/ipv4/icmp_echo_ignore_all
Add an iptables rule to drop ICMP echo requests:
sudo iptables -I INPUT -p icmp --icmp-type echo-request -j DROP
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
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?
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:
ip
command, as shown below (assumes TAP device name is tap0
):$ ip link set dev tap0 up
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).