You were apparently using Python2 back in 2019, I will still answer your question but using Python3 because I think it will be more helpful to people ending up here.
When using the TPROXY feature of the Linux kernel, in order to get the destination address of an incoming datagram, you must use the low level primitive socket.recvmsg()
. You will find its official documentation here.
You must also set the socket option IP_RECVORIGDSTADDR
which according to man 7 ip
:
Enables the IP_ORIGDSTADDR ancillary message in
recvmsg(2), in which the kernel returns the original destination address of
the datagram being received. The ancillary message contains a struct
sockaddr_in.
The sshuttle
tool created a wrapper function for it. I rewrote it to be more generic and easy to use for anyone:
import socket
import struct
from typing import Tuple
Host = Tuple[str, int]
# They are not in the standard library yet !
IP_RECVORIGDSTADDR = 20
SOL_IPV6 = 41
IPV6_RECVORIGDSTADDR = 74
def recv_tproxy_udp(bind_sock, bufsize) -> Tuple[Host, Host, bytes]:
max_ancillary_size = 28 # sizeof(struct sockaddr_in6)
data, ancdata, flags, client = bind_sock.recvmsg(bufsize,
socket.CMSG_SPACE(max_ancillary_size))
for cmsg_level, cmsg_type, cmsg_data in ancdata:
# Handling IPv4
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVORIGDSTADDR:
family, port = struct.unpack('=HH', cmsg_data[0:4])
port = socket.htons(port)
if family != socket.AF_INET:
raise TypeError(f"Unsupported socket type '{family}'")
ip = socket.inet_ntop(family, cmsg_data[4:8])
destination = (ip, port)
return client, destination, data
# Handling IPv6
elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_RECVORIGDSTADDR:
family, port = struct.unpack('=HH', cmsg_data[0:4])
port = socket.htons(port)
if family != socket.AF_INET6:
raise TypeError(f"Unsupported socket type '{family}'")
ip = socket.inet_ntop(family, cmsg_data[8:24])
destination = (ip, port)
return client, destination, data
raise ValueError("Unable to parse datagram")
Don't forget that in order to handle TPROXY, you must also set the IP_TRANSPARENT
option using setsockopt()
, and that it requires you to run your script as root:
server_sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
server_sock.setsockopt(socket.SOL_IP, socket.IP_TRANSPARENT, 1)
server_sock.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
server_sock.bind((bind_addr, bind_port))