user1601871
user1601871

Reputation: 101

Receive UDP packet from specific source

I am trying to measure the responses back from DNS servers. Making a sniffer for a typical DNS response that is less than 512 bytes is no big deal. My issue is receiving large 3000+ byte responses - in some cases 5000+ bytes. I haven't been able to get a socket working that can receive that data reliably. Is there a way with Python sockets to receive from a specific source address?

Here is what I have so far:

import socket
import struct


def craft_dns(Qdns):

    iden = struct.pack('!H', randint(0, 65535))
    QR_thru_RD = chr(int('00000001', 2)) # '\x01'
    RA_thru_RCode = chr(int('00100000', 2)) # '\x00'
    Qcount = '\x00\x01' # question count is 1
    ANcount = '\x00\x00'
    NScount = '\x00\x00'
    ARcount = '\x00\x01' # additional resource count is 1
    pad = '\x00' #
    Rtype_ANY = '\x00\xff' # Request ANY record
    PROtype = '\x00\x01' # Protocol IN || '\x00\xff' # Protocol ANY
    DNSsec_do = chr(int('10000000', 2)) # flips DNSsec bit to enable
    edns0 = '\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00' # DNSsec disabled
    domain = Qdns.split('.')
    quest = ''

    for x in domain:
        quest += struct.pack('!B', len(x)) + x

    packet = (iden+QR_thru_RD+RA_thru_RCode+Qcount+ANcount+NScount+ARcount+
            quest+pad+Rtype_ANY+PROtype+edns0) # remove pad if asking <root>

    return packet

def craft_ip(target, resolv):

    ip_ver_len = int('01000101', 2) # IPvers: 4, 0100 | IP_hdr len: 5, 0101 = 69
    ipvers = 4
    ip_tos  = 0
    ip_len = 0 # socket will put in the right length
    iden = randint(0, 65535)
    ip_frag = 0 # off
    ttl = 255
    ip_proto = socket.IPPROTO_UDP # dns, brah
    chksm = 0 # socket will do the checksum
    s_addr = socket.inet_aton(target)
    d_addr = socket.inet_aton(resolv)

    ip_hdr = struct.pack('!BBHHHBBH4s4s', ip_ver_len, ip_tos, ip_len, iden, 
                    ip_frag, ttl, ip_proto, chksm, s_addr, d_addr)
    return ip_hdr

def craft_udp(sport, dest_port, packet): 

    #sport = randint(0, 65535) # not recommended to do a random port generation
    udp_len = 8 + len(packet) # calculate length of UDP frame in bytes.
    chksm = 0 # socket fills in
    udp_hdr = struct.pack('!HHHH', sport, dest_port, udp_len, chksm)

    return udp_hdr

def get_len(resolv, domain):

    target = "10.0.0.3"
    d_port = 53
    s_port = 5353

    ip_hdr = craft_ip(target, resolv)
    dns_payload = craft_dns(domain) # '\x00' for root
    udp_hdr = craft_udp(s_port, d_port, dns_payload)
    packet = ip_hdr + udp_hdr + dns_payload
    buf = bytearray("-" * 60000)

    recvSock = socket.socket(socket.PF_PACKET, socket.SOCK_RAW, socket.ntohs(0x0800))
    recvSock.settimeout(1)

    sendSock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
    sendSock.settimeout(1)

    sendSock.connect((resolv, d_port))
    sendSock.send(packet)

    msglen = 0
    while True:
        try:

            pkt = recvSock.recvfrom(65535)
            msglen += len(pkt[0])
            print repr(pkt[0])

        except socket.timeout as e:

            break

    sendSock.close()
    recvSock.close()    

    return msglen

result = get_len('75.75.75.75', 'isc.org')
print result

For some reason doing

pkt = sendSock.recvfrom(65535)

Recieves nothing at all. Since I'm using SOCK_RAW the above code is less than ideal, but it works - sort of. If the socket is extremely noisy (like on a WLAN), I could end up receiving well beyond the DNS packets, because I have no way to know when to stop receiving packets when receiving a multipacket DNS answer. For a quiet network, like a lab VM, it works.

Is there a better way to use a receiving socket in this case? Obviously from the code, I'm not that strong with Python sockets.
I have to send with SOCK_RAW because I am constructing the packet in a raw format. If I use SOCK_DGRAM the custom packet will be malformed when sending to a DNS resolver.

The only way I could see is to use the raw sockets receiver (recvSock.recv or recvfrom) and unpack each packet, look if the source and dest address match within what is supplied in get_len(), then look to see if the fragment bit is flipped. Then record the byte length of each packet with len(). I'd rather not do that. It just seems there is a better way.

Upvotes: 1

Views: 730

Answers (1)

user1601871
user1601871

Reputation: 101

Ok I was stupid and didn't look at the protocol for the receiving socket. Socket gets kind of flaky when you try to receive packets on a IPPROTO_RAW protocol, so we do need two sockets. By changing to IPPROTO_UDP and then binding it, the socket was able to follow the complete DNS response over multiple requests. I got rid of the try/catch and the while loop, as it was no longer necessary and I'm able to pull the response length with this block:

recvSock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_UDP)
recvSock.settimeout(.3)
recvSock.bind((target, s_port))

sendSock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
#sendSock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sendSock.settimeout(.3)    
sendSock.bind((target, s_port))

sendSock.connect((resolv, d_port))
sendSock.send(packet)

pkt = recvSock.recvfrom(65535)
msglen = len(pkt[0])

Now the method will return the exact bytes received from a DNS query. I'll leave this up in case anyone else needs to do something similar :)

Upvotes: 1

Related Questions