Continuing on from my Setting up Wake-on-lan on Ubuntu Server 22.04 LTS article, I decided that I would probably like to shut down my server remotely too.
To explain the scenario – 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
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 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. Quite annoying to adjust to such a keyboard change, 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 usually a Python programmer. Shocking, right? 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:
if
Sidenote: This is a very crude program so it’ll simply quit if an error is encountered
The next section of code relating to
Following, we populate the
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()
and exit if it fails. If it’s successful, however, we will attempt to
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
The program is running on linux, so glibc is used to access
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/socket.h>
#include <netinet/in.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("%sn",buffer );
return 0;
}
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:
and placed in the above mentioned directory, I can simply open terminal on my MacBook and type “
I’ve slightly adapted this code since it was initially written, to take an argument and specify which computer to shutdown, since I now use it for my Windows desktop too. I added this code to the
char* target = "";
if(argc < 2) {
printf("Usage: sds computer_name\n\n");
exit(0);
} else {
if(strcmp(argv[1],"timination") == 0) {
target = "desktop_ip";
} else if(strcmp(argv[1], "thelab") == 0) {
target = "server_ip";
} else {
printf("Computer name not recognised\n\n");
exit(0);
}
}
It could be adapted even more to directly take an IP address too.
Creating a Service to automatically run the shutdown server on start-up
In the same way as detailed in Setting up Wake-on-lan on Ubuntu Server 22.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/tdc/cpp/sdserver
[Install]
WantedBy=multi-user.target
The file located at /home/tdc/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
and thats it! After putting all that together, I can now run