The Boston Diaries

The ongoing saga of a programmer who doesn't live in Boston, nor does he even like Boston, but yet named his weblog/journal “The Boston Diaries.”

Go figure.

Sunday, February 02, 2025

Artisanal code to solve an issue only I have

Update on Tuesday, February 4th, 2025

The code presented below has a bug that has been fixed.. The code linked to below contains the current fixed code. That is all.

Now on with the original post …

I'm still using my ISP, despite the repeated letters that my service will go away at some point. But in the meantime, they keep reissuing a new IP address every so often just to reiterate their dedication to their serving up a dynamic IP address at no addtional cost to me. One of the casualties of their new policy is the monitoring of the system logs on my public server. I used to send syslog output from my public server to my development system at home, just to make it easier to keep an eye on what's happening. No more.

What I needed was a reverse-proxy type of thing—where the client (my development machine) connects to the server, then the server sends a copy of the logs down the connection. A separate program would be easier to write then to modify the exiting syslog daemon I'm using. It was a simple matter of telling the syslog daemon to forward a copy of all the logs to another program on the same system. Then I just had to write that program. To begin with, I need to load some modules:

local syslog  = require "org.conman.syslog"
local signal  = require "org.conman.signal"
local errno   = require "org.conman.errno"
local tls     = require "org.conman.nfl.tls"
local nfl     = require "org.conman.nfl"
local net     = require "org.conman.net"

The nfl module is my “server framework” for network based servers. Each TCP or TLS connection will be run on its own Lua thread, making the code easier to write than the typical “callback hell” that seems to be popular these days. I still need to make some low-level network calls, so I need the net module as well.

On to the configuration:

local SYSLOG  = "127.0.0.1"
local HOST    = "brevard.conman.org"
local CERT    = "brevard.conman.org.cert.pem"
local KEY     = "brevard.conman.org.key.pem"
local ISSUER  = "/C=US/ST=FL/O=Conman Laboratories/OU=Security Division/CN=Conman Laboratories CA/emailAddress=ca@conman.org"
local clients = {}

I didn't bother with a configuration file. This whole code base exists to solve an issue I have as simply as possible. At this point, a configuration file is overkill. The SYSLOG variable defines the address this server will use to accept output from syslog. Due to the way my current syslog daemon works, the port number it uses to forward logs is hard coded, so no need to specify the port. I'm going to run this over TLS because, why not? The tls module makes it easy to use, and it will make authentication trivial for this program. The CERT and KEY are the certificates needed, and these are generated by some scripts I wrote to play around with running my own simple certificate authority. My server is set to accept certificates signed by my simple certificate authority, which you can see in the definition of the ISSUER variable.

The clients variable is to track the the clients that connect to collect syslog output. Even though I'll only ever have one client, it's easy enough to make this an array.

local laddr = net.address(SYSLOG,'udp',514)
local lsock = net.socket(laddr.family,'udp')
lsock:bind(laddr)

nfl.SOCKETS:insert(lsock,'r',function()
  local _,data,err = lsock:recv()
  if data then
    for co in pairs(clients) do
      nfl.schedule(co,data)
    end
  else
    syslog('error',"recv()=%s",errno[err])
  end
end)

And now we create the local socket to receive output from syslog, and then add the socket to a table of sockets the framework uses, telling it to handle “read-ready” events. The data is read and then for each thread (Lua calls them “coroutines”) in the clients list, we schedule said thread to run with the data received from syslog.

local okay,err = tls.listen(HOST,514,client_main,function(conf)
  conf:verify_client()
  return conf:keypair_file(CERT,KEY)
     and conf:protocols("tlsv1.3")
end)

if not okay then
  syslog('error',"tls.listen()=%s",err)
  os.exit(1,true)
end

signal.catch('int')
signal.catch('term')

nfl.server_eventloop(function() return signal.caught() end)
os.exit(0,true)

And before we get to the routine that handles the clients, this is the code that creates a listening socket for TLS connections. We configure the listening socket to require the client send a certificate of its own (this is half of the authentication routine) and the certificates required to secure the connection, and the minimum protocol level. There's some error checking, setting up to catch some signals, then we start the main loop of the framework, which will terminate upon receiving a SIGINT (interrupt) or SIGTERM (terminate).

And finally, the code that runs on each TLS connection:

local function client_main(ios)
  ios:_handshake()
  
  if ios.__ctx:peer_cert_issuer() ~= ISSUER then
    ios:close()
    return
  end
  
  syslog('info',"remote=%s",ios.__remote.addr)
  clients[ios.__co] = true
  
  while true do
    local data = coroutine.yield()
    if not data then break end
    local okay,errmsg = ios:write(data,'\n')
    if not okay then
      syslog('error',"tls:read() = %s",errmsg)
      break
    end
  end
  
  syslog('info',"remote=%s disconnecting",ios.__remote.addr)
  clients[ios.__co] = nil
  ios:close()
end

The handshake is required to ensure that the client certificate is fully sent before we can check the issuer of said certificate. This is the extent of my authentication—I check that the certificate is issued from my simple certificate authority and not just any random but valid certificate being presented. Yes, there is a chance someone could forge a certificate claiming to be from my simple certificate authority, but to get such a certificate, some real certificate authority would need to issue someone else a certificate that maches the issuer on my certificates. I'm not seeing that happening any time soon (and if that happens, there are bigger things I need to worry about).

Once I've authenticated the certificate, I then pause the thread, waiting for data from the UDP socket (see above). If there's no data, then the client has dropped the connection and we exit out of the loop. We then write the data from syslog to the client and if that fails, we exit out of the loop.

Once out of the loop, we close the connection and that's pretty much all there is to it.

Yes, I realize that the calls to syslog() will be sent to the syslog daemon, only to be passed back to this program, but at least there's a log of this on the server.

I should also note that I do not attempt to track which logs have been sent and which haven't—that's a deliberate design decision on my part and I can live with missing logs on my development server. The logs are still recorded on the server itself so if it's important, I still have them, and this keeps this code simple.

The client code on my development server is even simpler:

local clock  = require "org.conman.clock"
local signal = require "org.conman.signal"
local tls    = require "org.conman.net.tls"
local net    = require "org.conman.net"

local SYSLOG = "192.168.1.10"
local HOST   = "brevard.conman.org"
local CERT   = "/home/spc/projects/CA/ca/intermediate/certs/sean.conner.cert.pem"
local KEY    = "/home/spc/projects/CA/ca/intermediate/private/sean.conner.key.pem"

Again, load the required modules, and configure the program. Much like the server, having a configuration file for this is way overkill, thus the above variables.

signal.catch('int')
signal.catch('term')

local addr = net.address(SYSLOG,'udp',514)
local sock = net.socket(addr.family,'udp')

connect(sock,addr)

The code sets up some signal handlers, creates a socket to send the data to syslog and calls the main function.

local function connect(sock,addr)
  local ios,err = tls.connect(HOST,514,function(conf)
  return conf:keypair_file(CERT,KEY)
     and conf:protocols("tlsv1.3")
  end)
  
  if not ios then
    io.stderr:write("Failure: ",err," retrying in a bit ...\n")
    clock.sleep(1)
  else
    io.stderr:write("\n\n\nConnected\n")
    main(ios,sock,addr)
  end
  
  if not signal.caught() then
    return connect(sock,addr)
  end
end

The connect() function tries to connect to the server with the given certificates. If it fails (and I expect this to happen when I get reassigned an IP address) it waits for a bit and retries again. If the connection succeeds though:

local function main(ios,sock,addr)
  for data in ios:lines() do
    if signal.caught() then
      ios:close()
      os.exit(0,true)
    end
    sock:send(addr,data)
  end  
  ios:close()
end

The code just loops, reading lines from the server and then sending them directly to the syslog daemon. Any errors (like the IP address got reassigned so the connection drops) the loop ends, we close the connection and return, falling into the retry loop in the connect() function.

In case anyone is interested, here's the source code for the server and the client.

Obligatory Picture

Dad was resigned to the fact that I was, indeed, a landlubber, and turned the boat around yet again …

Obligatory Contact Info

Obligatory Feeds

Obligatory Links

Obligatory Miscellaneous

Obligatory AI Disclaimer

No AI was used in the making of this site, unless otherwise noted.

You have my permission to link freely to any entry here. Go ahead, I won't bite. I promise.

The dates are the permanent links to that day's entries (or entry, if there is only one entry). The titles are the permanent links to that entry only. The format for the links are simple: Start with the base link for this site: https://boston.conman.org/, then add the date you are interested in, say 2000/08/01, so that would make the final URL:

https://boston.conman.org/2000/08/01

You can also specify the entire month by leaving off the day portion. You can even select an arbitrary portion of time.

You may also note subtle shading of the links and that's intentional: the “closer” the link is (relative to the page) the “brighter” it appears. It's an experiment in using color shading to denote the distance a link is from here. If you don't notice it, don't worry; it's not all that important.

It is assumed that every brand name, slogan, corporate name, symbol, design element, et cetera mentioned in these pages is a protected and/or trademarked entity, the sole property of its owner(s), and acknowledgement of this status is implied.

Copyright © 1999-2025 by Sean Conner. All Rights Reserved.