simple-vpn

This repository contains a simple implementation of a client to server virtual private network or proxy service written in C. All connections are handled by TCP and are end to end encrypted using AES.

Makefile

Use make or make all to compile the program and all its dependencies.

all: client server

ecdh.o: libs/ecdh.c
    gcc -c libs/ecdh.c -I.

csprng.o: libs/csprng.c
    gcc -c libs/csprng.c -I.

sha256.o: libs/sha256.c
    gcc -c libs/sha256.c -I.

aes.o: libs/aes.c
    gcc -c libs/aes.c -I.

client: ecdh.o csprng.o sha256.o aes.o
    gcc -o client.out client.c ecdh.o csprng.o sha256.o aes.o -Wall -Werror -I.

server: ecdh.o csprng.o sha256.o aes.o
    gcc -o server.out server.c ecdh.o csprng.o sha256.o aes.o -Wall -Werror -I.

clean:
    rm -f *.out *.o *.html

The make command outputs the object files for each library in addition to two executable programs named ./client.out and ./server.out.

Testing

To test the code, first start the server by using the following command: ./server.out, which does not take any parameters. Next, to start the client run ./client.out <hostname>, replacing <hostname> with the hostname of the server. Note that the server is able to handle multiple clients simultaneously so it is possible to have more than one client connected to the server at the same time. Finally, on the client side, input the hostname or IP of the HTTP webserver on the command line and press enter. The client should forward the request to the server and save the return message to disk. Both the client and server are verbose, printing out intermediary values for transparency.

Header.h

In the shared header file, we include the nessecary library headers, define macros, and shared helper functions.

We define, print_hex(), print_hex_byte(), and print_hex_uint8() to output the msg buffer to stdout in hexadecimal format. All three of these functions follow the same basic implementation as shown below, except that the data type of the msg parameter differs in each case.

void print_hex(const char *msg, int len)
{
    for (int i = 0; i < len; i++)
        printf("%02x", (unsigned char)msg[i]);
    printf("\n");
}

Additionally, we also define get_in_addr(), a helper function we use to setup our TCP connections.

void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET){
    return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

Lastly, we define the helper functions pkcs7_pad() and pkcs7_unpad() which implements padding for our char buffers as specified by PKCS#7.

void pkcs7_pad(char *buf, int *data_len)
{
    uint8_t pad_len = AES_BLOCKLEN - ((*data_len) % AES_BLOCKLEN);
    for (int i = 0; i < pad_len; i++) {
        buf[(*data_len) + i] = pad_len;
    }
    (*data_len) += pad_len;
}
int pkcs7_unpad(char *buf, int *buf_len)
{
    // checks for error
    if ((*buf_len) % AES_BLOCKLEN != 0){
        fprintf(stderr, "pkcs7_unpad: invalid block size\n");
        return -1;
    }

    char pad_num = buf[(*buf_len) - 1];
    // check whether pad_num is bigger than AES_BLOCKLEN or not
    if (pad_num >= AES_BLOCKLEN){
        return 0;
    }

    for (int i = (*buf_len) - pad_num; i < (*buf_len); i++){
        if (buf[i] != pad_num){
            return 0;
        }
    }
    (*buf_len) -= pad_num;
    return 0;
}

Client.c

The file client.c contains our implementation for the client system. We first define a helper function send_url() that sends a packet to the server containing the URL or hostname of the HTTP server that the client would like to connect to. The packet consists of a header and a URL field. The first byte of the header is the flag field. The flag field currently is by default always set to 0x01 as our implementation only supports one mode but this field is being included for possible expansion in the future. Next, the header contains the 2 byte length of the url field in network byte order. Lastly, we append the URL specified by the user. Before the packet is sent off to the server, we also prepend the IV value to the start of the packet. The IV value, a 16 bytes values that we generate using a CSPRNG, is required for us to be able to properly decrypt the message using AES. Note that we generate a new IV value for each message that we send.

int send_url(int sockfd, char *url, BYTE *aes_key, BYTE *aes_iv){
    char message[MAXPACKETSIZE];

    // set message flag
    message[0] = 0x01;

    // set url msg_len
    uint16_t url_msg_len = htons(strlen(url));
    memcpy(message + 1, &url_msg_len, sizeof(uint16_t));

    // set url
    memcpy(message + 1 + sizeof(uint16_t), url, strlen(url));

    // pad message
    int msg_len = 1 + sizeof(uint16_t) + strlen(url);
    pkcs7_pad(message, &msg_len);

    // encrypt message
    struct AES_ctx aes_ctx;
    AES_init_ctx_iv(&aes_ctx, aes_key, aes_iv);
    AES_CBC_encrypt_buffer(&aes_ctx, (uint8_t*)message, msg_len);

    // prepend IV to message
    char packet[msg_len + AES_BLOCKLEN];
    memcpy(packet, aes_iv, AES_BLOCKLEN);
    memcpy(packet + AES_BLOCKLEN, message, msg_len);

    // send message to server
    if ((send(sockfd, packet, msg_len + AES_BLOCKLEN, 0)) == -1){
        perror("send");
        return -1;
    }
    
    return 0;
}

In the main function of this program, the client first attempts to set up a TCP connection with the server and exits on failure.

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((rv = getaddrinfo(argv[1], TCP_PORT, &hints, &servinfo)) != 0){
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // loop through all the results and connect to the first we can
    for (p = servinfo; p != NULL; p = p->ai_next){
        // establishes socket
        if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1){
            perror("client: socket");
            continue;
        }

        // Establishes connection between client and server
        if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1){
            close(sockfd);
            perror("client: connect");
            continue;
        }

        break;
    }

    // checks for client connection error
    if (p == NULL){
        fprintf(stderr, "client: failed to connect\n");
        return 2;
    }

    // Get the address of the server
    inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr), s, sizeof(s));
    printf("client: connecting to %s\n", s);

Then the client sets up poll() allowing us to both send and receive data at the same time.

   struct pollfd pfds[2];
   pfds[0].fd = 0;
   pfds[0].events = POLLIN;
   pfds[1].fd = sockfd;
   pfds[1].events = POLLIN;

The next step is to perform ECDH. We then set up and generate the required client side values like the private/public key. We receive the public key of the server and then send a copy of the client public key we just generated to the server as well. Using the server public key and our private key we generate a shared secret value that we hash using SHA256 to create a shared AES key that we will later use to encrypt all messages.

// Initialize CSPRNG
   CSPRNG rng = csprng_create();
   if (!rng) {
       fprintf(stderr, "error initializing CSPRNG\n");
       return 1;
   }

   // Initialize the client's keys
   static uint8_t cl_pub[ECC_PUB_KEY_SIZE];
   static uint8_t cl_prv[ECC_PRV_KEY_SIZE];

   // Generate client's private key
   csprng_get(rng, &cl_prv, ECC_PRV_KEY_SIZE);

   // Generate client's public key
   if (ecdh_generate_keys(cl_pub, cl_prv) != 1) {
       fprintf(stderr, "error generating client's public key\n");
       return 1;
   }

   // Receiving the server's public key
   uint8_t srv_pub[ECC_PUB_KEY_SIZE];
   if (recv(sockfd, srv_pub, ECC_PUB_KEY_SIZE, 0) == -1){
       perror("recv");
       return 1;
   }

   // prints srv_pub
   printf("srv_pub: ");
   print_hex_uint8(srv_pub, ECC_PUB_KEY_SIZE);

   // Send client's public key to server
   if (send(sockfd, cl_pub, ECC_PUB_KEY_SIZE, 0) == -1){
       perror("send");
       return 1;
   }

   // prints cl_pub
   printf("cl_pub: ");
   print_hex_uint8(cl_pub, ECC_PUB_KEY_SIZE);

   // Generate shared secret
   static uint8_t shared_secret[ECC_PUB_KEY_SIZE];
   if (ecdh_shared_secret(cl_prv, srv_pub, shared_secret) != 1) {
       fprintf(stderr, "error generating shared secret\n");
       return 1;
   }

   // print shared secret
   printf("shared secret: ");
   print_hex_uint8(shared_secret, ECC_PUB_KEY_SIZE);

   // Generate AES key
   SHA256_CTX sha256_ctx;
   BYTE aes_key[SHA256_BLOCK_SIZE];
   sha256_init(&sha256_ctx);
   sha256_update(&sha256_ctx, shared_secret, ECC_PUB_KEY_SIZE);
   sha256_final(&sha256_ctx, aes_key);

The client also opens a log file to record all of the data that it receives from the server.

    FILE *log_file = fopen("main.html", "wb");
    if (log_file == NULL){
        fprintf(stderr, "error opening log file\n");
        return 1;
    }

The client then enters an infinite loop, sending data when STDIN is ready, and receiving data when the server sends anything. When the server shuts down, the client also gracefully exits.

    int poll_count;

    for(;;)
    {
        // checks for poll error
        if ((poll_count = poll(pfds, 2, -1)) == -1)
        {
            perror("[Client] poll");
            exit(1);
        }

        // if stdin ready
        if (pfds[0].revents & POLLIN)
        {
            // Grabs the input into the terminal
            if (fgets(buf, MAXDATASIZE, stdin) == NULL)
            {
                perror("[Client] fgets");
                exit(1);
            }
            buf[strlen(buf) - 1] = '\0'; // remove newline

            // generate IV
            uint8_t iv[AES_BLOCKLEN];
            csprng_get(rng, &iv, AES_BLOCKLEN);

            // send url to server
            if (send_url(sockfd, buf, aes_key, iv) == -1){
                exit(1);
            }
            
        }

        // if recvfrom ready
        if (pfds[1].revents & POLLIN)
        {
            // receives data and checks for error
            if ((numbytes = recv(sockfd, buf, MAXDATASIZE, 0)) == -1)
            {
                perror("[Client] recvfrom");
                exit(1);
            }
            // buffer[numbytes] = '\0';
            
            // Server closed connection
            if (numbytes == 0){
                printf("[Client] Server closed connection\n");
                break;
            }

            // get IV from buf
            uint8_t iv[AES_BLOCKLEN];
            memcpy(iv, buf, AES_BLOCKLEN);

            // get ciphertext from buf
            char buftext[numbytes - AES_BLOCKLEN];
            memcpy(buftext, buf + AES_BLOCKLEN, numbytes - AES_BLOCKLEN);
            numbytes -= AES_BLOCKLEN;

            // Decrypt the message
            struct AES_ctx aes_ctx;
            AES_init_ctx_iv(&aes_ctx, aes_key, iv);
            AES_CBC_decrypt_buffer(&aes_ctx, (uint8_t*)buftext, numbytes);

            // unpad the message
            if (pkcs7_unpad(buftext, &numbytes) == -1) {
                fprintf(stderr, "error unpadding message\n");
                exit(1);
            }

            // Print padded message
            printf("message: ");
            print_hex(buftext, numbytes);

            // Save to file
            fwrite(buftext, sizeof(char), numbytes, log_file);
            fflush(log_file);
        }
    }

    // Close socket and shutdown process
    rng = csprng_destroy(rng);
    close(sockfd);
    fclose(log_file);

Server.c

The implementation for the server is much more complex compared to the client because it has to be able to handle multiple clients while keeping track of the nessecary information like encryption keys that is unique for each individual client. First we define the struct that we use to hold the client information.

struct cl_info{
    uint8_t cl_pub[ECC_PUB_KEY_SIZE];
    uint8_t shr_key[ECC_PUB_KEY_SIZE];
    BYTE aes_key[SHA256_BLOCK_SIZE];
};

Next we introduce the different helper functions that we define. The first helper function, get_listener_socket() creates and binds a listener socket that we later require to set up TCP connections.

int get_listener_socket(void)
{
    int listener;     // Listening socket descriptor
    int yes = 1;      // For setsockopt() SO_REUSEADDR
    int rv;

    struct addrinfo hints, *ai, *p;

    // Get us a socket and bind it
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;
    if ((rv = getaddrinfo(NULL, TCP_PORT, &hints, &ai)) != 0) {
        fprintf(stderr, "selectserver: %s\n", gai_strerror(rv));
        exit(1);
    }
    
    for(p = ai; p != NULL; p = p->ai_next) {
        listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
        if (listener < 0) { 
            continue;
        }
        
        // Lose the pesky "address already in use" error message
        setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));

        if (bind(listener, p->ai_addr, p->ai_addrlen) < 0) {
            close(listener);
            continue;
        }

        break;
    }

    freeaddrinfo(ai); // All done with this

    // If we got here, it means we didn't get bound
    if (p == NULL) {
        return -1;
    }

    // Listen
    if (listen(listener, 10) == -1) {
        return -1;
    }

    return listener;
}

Next, we define add_to_pfds() and del_from_pfds() that add/removes new file descriptors as we accept/disconnect connections to/from the pfds struct. The poll pfds struct has to be dynamically allocated on the heap because we can accept any number of connections from clients.

// Add a new file descriptor to the set
void add_to_pfds(struct pollfd *pfds[], int newfd, int *fd_count, int *fd_size)
{
    // If we don't have room, add more space in the pfds array
    if (*fd_count == *fd_size) {
        *fd_size *= 2; // Double it

        *pfds = realloc(*pfds, sizeof(**pfds) * (*fd_size));
    }

    (*pfds)[*fd_count].fd = newfd;
    (*pfds)[*fd_count].events = POLLIN; // Check ready-to-read

    (*fd_count)++;
}

// Remove an index from the set
void del_from_pfds(struct pollfd pfds[], int i, int *fd_count)
{
    // Copy the one from the end over this one
    pfds[i] = pfds[*fd_count-1];

    (*fd_count)--;
}

Very similar to add_to_pfds() and del_from_pfds(), the next two helper functions, add_to_secs() and del_from_secs() add/removes the client public key, shared key, and AES key from the client information struct.

// Add new client secret to the list
void add_to_secs(struct cl_info *cl_secs[], uint8_t *cl_pub, uint8_t *shr_key, BYTE *aes_key, int *sec_count, int *sec_size)
{
    // If we don't have room, add more space in the cl_secs array
    if (*sec_count == *sec_size) {
        *sec_size *= 2; // Double it

        *cl_secs = realloc(*cl_secs, sizeof(**cl_secs) * (*sec_size));
    }
    
    memcpy((*cl_secs)[*sec_count].cl_pub, cl_pub, ECC_PUB_KEY_SIZE);
    memcpy((*cl_secs)[*sec_count].shr_key, shr_key, ECC_PUB_KEY_SIZE);
    memcpy((*cl_secs)[*sec_count].aes_key, aes_key, SHA256_BLOCK_SIZE);

    (*sec_count)++;   
}

// Remove client secret from the list
void del_from_secs(struct cl_info cl_secs[], int i, int *sec_count)
{
    // Copy the one from the end over this one
    cl_secs[i-1] = cl_secs[*sec_count-1];

    (*sec_count)--;
}

The http_request() function takes the URL specified by the client, sends an HTTP request to the URL, and saves the response in the char buffer defined as a parameter to the function.

int http_request(char *response, int *response_len, int sender_fd, char *url)
{
    int sockfd, rv;
    struct addrinfo hints, *servinfo, *p;

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((rv = getaddrinfo(url, HTTP_PORT, &hints, &servinfo)) != 0){
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return -1;
    }

    // loop through all the results and connect to the first we can
    for (p = servinfo; p != NULL; p = p->ai_next){
        if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1){
            perror("client: socket");
            continue;
        }

        if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1){
            close(sockfd);
            perror("client: connect");
            continue;
        }

        break;
    }

    if (p == NULL){
        fprintf(stderr, "client: failed to connect\n");
        return -1;
    }

    freeaddrinfo(servinfo);
    
    char *request;
    if (asprintf(&request, "GET / HTTP/1.1\r\nHost: %s\r\n\r\n", url) == -1) {
        perror("asprintf");
        return -1;
    }
    printf("[socket %d] Making HTTP request to %s\n", sender_fd, url);

    if (send(sockfd, request, 27 + strlen(url), 0) == -1) {
        perror("send");
        return -1;
    }
    free(request);

    // read HTTP response
    if (((*response_len) = recv(sockfd, response, MAXDATASIZE-1, 0)) == -1) {
        perror("recv");
        return -1;
    }
    response[(*response_len)] = '\0';

    close(sockfd);

    return 0;
}

Libs

We use four free open source libraries to support CSPRNG, ECDH, SHA256, and AES.