* * * * *

                 Artisanal code to solve an issue only I have

Update on Tuesday, February 4^th, 2025

The code presented below has a bug that has been fixed. [1]. 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 (Internet Service Provider) [2], despite the repeated
letters that my service will go away at some point. But in the meantime, they
keep reissuing a new IP (Internet Protocol) 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 [3]. 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:

-----[ Lua ]-----
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"
-----[ END OF LINE ]-----

The nfl [4] module is my “server framework” for network based servers. Each
TCP (Transmission Control Protocol) or TLS (Transport Layer Security)
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 [5] module as
well.

On to the configuration:

-----[ Lua ]-----
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/[email protected]"
local clients = {}
-----[ END OF LINE ]-----

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
[6] 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.

-----[ Lua ]-----
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)
-----[ END OF LINE ]-----

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.

-----[ Lua ]-----
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)
-----[ END OF LINE ]-----

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:

-----[ Lua ]-----
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
-----[ END OF LINE ]-----

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:

-----[ Lua ]-----
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"
-----[ END OF LINE ]-----

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.

-----[ Lua ]-----
signal.catch('int')
signal.catch('term')

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

connect(sock,addr)
-----[ END OF LINE ]-----

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

-----[ Lua ]-----
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
-----[ END OF LINE ]-----

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:

-----[ Lua ]-----
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
-----[ END OF LINE ]-----

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 [7] and
the client [8].

[1] gopher://gopher.conman.org/0Phlog:2025/02/04.1
[2] gopher://gopher.conman.org/0Phlog:2024/09/30.1
[3] https://github.com/spc476/syslogintr
[4] https://github.com/spc476/lua-conmanorg/blob/master/lua/nfl.lua
[5] https://github.com/spc476/lua-conmanorg/blob/master/src/net.c
[6] https://github.com/spc476/lua-conmanorg/blob/master/src/tls.c
[7] gopher://gopher.conman.org/0Phlog:2025/02/02/slps.lua
[8] gopher://gopher.conman.org/0Phlog:2025/02/02/slpc.lua

Email author at [email protected]