Book Image

Hands-On Network Programming with C

By : Lewis Van Winkle
Book Image

Hands-On Network Programming with C

By: Lewis Van Winkle

Overview of this book

Network programming enables processes to communicate with each other over a computer network, but it is a complex task that requires programming with multiple libraries and protocols. With its support for third-party libraries and structured documentation, C is an ideal language to write network programs. Complete with step-by-step explanations of essential concepts and practical examples, this C network programming book begins with the fundamentals of Internet Protocol, TCP, and UDP. You’ll explore client-server and peer-to-peer models for information sharing and connectivity with remote computers. The book will also cover HTTP and HTTPS for communicating between your browser and website, and delve into hostname resolution with DNS, which is crucial to the functioning of the modern web. As you advance, you’ll gain insights into asynchronous socket programming and streams, and explore debugging and error handling. Finally, you’ll study network monitoring and implement security best practices. By the end of this book, you’ll have experience of working with client-server applications and be able to implement new network programs in C. The code in this book is compatible with the older C99 version as well as the latest C18 and C++17 standards. You’ll work with robust, reliable, and secure code that is portable across operating systems, including Winsock sockets for Windows and POSIX sockets for Linux and macOS.
Table of Contents (26 chapters)
Title Page
Dedication
About Packt
Contributors
Preface
Index

Listing network adapters from C


Sometimes, it is useful for your C programs to know what your local address is. For most of this book, we are able to write code that works both on Windows and Unix-based (Linux and macOS) systems. However, the API for listing local addresses is very different between systems. For this reason, we split this program into two: one for Windows and one for Unix-based systems.

We will address the Windows case first.

Listing network adapters on Windows

The Windows networking API is called Winsock, and we will go into much more detail about it in the next chapter.

Whenever we are using Winsock, the first thing we must do is initialize it. This is done with a call to WSAStartup(). Here is a small C program, win_init.c, showing the initialization and cleanup of Winsock:

/*win_init.c*/

#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")

int main() {
    WSADATA d;

    if (WSAStartup(MAKEWORD(2, 2), &d)) {
        printf("Failed to initialize.\n");
        return -1;
    }

    WSACleanup();
    printf("Ok.\n");
    return 0;
}

The WSAStartup() function is called with the requested version, Winsock 2.2 in this case, and a WSADATA structure. The WSADATA structure will be filled in by WSAStartup() with details about the Windows Sockets implementation. The WSAStartup() function returns 0 upon success, and non-zero upon failure.

When a Winsock program is finished, it should call WSACleanup().

If you are using Microsoft Visual C as your compiler, then #pragma comment(lib, "ws2_32.lib") tells Microsoft Visual C to link the executable with the Winsock library, ws2_32.lib.

If you are using MinGW as your compiler, the pragma is ignored. You need to explicitly tell the compiler to link in the library by adding the command-line option, -lws2_32. For example, you can compile this program using MinGW with the following command:

gcc win_init.c -o win_init.exe -lws2_32

We will cover Winsock initialization and usage in more detail in Chapter 2, Getting to Grips with Socket APIs.

Now that we know how to initialize Winsock, we will begin work on the complete program to list network adapters on Windows. Please refer to the win_list.c file to follow along.

To begin with, we need to define _WIN32_WINNT and include the needed headers:

/*win_list.c*/

#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif

#include <winsock2.h>
#include <iphlpapi.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>

The _WIN32_WINNT macro must be defined first so that the proper version of the Windows headers are included. winsock2.h, iphlpapi.h, and ws2tcpip.h are the Windows headers we need in order to list network adapters. We need stdio.h for the printf() function and stdlib.h for memory allocation.

Next, we include the following pragmas to tell Microsoft Visual C which libraries must be linked with the executable:

/*win_list.c continued*/

#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "iphlpapi.lib")

If you're compiling with MinGW, these lines will have no effect. You will need to link to these libraries explicitly on the command line, for example, gcc win_list.c -o win_list.exe -liphlpapi -lws2_32.

We then enter the main() function and initialize Winsock 2.2 using WSAStartup() as described earlier. We check its return value to detect any errors:

/*win_list.c continued*/

int main() {

    WSADATA d;
    if (WSAStartup(MAKEWORD(2, 2), &d)) {
        printf("Failed to initialize.\n");
        return -1;
    }

Next, we allocate memory for the adapters, and we request the adapters' addresses from Windows using the GetAdapterAddresses() function:

/*win_list.c continued*/

    DWORD asize = 20000;
    PIP_ADAPTER_ADDRESSES adapters;
    do {
        adapters = (PIP_ADAPTER_ADDRESSES)malloc(asize);

        if (!adapters) {
            printf("Couldn't allocate %ld bytes for adapters.\n", asize);
            WSACleanup();
            return -1;
        }

        int r = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, 0,
                adapters, &asize);
        if (r == ERROR_BUFFER_OVERFLOW) {
            printf("GetAdaptersAddresses wants %ld bytes.\n", asize);
            free(adapters);
        } else if (r == ERROR_SUCCESS) {
            break;
        } else {
            printf("Error from GetAdaptersAddresses: %d\n", r);
            free(adapters);
            WSACleanup();
            return -1;
        }
    } while (!adapters);

The asizevariable will store the size of our adapters' address buffer. To begin with, we set it to 20000 and allocate 20,000 bytes to adapters using the malloc() function. The malloc() function will return 0 on failure, so we test for that and display an error message if allocation failed.

Next, we call GetAdapterAddresses(). The first parameter, AF_UNSPEC, tells Windows that we want both IPv4 and IPv6 addresses. You can pass in AF_INET or AF_INET6 to request only IPv4 or only IPv6 addresses. The second parameter, GAA_FLAG_INCLUDE_PREFIX, is required to request a list of addresses. The next parameter is reserved and should be passed in as 0 or NULL. Finally, we pass in our buffer, adapters, and a pointer to its size, asize.

If our buffer is not big enough to store all of the addresses, then GetAdapterAddresses() returns ERROR_BUFFER_OVERFLOW and sets asize to the required buffer size. In this case, we free our adapters buffer and try the call again with a larger buffer.

On success, GetAdapterAddresses() returns ERROR_SUCCESS, in which case, we break from the loop and continue. Any other return value is an error.

When GetAdapterAddresses() returns successfully, it will have written a linked list into adapters with each adapter's address information. Our next step is to loop through this linked list and print information for each adapter and address:

/*win_list.c continued*/

    PIP_ADAPTER_ADDRESSES adapter = adapters;
    while (adapter) {
        printf("\nAdapter name: %S\n", adapter->FriendlyName);

        PIP_ADAPTER_UNICAST_ADDRESS address = adapter->FirstUnicastAddress;
        while (address) {
            printf("\t%s",
                    address->Address.lpSockaddr->sa_family == AF_INET ?
                    "IPv4" : "IPv6");

            char ap[100];

            getnameinfo(address->Address.lpSockaddr,
                    address->Address.iSockaddrLength,
                    ap, sizeof(ap), 0, 0, NI_NUMERICHOST);
            printf("\t%s\n", ap);

            address = address->Next;
        }

        adapter = adapter->Next;
    }

We first define a new variable, adapter, which we use to walk through the linked list of adapters. The first adapter is at the beginning of adapters, so we initially set adapter to adapters. At the end of each loop, we set adapter = adapter->Next; to get the next adapter. The loop aborts when adapter is 0, which means we've reached the end of the list.

We get the adapter name from adapter->FriendlyName, which we then print using printf().

The first address for each adapter is in adapter->FirstUnicastAddress. We define a second pointer, address, and set it to this address. Addresses are also stored as a linked list, so we begin an inner loop that walks through the addresses.

The address->Address.lpSockaddr->sa_family variable stores the address family type. If it is set to AF_INET, then we know this is an IPv4 address. Otherwise, we assume it is an IPv6 address (in which case the family is AF_INET6).

Next, we allocate a buffer, ap, to store the text representation of the address. The getnameinfo() function is called to convert the address into a standard notation address. We'll cover more about getnameinfo() in the next chapter.

Finally, we can print the address from our buffer, ap, using printf().

We finish the program by freeing the allocated memory and calling WSACleanup():

/*win_list.c continued*/

    free(adapters);
    WSACleanup();
    return 0;
}

On Windows, using MinGW, you can compile and run the program with the following:

gcc win_list.c -o win_list.exe -liphlpapi -lws2_32
win_list

It should list each of your adapter's names and addresses.

Now that we can list local IP addresses on Windows, let's consider the same task for Unix-based systems.

Listing network adapters on Linux and macOS

Listing local network addresses is somewhat easier on a Unix-based system, compared to Windows. Load up unix_list.c to follow along.

To begin with, we include the necessary system headers:

/*unix_list.c*/

#include <sys/socket.h>
#include <netdb.h>
#include <ifaddrs.h>
#include <stdio.h>
#include <stdlib.h>

We then enter the main function:

/*unix_list.c continued*/

int main() {

    struct ifaddrs *addresses;

    if (getifaddrs(&addresses) == -1) {
        printf("getifaddrs call failed\n");
        return -1;
    }

We declare a variable, addresses, which stores the addresses. A call to the getifaddrs() function allocates memory and fills in a linked list of addresses. This function returns 0 on success or -1 on failure.

Next, we use a new pointer, address, to walk through the linked list of addresses. After considering each address, we set address = address->ifa_next to get the next address. We stop the loop when address == 0, which happens at the end of the linked list:

/*unix_list.c continued*/

    struct ifaddrs *address = addresses;
    while(address) {
        int family = address->ifa_addr->sa_family;
        if (family == AF_INET || family == AF_INET6) {

            printf("%s\t", address->ifa_name);
            printf("%s\t", family == AF_INET ? "IPv4" : "IPv6");

            char ap[100];
            const int family_size = family == AF_INET ?
                sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);
            getnameinfo(address->ifa_addr,
                    family_size, ap, sizeof(ap), 0, 0, NI_NUMERICHOST);
            printf("\t%s\n", ap);

        }
        address = address->ifa_next;
    }

For each address, we identify the address family. We are interested in AF_INET (IPv4 addresses) and AF_INET6 (IPv6 addresses). The getifaddrs() function can return other types, so we skip those.

For each address, we then continue to print its adapter name and its address type, IPv4 or IPv6.

We then define a buffer, ap, to store the textual address. A call to the getnameinfo() function fills in this buffer, which we can then print. We cover the getnameinfo() function in more detail in the next chapter, Chapter 2, Getting to Grips with Socket APIs.

Finally, we free the memory allocated by getifaddrs() and we have finished:

/*unix_list.c continued*/

    freeifaddrs(addresses);
    return 0;
}

On Linux and macOS, you can compile and run this program with the following:

gcc unix_list.c -o unix_list
./unix_list

It should list each of your adapter's names and addresses.