Quantcast
Channel: Planet Ubuntu
Viewing all articles
Browse latest Browse all 12025

Ian Weisser: upstart-socket-bridge

$
0
0
Upstart-socket-bridge is a lot like xinetd. They replace the need for some daemons by monitoring a port, and then launching the desired application when an inbound connection is detected. U-s-b is part of a family of Upstart services that replace many daemon monitoring and listening functions and hooks.

Unlike xinetd, services need to be customized (patched) to run with upstart-socket-bridge. The listening application only seems to work if written in C.  Here is my AskUbuntu question looking for help with Python 3.

Documentation is quitesparse. Hence this blog post. That's not intended to criticize; it's really hard to write "good" documentation when you don't know the use cases or the experience level of the user. If you have experience with writing sockets in C, and understand what a file descriptor is and how to use one,  then the documentation is just fine. I didn't before I began this odyssey.




How do I make it work?


Here's a "Hello, World!" script that gets triggered by the port action. The port is just a trigger, no data gets exchanged on the port.

1) Let's create a shell script called test-script. This script merely prints out the Upstart-related environment variables into a file.

#!/bin/sh
outfile=/tmp/outfile
date > $outfile # Timestamp
printenv | grep UPSTART >> $outfile
exit 0


2)  Create an Upstart .conf, let's call it /etc/init/socket-test.conf

description "upstart-socket-bridge test"
start on socket PROTO=inet PORT=34567 ADDR=127.0.0.1 # Port 34567
setuid exampleuser # Run as exampleuser, not root
exec /bin/sh /tmp/test-script # Launch the service


3)  Let's run it. Connect to the port using netcat.

$ nc localhost 34567
^C # End the process using CTRL+C


4)  Look at the result.

$ cat /tmp/outfile


5)  Clean up:

$sudo rm /etc/init/socket-test.conf
$rm /tmp/test-script /tmp/outfile




Here's another "Hello, World!" service in C. It's a simple echo server. It requires two files, the application and the Upstart .conf. It demonstrates how a service reads uses the port connection for a trigger and exchanging data.

1)  Let's create a C file. Let's call it test-service.c

#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>

int main()
{
/* Read the UPSTART_FDS env var to get the socket fd */
char *name = "UPSTART_FDS";
char *env = getenv (name); // Read the environment variable
int sock_fd = atoi(env); // Socket file descriptor

/* Don't need to do any of these normal socket tasks! Hooray!
/ int port_num;
/ int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
/ memset((char *) &serv_addr, 0, sizeof(serv_addr));
/ serv_addr.sin_family = AF_INET;
/ serv_addr.sin_addr.s_addr = INADDR_ANY;
/ serv_addr.sin_port = htons(port_num);
/ struct sockaddr_in serv_addr
/ bind(sock_fd, (struct sockaddr *) &serv_addr, sizeof(serv_addr));
/ listen(sock_fd, 5)
*/

/* Accept() the connection. Returns the second fd: 'conn_fd' */
struct sockaddr_in cli_addr; // Requires netinet/in.h
int clilen = sizeof(cli_addr);
int conn_fd = accept(sock_fd, (struct sockaddr *) &cli_addr, &clilen);

/* Service is active. Read-from and write-to the connection fd */
char response[276] = "I got your message: ";
char buffer[256];
memset((char *) &buffer, 0, sizeof(buffer));
read(conn_fd, buffer, 256); // Read from conn_fd
strcat(response, buffer);
write(conn_fd, response, strlen(response)); // Write to conn_fd

/* Close the connection fd. Socket fd can be reused */
close(conn_fd);
return 0;
}

2)  Compile it using gcc, and output the compiled application as an executable called test-service. I put mine in /tmp to make cleanup easier. If you're familiar with gcc, the important element is that there are no flags and no libraries:

gcc -o test-service test-service.c


3)  Create an Upstart .conf, let's call it /etc/init/socket-test.conf

description "upstart-socket-bridge test"
start on socket PROTO=inet PORT=34567 ADDR=127.0.0.1 # Port 34567
setuid exampleuser # Run as exampleuser, not root
exec /tmp/test-service # Launch the service


4) Let's run it. Connect to the port using netcat, and then type in a string.

$ nc localhost 34567
Hello, World! # You type this in. Server read()s it.
I got your message: Hello, World! # Server response. Server write()s it.


5) Cleanup is simple. Simply delete the three files.

$ sudo rm /etc/init/socket-test.conf         # Disconnects the bridge
$ rm /tmp/test-service.c /tmp/test/service # Delete the test service



How does it work?

Here is the oversimplified explanation. Each stream of data whizzing round inside your system is tracked by the kernel. That tracking, sort of like an index or a pointer, is called a file descriptor (fd). A few fds are reserved (0=stdin, 1=stdout, 2=stderr) and you run into these in shell scripting or cron jobs.

A pipe or port or socket is just a way to tell the kernel that a stream of data output from Application A should be input to Application B. Let's look at it another way, and add that fd definition: An fd identifies a data stream output from A and input to B. The pipe/socket/port is a way to express how you want the fd set up.

Now the gritty stuff: A socket/port can have a single server and multiple clients attached. The server bind()s the port, and listen()s on the port for connections, and accept()s each connection from a client. Each connection to a client gets issues another file descriptor.

That's two file descriptors: The first defines the port/socket in general, and the second defines the specific connection between one client and the server.

Upstart tells your server application (since it's not actually serving, let's just call it the "service") the first file descriptor.
  • Your service doesn't start a daemon at boot.
  • Your service doesn't bind() to the socket/port. Upstart does that.
  • Your service doesn't listen() on the socket/port. Upstart does that.
  • Your service doesn't fork() for each connection. Upstart launches an instance of your service for each connection. Your service can terminate when the connection ends...if you want it to.
  • Your service does accept() the connection from a client, communicate using the resulting file descriptor, and end when the connection close()s.

Let's try it with the example above:
  1. Upstart and Service are running on Server. Client is running somewhere else - maybe it's also running on Server, maybe it's out on the network somewhere.
  2. The file /etc/init/socket-test.conf tells Upstart to monitor port #34567 on behalf of test-service application. As currently written, it will begin monitoring at boot and stop monitoring at shutdown.
  3. When Client --like netcat-- connect()s to port #34567, Upstart launches test-service application with a couple extra environment variables.
  4. test-service reads the environment variables, including the file descriptor (fd).
  5. test-service accept()s the connection on the file descriptor. This creates a second fd that Service can use to communicate.
  6. When Client and test-service are done communicating, they close() the connection fd.
  7. test-service can end. Upstart will restart it next time an inbound connection is received.



Does it work with Python?

I can't get it to work with Python 3. You're sure welcome to try.

In theory, it should work like:

import os, socket
sock_fd = socket.fromfd(os.environ["UPSTART_FDS"], socket.AF_INET, socket.SOCK_STREAM)
# The above line is where it breaks for me: "OSError: [Errno 9] Bad file descriptor"

conn, addr = sock_fd.accept()
message = conn.recv(1024).decode('UTF-8')
reply = ("I got your message: " + message).encode('UTF-8')
conn.send(reply)
conn.close()




How do I make it work with a service I didn't write? (like my favorite Game Server or Media Server)

If you don't have experience with source code, and with creating patches, it won't be easy. But it is a good learning experience.

If the service isn't written in C, it won't be easy.

Portability is also an issue. If the service needs to work under SystemV or systemd in a different Linux distro, then the developer may not want to support (and test) an Upstart-only or systemd-only method when a good ol' daemon will work on all of them.


Viewing all articles
Browse latest Browse all 12025

Trending Articles