Remote shutdown with C++ on Linux

So, carrying on from my Setting up Wake-on-lan on Ubuntu Server 18.04 LTS article, I decided that I would probably like to shut down my server a little bit more easily too.

In case you’re not sure what server I’m referring to – I run a local headless ubuntu server at home to allow me to work on database driven projects across multiple devices without having to keep local copies of the data on each device. It has your standard LAMP stack on there, nothing too fancy.

Until recently, I was logging in via SSH and running sudo poweroff to shut down the system, not terribly convenient. I figured, if I’m running a python script from terminal to fire up my server, I should probably get another system implemented to run another thing from terminal to shut it down, and so I did.

It’s been quite a while since I’ve written anything meaningful in C++. In fact, the last piece of C++ code I wrote with a purpose was in 2012 when my laptop J key was broke – it kept sending keystrokes without being pushed. So I threw together a low level keyboard hook to discard the key and listen for the period key instead, which it would then replace with a J. Not very convenient but it was a good temporary solution at the time.

Anyway, I digress. So, I could probably have done this in Python but I’m not a Python programmer, I’ve barely even studied it. Shocking, right? I can read it, sure, but not write. So, C++ was a nice alternative since I didn’t want to go through the hassle of installing java on my server for the sake of one task, all that extra JVM overhead was burning my soul. That and my linux distro has C/C++ compilers installed already so no extra work needed there.

 

The Client-Server Model

Everyone with even a slight understanding of the internet is familiar with the basics of a client-server model. The server listens, the client hollers and the server responds. Without getting into the details of protocols, packets and such, that’s the crux of it. With that in mind, it gave me a good place to start.

I was going to need a server, on my server, to listen. That sounds crazy, right? ambiguity at its finest!

What I mean is, I needed a program on my server machine to listen on a particular port for a particular piece of data to trigger a shutdown, I decided to call it the shutdown server since that would be its sole purpose and it describes its purpose perfectly.

The Server

So I’m just going to dump the code here and let you have a read before I explain anything about it:

#include <unistd.h>
#include <iostream>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <linux/reboot.h>
#include <sys/reboot.h>

int main(int argc, char const* argv[]) {
	const int port = 8000;

	bool shutdown = false;
	struct sockaddr_in address;
        int new_socket, input;
        int opt = 1;
	int addrlen = sizeof(address);
	char *response = "shutdown command received";

	int sockfd = socket(AF_INET, SOCK_STREAM, 0);

	if(sockfd < 0) {
		std::cout << "Socket Creation Failed" << std::endl;
		exit(-1);
	}

	if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)) < 0) {
		std::cout << "setsockopt failed";
		exit(-1);
	}	

	address.sin_family = AF_INET;
	address.sin_addr.s_addr = INADDR_ANY;
	address.sin_port = htons(port);

	if(bind(sockfd, (struct sockaddr *) &address, sizeof(address))<0) {
		std::cout << "Bind failed" << std::endl;
		exit(-1);
	}

	if(listen(sockfd, 3) < 0){
		std::cout << "Listen failed" << std::endl;
		exit(-1);
	}

	while(1) {
		char buffer[1024] = {0};

	       	if ((new_socket = accept(sockfd, (struct sockaddr*) &address, (socklen_t*)&addrlen))<0) {
                std::cout << "accept failed" << std::endl;
                exit(-1);
		    }
	
		input = read( new_socket , buffer, 1024);
		if(input <= 0) { break;}

		if(strcmp(buffer, "shutdown -local") == 0) {
			send(new_socket, response, strlen(response), 0);

			shutdown = true;
			close(new_socket);
			break;
		} else {
			send(new_socket, "hello", strlen("hello"),0);
			close(new_socket);
		}
	
	}	

	if(shutdown) {
		std::cout << "Remote shutdown requested" << std::endl;
		reboot(LINUX_REBOOT_CMD_POWER_OFF);
	}
	return 0;
}

First of all, we need to create a socket file descriptor to use:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

if sockfd contains a negative integer, something went wrong. So we check that with an if statement before going any further.

Sidenote: This is a very crude program so it’ll simply quit if an error is encountered

The next section of code relating to setsockopt() doesn’t strictly have to be used, it attempts to force the program to use the defined port. Since my machine is absolutely not running anything on port 8000, I’m happy to make use of the function.

Following, we populate the sockaddr_in struct with some required values (tell it to use IPv4, accept connections from any address and provide the port to listen on).

Sidenote: I’m not worried about accepting connections from any address because my server is configured to listen on LAN only and it sits behind two firewalled routers and runs its own firewall too

The next step is to attempt to bind the socket file descriptor to the given port with the pre-configured struct data, using bind()

bind(sockfd, (struct sockaddr *) &address, sizeof(address)

and exit if it fails. If it’s successful, however, we will attempt to listen(sockfd, 3) for new connections and enter an infinite loop.

The infinite loop is required to continually accept incoming connections and respond accordingly. There are only two responses here. The first will shut down the system if the data “shutdown -local” is received, the other will simply reply with “hello” and then close the connection and await a new one.

If the accept() function is called before the loop, it will only ever accept one connection and never wait for a subsequent connections.

The program is running on linux, so glibc is used to access reboot(LINUX_REBOOT_CMD_POWER_OFF); which will tell the system to power off. This actually requires sufficient permissions to work but I added the compiled program to systemd to run as a service, since I’ll want the shutdown server to be listening automatically as soon as the server is powered on. I’ll explain this a bit more shortly. The service runs with sufficient permissions; otherwise, if you’re testing the code, you’ll probably need to run it as root.

g++ was used to compile.

The Client

The client program will primarily be running from my MacBook so it was compiled on there with g++. It is considerably shorter:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <iostream>

   
int main(int argc, char const *argv[]) 
{ 
    const int port = 8000;
    struct sockaddr_in address; 
    int sock = 0, response;
    struct sockaddr_in serv_addr; 
    char *data = "shutdown -local";
    char buffer[1024] = {0}; 
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    { 
        std::cout << "socket creation error" << std::endl;
        return -1; 
    } 
   
    memset(&serv_addr, '0', sizeof(serv_addr)); 
   
    serv_addr.sin_family = AF_INET; 
    serv_addr.sin_port = htons(port); 
       
    if(inet_pton(AF_INET, "local.ip.here", &serv_addr.sin_addr)<=0)
    { 
        std::cout << "invalid address" << std::endl;
        return -1; 
    } 
   
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
    { 
        std::cout << "connection failed" << std::endl;
        return -1; 
    } 
    send(sock, data, strlen(data), 0);
    response = read( sock , buffer, 1024);
    printf("%s\n",buffer ); 
    return 0; 
} 

A quick client summary

Similarly to the server, we define our variables and create a socket. If that’s successful, we validate the given IP address and attempt to connect. Providing there are no errors, we send the data to the server. In this case the data is simply a string containing the phrase “shutdown -local” which is a specific string that the shutdown server is listening out for. The client then waits for a response and then displays it before exiting gracefully by returning zero.

This client source code was compiled on my MacBook and placed in a directory that is added to PATH dedicated to custom scripts and programs that I might like to run from terminal, so I don’t have to navigate anywhere or type full path names when I open terminal.

Compiled with: g++ client.cpp -o sds

and placed in the above mentioned directory, I can simply open terminal on my MacBook and type “sds” to shutdown my server. This coupled with the “wol.py script to wake the server means that I can turn it on and off with absolute ease, remotely.

Creating a Service to automatically run the shutdown server on star-up

In the same way as detailed in Setting up Wake-on-lan on Ubuntu Server 18.04 LTS, I created a service to run my shutdown server automatically too.

The service is incredibly basic. I created a file called sdserv.service in /etc/systemd/system which contains the following:

[Unit]
Description=Listen for local shutdown command

[Service]
ExecStart=/home/tim/cpp/sdserver

[Install]
WantedBy=multi-user.target


The file located at /home/tim/cpp/sdserver is the binary of the compiled server source code above. ExecStart requires an absolute file path.

I then told systemd to refresh its cache of services with systemctl daemon-reload

systemctl enable sdserv.service tells systemd to run the service on start-up and systemctl start sdserv.service starts the service.

and thats it! After putting all that together, I can now run wol.py to turn on my server and sds to turn it off!

Xcode, ffmpeg and mac

So for the last 2 hours I’ve been getting errors trying to compile some C++ code on my mac that makes use of ffmpeg. For clarity, I’m using a modern MBP, Xcode and C++ to try and compile some simple ffmpeg code to spit out a list of stuff I can’t even remember the reason for right now.

...Undefined symbols for architecture x86_64:
"avcodec_register_all()...

This error was telling me that the linker can’t find a particular library required for compiling. So I tried rebuilding ffmpeg a few times with different options, I tried linking the include and lib directories to Xcode a couple of different ways, I tried changing the code several times too, I even tried compiling via terminal with g++, all with no luck.

So, tired as hell, I give it one more try. I realise that, thanks to some crazy Chinese forum with snippets of English on it, ffmpeg uses C, not C++. So I had to change my header includes to reflect that.


#ifdef __cplusplus
extern "C" {
#include "libavformat/avformat.h"
}
#endif

instead of:

#include <libavformat/avformat.h>

An unbelievably simple fix to a nightmare-ish problem, such is the life of a programmer.

So, I don’t write a lot of stuff here anymore but decided to put this up just in case someone else finds themselves in the same situation in the distant future.