import socket import struct import logging import threading from pytun import TunTapDevice, IFF_TAP ########################## Setting global variables ADDRESS = "127.0.0.1" # Address to listen on PORT = 9999 # Port to listen on NIC_NAME = "server" # Name for the TAP interface that gonna be created CONFIGURE_NIC = True # If you prefer to set up the interface manualy set it to False NIC_IP = "10.0.0.1" # IP to be set for the interface NIC_NETMASK = "255.255.255.0" # Netmask to be used with the interface MTU = 1500 # MTU that gonna be set for the interface, please don't change it if you don't know what you are doing logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s %(message)s\n') # Sets logging level and format. Possible levels: DEBUG, INFO, WARNING, ERROR, CRITICAL Read python logging module documentation for more detailes ## Don't change this if you not sure what you are doing client = {} # Dictionary of clients, Key is the MAC address of a client, Value is an object of class Client client_thread = {} # Client thread contains a Key that is MAC of a client, and object of type Thread, that runs "recive" function FRAME_SIZE = MTU + 42 # MTU is the size of payload, and the frame itself takes 42 bytes ########################## Setting global variables (END) ########################## Defining functions class Client: # Class Client contains a client info and some methods to work with the client def __init__(self, socket, address, NIC, MAC): # Constructor for class Client self.socket = socket self.address = address self.NIC = NIC self.MAC = MAC def recive(self): # Function that listens the client socket for a frames and writes the frames into the NIC. It runs in a loop # Importing global variables global FRAME_SIZE # Read what they are used for in "Defining global variables" section above global client global client_thread buffer_ = " " # Setting buffer to " " so it's not empty and the loop can start while buffer_ != b'': # If buffer_ is b'' that meas that client closed the socket, and needs to be deleted buffer_ = self.socket.recv(FRAME_SIZE) # reciving data from the client if buffer_ != b'': # If buffer_ is still not empty, write it to the server's NIC self.NIC.write(buffer_) logging.debug(f'Recived: From {self.address} {buffer_}') logging.info(f'Connection closed: {self.address}') del client[self.MAC] # Deleting the client from the dictionary, and removing it's "recive" thread del client_thread[self.MAC] def send(self, frame): # Just sends the data to the client's socket self.socket.send(frame) logging.debug(f'Sent: To {self.address} {frame}') def send(client, NIC): # Not to be confused with "send" from the Client class, this one gets all outgoing frames and decides what client it needs to be sent to global FRAME_SIZE # Read what it used for in "Defining global variables" section above while True: buffer_ = NIC.read(FRAME_SIZE) # Reads NIC for outgoing frames dst = struct.unpack("! 4x 6s", buffer_[:10]) # Extracts destination MAC address from the frame if (client.get(dst)): # If client with the destination MAC address exists, send the frame to the client client[dst].send(buffer_) logging.debug(f'Sending: To {dst} {buffer_}') else: # If client with the destination MAC address doesn't exist, then send the frame to all of the clients (Send to broadcast) logging.warning(f'Client with MAC {dst} not found, sending to broadcast') for mac in client.keys(): # Going throught all of the clients, and send the frame to each of them try: # The "try" needs to be here, in case a client closes connection, so instead of crashing the thread, it will print the error message logging.debug(f'Sending a broadcast message to {mac} {buffer_}') client[mac].send(buffer_) except: logging.error('Cannot send the frame to {mac}') def main(): # Importing global variables global NIC_NAME # Read what they are used for in "Defining global variables" section above global MTU global FRAME_SIZE global CONFIGURE_NIC global NIC_IP global NIC_NETMASK global ADDRESS global PORT global client global client_thread # Setting up a TAP interface NIC = TunTapDevice(flags=IFF_TAP, name=NIC_NAME) NIC.mtu = MTU if CONFIGURE_NIC: NIC.addr = NIC_IP NIC.netmask = NIC_NETMASK NIC.up() # Setting up a socket server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.bind((ADDRESS, PORT)) # Creating a "routing" thread, it will read the outgoing frames, and decide to who it will be sent routing = threading.Thread(target=send, args=(client, NIC)); FirstRun = False # Used to only start thread at the end of first cicle of the main loop, if you know more elegant way to do this, please let me know # Main loop try: while True: logging.info("Listening for a new client") server_socket.listen(1) # Listening new_client, new_client_address = server_socket.accept() # When there is a client accepting the connection request and creating socket for the new client (new_client), new_client_addrss contains... new client's address buffer_ = new_client.recv(FRAME_SIZE) # Reciving a frame and putting it to the buffer new_client_mac = struct.unpack("! 10x 6s", buffer_[:16]) # Extracting a source MAC address, to use it later as a Key for the new client logging.info(f'New client connected: {new_client_address} MAC: {new_client_mac}') client[new_client_mac] = Client(new_client, new_client_address, NIC, new_client_mac) # Creating an object of type Client with all the information for the new client, and putting it to "client" dictionary, using the client MAC address as a Key to access the client new_client_thread = threading.Thread(target=client[new_client_mac].recive) # Creating a new thread that runs "recive" method, it will read client's socket for new data and write the data to the server's NIC. It runs in a loop client_thread[new_client_mac] = new_client_thread # Putting the thread to "client_thread" and useing client's MAC as a Key, and the "new_client_thread" object as a value client_thread[new_client_mac].start() # Starting the thread logging.debug(f'Client list: {client.keys()}') if FirstRun is False: # If loop runs the first time, start the routing function, it is required as the function will fail without a client's data (Might be fixed soon) routing.start() FirstRun = True except KeyboardInterrupt: # When CTRL + C pressed run the code below NIC.close() # Closing everything that needs to be closed server_socket.close() # Main loop (END) ########################## Defining functions (END) if __name__ == '__main__': # Starting the Main function main()