note
This article was last updated on March 10, 2024, 8 months ago. The content may be out of date.
Previously, we talked about how to implement a simple Caddy plugin. In this post, we’ll talk about some of the more powerful features we can use.
Registering New Types of Network
Sometimes, we may want to securely expose a socket on a machine to a remote machine. We can set up an SSH tunnel to do so.
The following example from the official documents shows how to set up an HTTP server this way.
package main
import (
"fmt"
"log"
"net/http"
"golang.org/x/crypto/ssh"
)
func main() {
var hostKey ssh.PublicKey
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
HostKeyCallback: ssh.FixedHostKey(hostKey),
}
// Dial your ssh server.
conn, err := ssh.Dial("tcp", "localhost:22", config)
if err != nil {
log.Fatal("unable to connect: ", err)
}
defer conn.Close()
// Request the remote side to open port 8080 on all interfaces.
l, err := conn.Listen("tcp", "0.0.0.0:8080")
if err != nil {
log.Fatal("unable to register tcp forward: ", err)
}
defer l.Close()
// Serve HTTP with your SSH server acting as a reverse proxy.
http.Serve(l, http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintf(resp, "Hello world!\n")
}))
}
We can achieve so by registering a network.
The most obvious way to do so is this:
caddy.RegisterNetwork("ssh", func(ctx context.Context, network, addr string, cfg net.ListenConfig) (any, error) {
var hostKey ssh.PublicKey
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
HostKeyCallback: ssh.FixedHostKey(hostKey),
}
// Dial your ssh server.
var dialer = net.Dialer{
KeepAlive: cfg.KeepAlive,
Control: cfg.Control,
}
conn, err := dialer.DialContext(ctx, "tcp", "localhost:22")
if err != nil {
return nil, err
}
c, chans, reqs, err := ssh.NewClientConn(conn, addr, config)
if err != nil {
return nil, err
}
client:= ssh.NewClient(c, chans, reqs)
return client.Listen("tcp", addr)
})
info
Aside from non-configurable SSH parameters, this naive approach suffers from non-overlappable listeners, i.e., when the configuration is reloaded, old and new configurations can’t be both active at the same time.
We should use a usage pool to manage listeners. The examples can be found here for platforms that don’t support SO_REUSEPORT
.
We can either use this for HTTP servers, or use it with caddy-l4 to do anything that’s possible with a plain socket.
Using Apps
To expose a socket securely via SSH tunneling is convenient. Being network addresses makes them impossible to embed SSH tunnel information. However, it’s not good to hardcode these parameters because if the setup changes, we have to modify these parameters and recompile Caddy.
We can use environmental variables, but we need to restart Caddy to make sure new environmental variables take effect.
Caddy apps are useful in this case, as we can use them to store SSH tunnel configurations and retrieve them as needed.
We can retrieve an app using Context.App
or Context.AppIfConfigured
depending on if an unconfigured app is acceptable.
note
Refer to previous post to learn how to implement a Caddy app along with Caddyfile parsing.
Namespaces
Sometimes our module needs to be extensible, i.e., giving other developers the means to customize our plugin. Namespaces in the standard distribution allow us to add custom behaviors.
It’s called host module
, but the document never mentions how to deal with them when parsing Caddyfile.
Take a look at reverse_proxy
:
|
|
It’s assumed users will use the last part of the module ID to refer to the specific module, so we unmarshal the module using the full module ID and check if it implements the necessary interface.
Then the module is marshaled for persistence.
|
|
protocol
is the inline key that’s used to find out which guest module to unmarshal.