[NETENG] Python UDP Programming

All scripts I write for this are found from the latest edition of Foundations of Python Networking by Brandon Rhodes which you can buy here: https://www.amazon.com/Foundations-Python-Network-Programming-Brandon/dp/1430258543

Contrasting the two biggest fields in IT to me is essential, like the concept of yin and yang, the two cannot coexist without the other. The two fields I am talking about are Network Engineering and Cybersecurity; I feel that it is essential to have an understanding of both as opposed to just one because the more you understand about one the more control you have over a network. Network Engineering is about solving problems...well through the use of engineering. Essentially what you are doing is solving problems within a network or making things easier through automation, plain and simple, using programming languages to do so. So to start this topic off I'll give an overview by introducing network programming with python.

To start off, we all know UDP is a non connection oriented protocol that sends out packets in the form of datagrams. Working with the protocol can be a tricky because in the world of cybersecurity clients using the protocol can be seen as promiscuous simply because often times the programmer may forget to check who the sender of the packet they just received is. Here's an example of a dangerous client:

#!/usr/bin/python3
import argparse, socket
from datetime import datetime

def server(port):
        server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        server.bind(('127.0.0.1',port))
        print('Listening at {}'.format(server.getsockname()))
        while True:
                data, address = server.recvfrom(1024)
                text = data.decode('ascii')
                print('Client says {}'.format(text))
                text = 'Your data was {} bytes long'.format(len(data))
                data = text.encode('ascii')
                server.sendto(data, address)

def client(port):
        client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        text = 'The time is {}'.format(datetime.now())
        data = text.encode('ascii')
        client.sendto(data, ('127.0.0.1', port))
        print('The OS assigned me the address {}'.format(client.getsockname()))
        data, address = client.recvfrom(1024) #this is promiscuous, check address before processing!
        text = data.decode('ascii')
        print('The server replied {}'.format(text))

if __name__ == '__main__':
        choices = {'client': client, 'server': server}
        parser = argparse.ArgumentParser(description='Send and receive UDP locally')
        parser.add_argument('role', choices=choices, help='which role to play')
        parser.add_argument('-p', metavar='PORT', type=int, default=1060, help='UDP port (default 1060)')
        args = parser.parse_args()
        function = choices[args.role]
        function(args.p)

Running this on separate terminals one for the client, one for the server will yield the wanted result, but the problem as indicated by the comment is that the client doesn't care who it got the packet from. Pausing the server by using ctrl+Z and using python to send a packet like so:
$ python udp_local.py server
Listening at ('127.0.0.1', 1060)
^Z
[1] + 9370 suspended python udp_local.py server
$ python udp_local.py client
The OS assigned me the address ('0.0.0.0', 39692)
$ python3
Python 3.4.0 (default, Apr 11 2014, 13:05:18)
[GCC 4.8.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
>>> sock.sendto('FAKE'.encode('ascii'), ('127.0.0.1', 39692))

will yield this on the client awaiting the response from the server:
The server ('127.0.0.1', 37821) replied 'FAKE'

Shocking right? A client accepting whatever packets come into it is never good. So to solve this issue, there are a few ways to go about it; for one, you can check for the address and port, that the packet came from, or you can simply use the connect() method to automatically do this. Another underlying issue is connectivity insurance, which is a more common real world scenario, you want to make sure that if a client doesn't get a response from the server that it doesn't simply flood the server with more packets than it actually needs to reprocess the requests, so you introduce a feature to resend packets in a timely manner. I'll give you an example here, and this time with IP addresses included:
#!/usr/bin/python3

import argparse, random, socket, sys
MAX_BYTES = 65535
def server(interface, port):
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.bind((interface, port))
        print('Listening at', sock.getsockname())
        while True:
                data, address = sock.recvfrom(MAX_BYTES)
                if random.random() < 0.5:
                        print('Pretending to drop packet from {}'.format(address))
                        continue
                text = data.decode('ascii')
                print('The client at {} says {!r}'.format(address, text))
                message = 'Your data was {} bytes long'.format(len(data))
                sock.sendto(message.encode('ascii'), address)

def client(hostname, port):
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        hostname = sys.argv[2]
        #using connect rather than just sendto makes the client
        #no longer promiscuous
        sock.connect((hostname, port))
        print('Client socket name is {}'.format(sock.getsockname()))
        delay = 0.1 # seconds
        text = 'This is another message'
        data = text.encode('ascii')
        while True:
                sock.send(data)
                print('Waiting up to {} seconds for a reply'.format(delay))
                sock.settimeout(delay)
                try:
                        data = sock.recv(MAX_BYTES)
                #this is called exponential backoff if the socket times out
                #multiply the delay by two and and resend the data
                except socket.timeout:
                        delay *= 2 # wait even longer for the next request
                        if delay > 2.0:
                                raise RuntimeError('I think the server is down')
                else:
                        break
        # we are done, and can stop looping
        print('The server says {!r}'.format(data.decode('ascii')))

if __name__ == '__main__':
        choices = {'client': client, 'server': server}
        parser = argparse.ArgumentParser(description='Send and receive UDP pretending packets are often dropped')
        parser.add_argument('role', choices=choices, help='which role to take')
        parser.add_argument('host', help='interface the server listens at;host the client sends to')
        parser.add_argument('-p', metavar='PORT', type=int, default=1060,help='UDP port (default 1060)')
        args = parser.parse_args()
        function = choices[args.role]
        function(args.host, args.p)

Now the client is not only less promiscuous, but also not the cause of a potential DOS attack, especially when a timeout a set. Something that can help even further the reliability and reduce the potential for a spoof attack is to use request IDs on the transferring packets, this is a way of checking that you have the right packet to begin with and that the sender isn't a malicious target, I'll let you look into that.

Ideally you want to reduce re-transmission of packets as much as possible within a network. To do this, you want to limit the size of the packet you're sending to the size of the MTU (max transmission unit) of the network, you can do this by attempting to push a a datagram with the maximum size of a packet (64 kB) through a network, and reading out what the MTU of the network is like so:

#!/usr/bin/env python3
import argparse, socket, sys
# Inlined constants, because Python 3.6 has dropped the IN module.
# the IN module used to be a module containing the MTU socket
# options listed below
class IN:
    IP_MTU = 14             #family subprotocol
    IP_MTU_DISCOVER = 10    #operation value
    IP_PMTUDISC_DO = 2      #allow discovery of MTU

if sys.platform != 'linux':
    print('Unsupported: Can only perform MTU discovery on Linux',
          file=sys.stderr)
    sys.exit(1)

def send_big_datagram(host, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.IPPROTO_IP, IN.IP_MTU_DISCOVER, IN.IP_PMTUDISC_DO)
    sock.connect((host, port))
    try:
        sock.send(b'#' * 999999)#here I send 999999 bytes which is way more than 64kB but it works...
    except socket.error:
        print('Alas, the datagram did not make it')
        max_mtu = sock.getsockopt(socket.IPPROTO_IP, IN.IP_MTU)
        print('Actual MTU: {}'.format(max_mtu))
    else:
        print('The big datagram was sent!')

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Send UDP packet to get MTU')
    parser.add_argument('host', help='the host to which to target the packet')
    parser.add_argument('-p', metavar='PORT', type=int, default=1060,
                        help='UDP port (default 1060)')
    args = parser.parse_args()
    send_big_datagram(args.host, args.p)

When you run this to a different client on your network, in my example below I'll use the host with 192.168.1.7, it will show the maximum:
$ ./big_sender 127.0.0.1 -p 9000  # running locally
Alas, the datagram did not make it
Actual MTU: 65535
$ ./big_sender 192.168.1.7 -p 1060 #running against another
Alas, the datagram did not make it
Actual MTU: 1500

A quick recap on the program I wrote earlier shows that I use a concept called socket options. If you don't already know, yes, you can set options on your socket, I have searched extensively for a chart to describe them in detail and here is a guide made by Sichao (I don't know who this is Tongue) :
https://notes.shichao.io/unp/ch7/
The idea in python is to use the getsockopt and setsockopt methods in the socket module to get and set them. The syntax follows, option family first (called the level), then the specific option (called the optname), then any value that the option takes in (0 for most defaults or simply blank). For example getting and setting options for a UDP broadcast looks like so:
value = s.getsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, value)

Ah yes, the power of UDP comes from its ability to send broadcasts. Here's a full scale program that allows you to send a broadcast out into your LAN:
#!/usr/bin/python3
# UDP client and server for broadcast messages on a local LAN

import argparse, socket

BUFSIZE = 65535

def server(interface, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind((interface, port)) #key thing to note here...I'll explain this.
    print('Listening for datagrams at {}'.format(sock.getsockname()))
    while True:
        data, address = sock.recvfrom(BUFSIZE)
        text = data.decode('ascii')
        print('The client at {} says: {!r}'.format(address, text))

def client(network, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) # here I set the option on the client; 1 for yes
    text = 'Broadcast datagram!'
    sock.sendto(text.encode('ascii'), (network, port))

if __name__ == '__main__':
    choices = {'client': client, 'server': server}
    parser = argparse.ArgumentParser(description='Send, receive UDP broadcast')
    parser.add_argument('role', choices=choices, help='which role to take')
    parser.add_argument('host', help='interface the server listens at;'
                        ' network the client sends to')
    parser.add_argument('-p', metavar='port', type=int, default=1060,
                        help='UDP port (default 1060)')
    args = parser.parse_args()
    function = choices[args.role]
    function(args.host, args.p)

Something to take note of in the example above is that for the server to receive broadcast packets, you need to bind it to the interface's broadcast IP. You can find the broadcast IP by looking at the results from ifconfig; the common /24 class C subnet, for example, would have a broadcast IP of 192.168.1.255, so you would parse that as the argument for the server, then when you run it you'll see that the server is able to take in the broadcasts being sent by the client. You can also use the IP loopback 0.0.0.0 to do this, but always try to keep it sensible.