Skip to content

Client / Server architecture

The network computer that contains the hard drives, printers, and other shared resources with other network computers is called a server.Any computer that’s not a server is considered as a client. A client is just a process or a script that makes a request (asking for a webpage, a response from OpenAI API...) to a process/script called server who processes the request and responds (sends you the the webpage, OpenAI completion response...). The communication is usually done over a network, allowing clients and servers to interact, even from different corners of the world.

This model is at the heart of most internet communications and many internal network applications.

Only two kinds of computers are on a network: servers and clients, let's try to understand client/server relation with an example. Imagine you are in a restaurant for dinner :

  1. Client (You): You are hungry and want to eat, so you decide to go to a restaurant. You are like the "client" in the computer world, who has a request or need.
  2. Server (Waiter): When you sit down, a waiter comes to your table to take your order. In the computer world, the "server" is the system that awaits and responds to requests from clients.
  3. Request: You tell the waiter that you want a bowl of soup. This is like a client sending a request to a server over the network. Processing: The waiter takes your order to the kitchen where it's prepared. Similarly, the server processes the client's request.
  4. Response: The waiter brings the soup to your table. In the computer world, the server sends back the requested data (or a response) to the client.
  5. Consumption: You enjoy your soup. Similarly, the client uses the data received from the server.

TCP & UDP

Now an other image to understand, let's imagine the Internet is like a highway. Now, on this highway, you can choose different types of vehicles to deliver your goods (data). TCP and UDP are two different vehicles you can choose, and each has its own style (pros and cons) of delivering goods.

We have seen earlier that TCP and UDP are in the layer 4 of the OSI model. You can consider the OSI model like the rulebook for the Internet highway. It has different chapters (layers) explaining how vehicles should behave on the road. The layer 4 is the chapter where TCP and UDP are discussed like you can see on the schema above. It talks about how data should be sent from one point to another and how to ensure it arrives correctly.

Let's continue with the example of delivery, imagine TCP like a reliable delivery truck:

  • Delivery Confirmation: TCP, like a meticulous delivery truck driver, makes sure that all your goods are delivered to the destination, and it gets a signature (acknowledgment or ACK) for each delivery 🤓
  • Ordered Delivery: TCP also ensures that all goods are delivered in the exact order they were sent, just like a delivery truck following a strict checklist ✅
  • Error-checking: If there's a roadblock or issue along the way, TCP will figure it out and ensure the delivery is successful, even if it has to resend some goods 🚛

The main advantages of TCP/IP Model are : - TCP/IP supports various Network Routing Protocols. - It is scalable and based on client-server architecture. - It is an open protocol suite i.e., it’s not proprietary, so anyone can use it. - TCP/IP works independently of the OS.

Sockets

Think back for a second to what we just finished discussing, the TCP/UDP protocols. The Internet layer gives us just an IP Address. The Transport layer establishes the idea of a port. The Application layer doesn’t care about what happens below... But we need to communicate out of our applications to other applications. We can do that by knowing an endpoint (IP:Port) A Socket is the software representation of that endpoint. Opening (or binding) a socket creates a kind of transceiver that can send and/or receive bytes at a given IP address and Port.

Binding a socket (tuff word lol) is like setting up a desk in a particular office room where people know they can find you. In networking terms, it's about assigning a specific address and port to a socket, so that network traffic knows where to find and communicate with your service.

In a more formal term, a socket is one endpoint of a two way communication link between two programs running on the network. The socket mechanism provides a means of inter-process communication (IPC) by establishing named contact points between which the communication take place. The socket provides bidirectional FIFO Communication facility over the network.

A socket connecting to the network is created at each end of the communication. Each socket has a specific address. This address is composed of an IP address and a port number.

Socket are generally employed in client server applications. The server creates a socket, attaches it to a network port addresses then waits for the client to contact it. The client creates a socket and then attempts to connect to the server socket. When the connection is established, transfer of data takes place, more details on Geeksforgeeks

In Python we will be using the socket module who provides a way of using a variety of socket-based services.

TCP Server

Programmers have many third-party tools to create networked servers and clients in Python like Flask, FastAPI, tornado... But the core module for all of those tools is socket. Creating TCP servers in Python with the socket module is just as easy as creating a client. You might want to use your own TCP server when writing command shells or crafting a proxy (both of which we’ll see later).

This module exposes all of the necessary pieces to quickly write Transmission Control Protocol (TCP) and User Datagram Protocol (UDP) clients and servers and use raw sockets (more informations on raw sockets).

For the purposes of breaking in or maintaining access to target machines, this module is all we really need. Let’s start by creating some simple clients and servers. During penetration tests, we have needed to whip up a TCP client to test for services, send garbage data, fuzz, or perform any number of other tasks. So it's always good to know how to code simple server/client scripts.

Let’s start by creating a standard python multithreaded TCP server 😎

simple_server_tcp.py
import socket
import threading

IP = '0.0.0.0'
PORT = 9998

def main():
    #create the socket object and specifies the address family, which in our case is IPv4
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    #binding the socket 
    server.bind((IP, PORT))
    #sets up the server to listen for incoming connections, with a backlog of 5 (it can queue up to 5 connection requests before refusing new ones)
    server.listen(5)
    print(f'[*] Listening on {IP}:{PORT}')

    while True:
        #continuously accepts new connections and blocks until a new connection is made 
        client, address = server.accept()
        print(f'[*] Accepted connection from {address[0]}:{address[1]}')
        #for each new connection, a new thread is created to handle client communication, ensuring that the server can handle multiple clients concurrently that's wh ywe call it multithreaded TCP server at the beginning
        client_handler = threading.Thread(target=handle_client, args=(client,))
        client_handler.start()

def handle_client(client_socket):
    with client_socket as sock:
        #data is received from the client with a buffer size of 1024 bytes (random you can change this)
        request = sock.recv(1024)
        print(f'[*] Received: {request.decode("utf-8")}')
        #some response is sent back to the client 
        sock.send(b'200 - OK')


if __name__ == '__main__':
    main()

Just run this script and open an other terminal window for sending informations to our server (act like a client) with the curl command. If you do not know this command I recommend this incredible tutorial on curl and it's 1000th utilisations ! For now let just get

curl -X GET "localhost:9998"
or
curl -X POST http://localhost:9998 \                                        
    -H 'content-type: application/json' \
    -d '{ "example":"hello my first baby packet" }'

You will get on your server terminal the following response :

[*] Accepted connection from 127.0.0.1:58153
[*] Received: POST / HTTP/1.1
Host: localhost:9998
User-Agent: curl/7.64.1
Accept: */*
content-type: application/json
Content-Length: 39

{ "example":"hello my first baby packet" }

Now let's try to code a simple client script to send some informations to our python server, because as you may notice our curl commands are not very easy to use 😂

TCP Client

If you are working within the confines of large enterprise environments, you won’t have the luxury of using networking tools or compilers, and sometimes you’ll even be missing the absolute basics, like the ability to copy/paste or connect to the internet 😭

This is where being able to quickly create a TCP client comes in extremely handy. Let’s get coding this simple TCP client :

simple_client_tcp.py
import socket

IP = '0.0.0.0'
PORT = 9998

#define a send_message function that takes a message argument
def send_message(message):
    #create a socket object client using socket.socket() and connect to the server using the client.connect() method, specifying the IP address and port of the server
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect((IP, PORT))
    #send test message encoding the message to bytes using utf-8 encoding
    client.send(message.encode('utf-8'))
    #receive a response from the server using the client.recv() method, specifying a buffer size of 4096 bytes (you can change this if you want)
    response = client.recv(4096)
    #print the received response, decoding it from bytes to a string using utf-8 encoding
    print(f'Received response from server : {response.decode("utf-8")}')
    #close the connection
    client.close()

if __name__ == '__main__':
    print(f'\n--- Enter messages to send to {IP}:{PORT} or write `quit` to exit ---\n')
    while True:
        message = input("Enter a message to send: ")
        send_message(message)
        if message=='quit':
            break

We first create a socket object with the AF_INET and SOCK_STREAM parameters. The AF_INET parameter indicates we’ll use a standard IPv4 address or hostname, and SOCK_STREAM indicates that this will be a TCP client.

We then connect the client to the server and send it some data as bytes. The last step is to receive some data back and print out the response and then close the socket. This is the simplest form of a TCP client, but it’s the one you’ll write most often.

This code snippet makes some serious assumptions about sockets that you definitely want to be aware of. The first assumption is that our connection will always succeed, and the second is that the server expects us to send data first (some servers expect to send data to you first and await your response).

Our third assumption is that the server will always return data to us in a timely fashion. We make these assumptions largely for simplicity’s sake. While programmers have varied opinions about how to deal with blocking sockets, exception-handling in sockets, and the like, it’s quite rare for pentesters to build these niceties into their quick-and-dirty tools for recongnition or exploitation work.

UDP Server

Now, imagine UDP like a speedy motorcycle courier:

  • Fast Delivery: UDP, like a motorcycle, zips through the traffic, delivering goods super fast. No Confirmation: Unlike the truck, the motorcycle courier doesn't wait for a signature. It drops the goods and zooms off to the next delivery.
  • No Specific Order: UDP doesn’t ensure order; it delivers goods as fast as possible, even if that means they arrive in a different order than they were sent.

Let's code a simple UDP python server on the same principles :

import socket

def main():
    # Create a UDP socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    # Bind the socket to a public host, and a port
    server_socket.bind(('0.0.0.0', 9999))

    print("Server is ready to receive messages")

    while True:
        # Receive data from the client
        data, address = server_socket.recvfrom(1024)
        print(f"Received {data.decode('utf-8')} from {address}")

        # Send a response back to the client
        server_socket.sendto(b'ACK', address)

if __name__ == '__main__':
    main()

UDP Client

import socket

def main():
    # Create a UDP socket
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    # Define the server address and port
    server_address = ('localhost', 9999)

    # Send data to the server
    client_socket.sendto(b'Hello, Server', server_address)

    # Wait for a response from the server
    data, server = client_socket.recvfrom(4096)
    print(f"Received {data.decode('utf-8')} from {server}")

    # Close the socket
    client_socket.close()

if __name__ == '__main__':
    main()

Congrats you just coded from scratch TCP and UDP servers and learn the basics about sockets 🥳