The http service of the website is developed in Go. Because it listens to the standard ports 80 and 443, the http service is always run under the root account. But running with root is very risky. If the code is vulnerable and the bad guys break it, they will get root access and the consequences will be unthinkable. That’s why http services like nginx support setting the account under which the worker processes run. In other words, the main process is started with the root account, which completes the port-listening binding action, and then the worker process is started with an unprivileged account to provide services to the public. My blog is developed in Go language, and it is not easy to implement similar mechanism. So I use the socket activation function provided by systemd to implement it. Originally, the http service also needs to use systemd management, so you don’t need to implement your own port listening function. Today, I will share my knowledge with you.

For a detailed description of the socket activation feature of systemd, you can refer to article by Pid Eins, the core systemd developer. In short, socket activation means that systemd can listen to a port on behalf of a service process and pass the corresponding fd to the service process in the form of an environment variable.

What problem does socket activation solve? The answer is system startup speed. unix traditionally starts system services sequentially, one after the other. If a lot of services are started on boot, the whole process can be very slow and long. Later, linux distributions like ubuntu introduced the concept of parallel boot. For services that do not depend on each other (e.g. Bluetooth and http services do not depend on each other), they can be started together to speed up the system.

Can the services that depend on each other be started in parallel? Let’s say there are three services: syslog, dbus and bluetooth. dbus depends on syslog and bluetooth depends on dbus. how can they be started in parallel? This requires understanding the nature of the dependencies!

When syslog starts, it listens to a unix socket file to receive log data. dbus needs to output logs when it starts and runs, so it has to wait until syslog starts before it can run properly. By the same token, dbus, as a message bus, listens to a unix socket file. bluetooth needs to send and receive messages when it starts, and must be started after dbus.

But how does dbus communicate with syslog? Using sockets! It is also a socket. So, the dependency here is essentially a socket dependency. In order to achieve parallel startup, systemd can listen to the sockets for the service, even if the service is not yet started. That is, systemd can listen for sockets on behalf of syslog and dbus, and then start all three services in parallel. If another service wants to send log data before syslog starts, systemd can cache the data and forward it to syslog when it starts. The same is true for dbus. So bluetooh can be started at the same time as syslog and dbus. For bluetooth, the syslog and dbus sockets are ready.

So, systemd can maximize parallel boot and reduce system boot time. This technology seems to be of little use for servers, but for PCs it can greatly improve the user experience. (That’s why systemd is so controversial, some distributions insist on not using it)

Since systemd is process number one, it can listen to any port. systemd can specify a running account when starting a service, so it fits my needs perfectly. So how should I implement the so-called socket activation?

First we need to modify the http service code to support listening to ports from systemd.

systemd can listen to multiple ports. After binding, the http service process is started and three environment variables are injected into the service process: * LISTEN_PID

  • LISTEN_PID PID of the service process
  • LISTEN_FDS The number of sockets currently listened to
  • LISTEN_FDNAMES the names of the sockets currently listened to, separated by colons (to be specified by systemd configuration, discussed later)

The code for inheriting sockets from systemd is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
if os.Getenv("LISTEN_PID") == strconv.Itoa(os.Getpid()) {
  if os.Getenv("LISTEN_FDS") != "3" {
    panic("LISTEN_FDS should be 3")
  }
  names := strings.Split(os.Getenv("LISTEN_FDNAMES"), ":")
  for i, name := range names {
    switch name {
    case "http":
      f1 := os.NewFile(uintptr(i+3), "http port")
      ln80, err = net.FileListener(f1)
    case "https":
      f2 := os.NewFile(uintptr(i+3), "https port")
      ln443, err = net.FileListener(f2)
    case "quic":
      f3 := os.NewFile(uintptr(i+3), "quic port")
      lnUDP, err = net.FilePacketConn(f3)
    }
  }
}

First, we check if the current mileage ID is the same as LISTEN_PID, if not, it means that the current process is a child process of the service process fork, so we don’t need to listen to it again.

Then we check the number of ports by LISTEN_FDS. My application needs to listen to tcp 80/443 and udp 443 ports.

The last thing is to bind fd by the name specified by LISTEN_FDNAMES. systemd listens to sockets with fd starting from 3, plus one, in the same order as the name specified by LISTEN_FDS. That is, if the value of LISTEN_FDNAMES is http:https:quic, then the fd for port 80 is 3, 443 is 4, and udp 443 is 5.

In Go, you can use os.NewFile to convert fd to the corresponding file object and then generate the corresponding listener object.

This is the part of the server-side code that needs to be modified. Now let’s talk about the systemd configuration.

To make systemd listen to sockets or ports, you need the repository .socket configuration file. The structure is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Unit]
Description=lehu http socket

[Socket]
ListenStream=80
FileDescriptorName=http
Service=lehu.service

[Install]
WantedBy=sockets.target

There are three parts here: Unit, Socket and Install. the Unit part writes the configuration description, it is not important. the Install part specifies the installation location. The core is the Socket part. ListenStream means listen to tcp port. If you want to listen to udp, use ListenDatagram. fileDescriptorName means the socket name, which is the value in LISTEN_FDNAMES. service means the corresponding service. systemd will listen to the port first, and then start the corresponding service.

After using the .socket configuration, let’s look at the service configuration, i.e. the .service configuration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[Unit]
Description=lehu
After=network.target
Requires=lehu-http.socket lehu-https.socket lehu-quic.socket

[Service]
ExecStart=...
User=nobody
Group=nobody
KillMode=process
Restart=on-failure

[Install]
WantedBy=multi-user.target

The structure is the same as .socket, with After and Requires in the Unit section. after means that the current service must be started after network.target is started. Requires means that the current service depends on other services or listeners. Here it is specified that lehu.service depends on three listening ports: lehu-http.socket, lehu-https.socket and lehu-quic.socket. In other words, if you want to start lehu.service, systemd will listen to the three ports first. the Service section sets the service executable path and run parameters via ExecStart, and the run account via User and Group. The [Install] section adds the current service to the multi-user.target group, which corresponds to the sysv 3 runlevel.

Then add all the configuration to /etc/systemd/system and execute.

1
systemctl enable lehu.service lehu-http.socket lehu-https.socket lehu-quic.socket

At this point, systemd is already listening to the port. Because the corresponding port is not yet accessible, systemd will not start lehu.service.

If you stop all units and start only lehu-http.socket, and then access port 80, systemd will try to start lehu.service after receiving the request. systemd will try to start lehu.service when it receives the request, and because lehu.service depends on other ports to listen, systemd will also start listening.

This is the main point of this article. systemd takes over a lot of system startup and management tasks, which is not quite in line with the unix philosophy of small and medium size. But systemd does solve a lot of key problems, so it is more controversial. But the big picture is that distributions like debian and ubuntu have migrated to systemd and we have to learn about it. And systemd even supports cgroups to isolate resources for service usage, which is an inescapable infrastructure in the cloud-native era. This article just introduces the usage of systemd from the socket activation point of view, and hopefully it will give you some insight.