TL;DR I’m releasing a simple reverse-proxy that is very easy to use and configure programmatically. It’s published as Go package with a vanity domain: go.hasen.dev/core_server
A web server is just a program that listens to incoming tcp connections on port 80 (or 443, if you want to support https, which you should).
The only problem with this statement is that you can only have one program listening on the port.
Now, mitigating this would be easy if the OS would let you listen on a domain:port combination.
The OS provides a mechanism for different programs to listen on different ports, and even on different IP:port combinations, however, it does not provide a mechanism for different web servers to listen on different domains.
This is because the TCP/IP protocol does not know about domain names. They are separate systems.
The HTTP protocol is aware of domain names though; via the Host header. When you visit example.com
, the browser sets the `Host: example.com
` header. But that information is at the http level, not at the tcp level, and as far as I understand, the Linux kernel does not have builtin http support.
So while it is possible in principle to multiplex incoming requests to different programs based on the requested domain, the OS does not provide a standard mechanism for this.
Instead, you need a user-land process to fill this role: it would listen on the http and https ports, handles the TLS handshake, parses the request header, and then forwards the request to another program, based on the Host header.
This kind of program is called a reverse-proxy. A regular proxy (in internet parlance) is used by a client to hide their address: your server sees the reqeust coming from the proxy, instead of the end user. But here we’re doing the opposite: the user sends a request to your server, but unbeknownst to them, your server process is behind a proxy.
The most popular program that does this is nginx
. It’s very versatile and can do many more things than just act as a reverse proxy. It’s just not very easy to configure programmatically: you have to find out where and how it manages its configuration files, and then edit them to add an entry to reverse proxy to your program, and then send it a signal to reload the configuration files.
Another popular tool in recent years appears to be Traefik. I have no experience with it, but their official tutorial reveals that they depend on Docker, and require configurations with weird and obscure options.
A recent addition is Caddy. It’s particularly good because it handles https and uses Let’s Encrypt to automatically issue certificates. It’s written in Go and has an command line interface as well as an http json api. However, it’s not obvious how to do it programmatically via the json API. Essentially, just like nginx, it does many more things than just reverse-proxying.
The problem is actually very simple, and the solution does not need to be that complicated. At least, not complicated to setup and use.
Internally it might involve some complications: handling https, issuing certificates, forwarding requests.
The usage pattern should be very simple:
Launch it via the command line
Safe to launch multiple times (e.g. new version)
Previous process gracefully shuts down
Configuration is carried forward by the new process
Exposes a command that accepts two parameters: the domain and the port
Easy to call this command programmatically from any programming language
Does not output anything to the command line
Log filepath is well defined and easy to discover
I’m releasing a Go program that does just that. Check it out at go.hasen.dev/core_server
It’s only a few hundred lines of code. The way to configure the mapping is to send the following kind of message to the UDP port 40608
add example.com 5555
The structure of the message is simple:
the word add
the domain name
the port number
That’s it.
If you use it, I’d love to hear your feedback.
P.S. I asked an LLM to provide the implementation of a simple function that sends a string message over a udp port, and here’s the output:
Python:
import socket
def send_udp_message(port, message):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(message.encode(), ('localhost', port))
sock.close()
Ruby:
require 'socket'
def send_udp_message(port, message)
sock = TCPSocket.new('localhost', port)
sock.send(message, 0)
sock.close
end
PHP:
function send_udp_message($port, $message) {
$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
socket_sendto($sock, $message, strlen($message), 0, 'localhost', $port);
socket_close($sock);
}
Node.JS (with modifications; perhaps needs further cleanup)
const dgram = require('dgram');
async function sendUDPMessage(port, message) {
return new Promise(resolve => {
const client = dgram.createSocket('udp4');
client.send(message, port, 'localhost', (err) => {
client.close();
resolve(err)
});
})
}
And here’s my Go implementation:
func SendUDP(port int, cmd string) error {
var udpAddress = net.UDPAddr{Port: port}
conn, err := net.DialUDP("udp", nil, &udpAddress)
if err != nil {
return err
}
defer conn.Close()
_, err = io.WriteString(conn, cmd)
return err
}
—