Found a tiny NTP server written in C…

Here’s the situation: I was playing with OpenWRT in a systemd-nspawn container. Attempting to run an NTP server in a container turned out to be problematic, as containers generally can’t write to the system clock.

NTPD couldn’t update the time and threw errors in the log. Chrony on the other hand wouldn’t even run, experiencing permission issues which led to “Fatal error : Could not open /var/run/ : Permission denied”.

While it’s always possible there are sane workarounds for these issues, after hitting a couple walls I decided I’d aim for the following:

  1. Run chrony on the host to keep the system time in sync.
  2. Find a standalone NTP server to run in the OpenWRT container and serve time.

I wasn’t having much luck searching the web for a small standalone NTP server, so I decided to search through GitHub. GitHub actually contains quite a few small NTP-related projects in a variety of programming languages. After looking through pages of projects, I found what I was looking for: a small C program that looked like it would do the trick.

The timing was great, as GitHub temp-banned me from search right after I’d found it at page 18 or so (“rate limit” for “up to an hour”).

xdanik/jans NTP server

GitHub link:

Building from within your normal distribution and copying to OpenWRT will often result in “-ash: YOUR-PROGRAM: not found” when you try to run your program unless it’s a static build. So here are a couple options.

OPTION A: Building from within your OpenWRT environment

This is the ideal method. I’ll just dump everything here and you can pick and choose what you want:

opkg update
opkg install gcc make git git-http
git clone
cd jans
make jans

You will have an executable in the directory called jans. To see the options, just type ./jans -h.

I modified my version to also make use of the -f (forking) that already existed. For anyone interested you can grab my modified source from:

OPTION B: Building a static binary for OpenWRT x86_64 from another x86_64 system

If building from another system, the reason for a static build is to make the binary portable to OpenWRT with little effort. However, be aware that this balloons the executable from 47K to 930K. Running side-by-side and looking at memory consumption, both VSZ and RSS increased by about 50% compared to the one built via OPTION A.

With a build system installed (apt install build-essential for Debian-based distros), the process is pretty simple. I’ll assume you’ve already grabbed the source code. Here goes:

  1. (for a static build) Edit the Makefile and change:
    $(CC) -Wall -W $(OBJS) $(LDFLAGS) -o jans


    $(CC) -Wall -W $(OBJS) $(LDFLAGS) -static -o jans
  2. From within the directory, type: make jans
  3. You should be left with an executable called “jans”. To see the available options, run ./jans -h

I additionally modified my version to also make use of the -f (forking) that already existed. For anyone interested you can grab my modified source from:

An OpenWRT init script

For anyone else who may be using this specifically for OpenWRT, here’s a script you can save as /etc/init.d/jans . Note that if you look at the first few lines of the file, it assumes you have placed the executable that you compiled into /usr/sbin/jans , and it also uses the following switches: -s 2 -i 6 -p -12 -R LOCL -t real . You can of course modify these as you see fit.

#!/bin/sh /etc/rc.common



cmd="/usr/sbin/jans "-s" "2" "-i" "6" "-p" "-12" "-R" "LOCL" "-t" "real""

start_service() {
    echo "Starting ${name}"

    procd_set_param command ${cmd}
    procd_set_param respawn             # respawn automatically if something died
    procd_set_param stdout 1            # forward stdout of the command to logd
    procd_set_param stderr 1            # same for stderr
    procd_set_param pidfile ${pid_file} # write a pid file on instance start and remove it on stop

    echo "${name} has been started"

stop_service() {
    echo "Stopping ${name}"

EXTRA_HELP="        status  Print the service status"

get_pid() {
    cat "${pid_file}"

is_running() {
    [ -f "${pid_file}" ] && ps | grep -v grep | grep $(get_pid) >/dev/null 2>&1

status() {
    if is_running; then
        echo "Running"
        echo "Stopped"
        exit 1

Once placed, chmod +x /etc/init.d/jans and then service jans start to start it. If all goes well, service jans status should show it running and you should see it in top or ps | grep -v "grep" | grep "jans". To fire it up on boot, you can run service jans enable .

Other little tidbits

The options page is as follows:
xdanik-jans NTP server

Defaults seem pretty rational. For those wondering about the -t option, real is what the few public time servers I tested seemed to be using. For everything else, RFC4330 covers just about everything though I doubt it matters for a small home/work network.

If you’re testing, using the -v flag will show verbose input any time a client connects. Something like ./jans -P 123 -s 2 -i 6 -p -12 -R LOCL -t real -v will run on port 123 a stratum 2 server with a 64 (2^6) second poll interval at -12 precision claiming a LOCL uncalibrated clock with real time and report verbosely.

It doesn’t appear to listen for traffic over IPv6.

In a basic stress test, jans handled 1000 requests per second from a single machine without issue. CPU usage only hit around 1%.

(code observation) It builds with DEBUG symbols. A little hack-and-slash in the Makefile can bring down the file size a bit if desired.

(code observation) If recvfrom() were to spit out an error other than EINTR, there’s a tendency for the program to exit. Perhaps this is appropriate given those particular errors, but if you’d prefer it to just ignore errors altogether, another project at simply loops as long as the return value is less than 48 – instead of exiting, recvfrom() errors will essentially be ignored. A quick-and-dirty way to change to that variant would be to change:

msglen = recvfrom(fd, msgbufferin, sizeof(msgbufferin), 0, (struct sockaddr *)&from, &fromlen);
if (msglen == -1)
if (errno == EINTR)
error_exit("recv failed");


msglen = 0;
while (msglen < 48) {
msglen = recvfrom(fd, msgbufferin, sizeof(msgbufferin), 0, (struct sockaddr *)&from, &fromlen);

Overall it's a nifty little NTP server.

Leave a Comment

You can use an alias and fake email. However, if you choose to use a real email, "gravatars" are supported. You can check the privacy policy for more details.