// Package forward_tcp forwards TCP traffic.
package forward_tcp
import (
"context"
"errors"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/point-c/caddy/module/forward"
"github.com/point-c/caddy/module/point-c"
"github.com/point-c/caddy/pkg/caddyreg"
"github.com/point-c/caddy/pkg/configvalues"
"github.com/point-c/simplewg"
"go.mrchanchal.com/zaphandler"
"io"
"log/slog"
"net"
"sync"
)
func init() {
caddyreg.R[*ForwardTCP]()
}
var (
_ forward.ForwardProto = (*ForwardTCP)(nil)
_ caddy.Module = (*ForwardTCP)(nil)
_ caddyfile.Unmarshaler = (*ForwardTCP)(nil)
_ caddy.Provisioner = (*ForwardTCP)(nil)
_ caddy.CleanerUpper = (*ForwardTCP)(nil)
)
type (
// ForwardTCP is able to forward TCP traffic through networks.
ForwardTCP struct {
Ports configvalues.PortPair `json:"ports"`
BufSize BufSize `json:"buf"`
logger *slog.Logger
ctx context.Context
cancel func()
wait func()
}
BufSize = configvalues.CaddyTextUnmarshaler[uint64, configvalues.ValueUnsigned[uint64], *configvalues.ValueUnsigned[uint64]]
)
// Provision implements [caddy.Provisioner].
func (f *ForwardTCP) Provision(ctx caddy.Context) error {
f.logger = slog.New(zaphandler.New(ctx.Logger()))
f.ctx, f.cancel = context.WithCancel(ctx)
return nil
}
// Cleanup implements [caddy.CleanerUpper].
func (f *ForwardTCP) Cleanup() error {
if f.cancel != nil {
f.cancel()
}
if f.wait != nil {
f.wait()
}
f.ctx = nil
f.wait = nil
f.logger = nil
f.cancel = nil
return nil
}
// CaddyModule implements [caddy.Module].
func (f *ForwardTCP) CaddyModule() caddy.ModuleInfo {
return caddyreg.Info[ForwardTCP, *ForwardTCP]("point-c.op.forward.tcp")
}
// UnmarshalCaddyfile unmarshals the caddyfile.
// Buffer size is the size of the buffer to use per stream direction.
// Buffer size will be double the specified amount per connection.
// ```
//
// point-c netops {
// forward <src network name>:<dst network name> {
// tcp <src port>:<dst port> [buffer size]
// }
// }
//
// ```
func (f *ForwardTCP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
if d.Val() == "tcp" {
if !d.NextArg() {
return d.ArgErr()
}
}
if err := f.Ports.UnmarshalCaddyfile(d); err != nil {
return err
}
if d.NextArg() {
if err := f.BufSize.UnmarshalCaddyfile(d); err != nil {
return err
}
}
}
return nil
}
// Start implements [ForwardProto]. It is responsible for starting the forwarding of network traffic.
func (f *ForwardTCP) Start(n *forward.ForwardNetworks) error {
ln, err := n.Src.Listen(&net.TCPAddr{IP: n.Src.LocalAddr(), Port: int(f.Ports.Value().Left)})
if err != nil {
return err
}
context.AfterFunc(f.ctx, func() { ln.Close() })
var wg simplewg.Wg
f.wait = wg.Wait
bufSize := f.BufSize.Value()
if bufSize == 0 {
bufSize = 4096
}
copyBufs := sync.Pool{New: func() any { return make([]byte, bufSize) }}
conns := make(chan net.Conn)
wg.Go(func() { ListenLoop(ln, conns) })
pairs := make(chan *ConnPair)
var pairsListeners sync.WaitGroup
dialed := make(chan *ConnPair)
var dialedListeners sync.WaitGroup
for i := 0; i < 10; i++ {
pairsListeners.Add(1)
dialedListeners.Add(1)
wg.Go(func() { defer pairsListeners.Done(); PrepareConnPairLoop(f.ctx, f.logger, conns, pairs) })
wg.Go(func() { defer dialedListeners.Done(); DialRemoteLoop(n.Dst, f.Ports.Value().Right, pairs, dialed) })
wg.Go(func() {
StartCopyLoop(dialed, func(done func(), logger *slog.Logger, dst io.Writer, src io.Reader) {
buf := copyBufs.Get().([]byte)
defer copyBufs.Put(buf)
TcpCopy(done, logger, dst, src, buf)
})
})
}
go func() {
defer close(pairs)
pairsListeners.Wait()
}()
go func() {
defer close(dialed)
dialedListeners.Wait()
}()
return nil
}
// ListenLoop accepts connections and sends them to the next operation.
func ListenLoop(ln net.Listener, conns chan<- net.Conn) {
defer close(conns)
for {
c, err := ln.Accept()
if err != nil {
return
}
conns <- c
}
}
var connPairPool = sync.Pool{New: func() any { return new(ConnPair) }}
// PrepareConnPairLoop initializes the forwarding session.
func PrepareConnPairLoop(ctx context.Context, logger *slog.Logger, conns <-chan net.Conn, pairs chan<- *ConnPair) {
for c := range conns {
select {
case <-ctx.Done():
c.Close()
default:
cp := connPairPool.Get().(*ConnPair)
cp.Tunnel = nil
cp.Logger = logger
// Copy c so it can be used in goroutines
cp.Remote = c
// Prepare connection specific context
cp.Ctx, cp.Cancel = context.WithCancel(ctx)
// Prevent leakage?
context.AfterFunc(ctx, cp.Cancel)
// Close remote connection when context is canceled
context.AfterFunc(cp.Ctx, func() { cp.Remote.Close() })
pairs <- cp
}
}
}
// ConnPair helps manage the state of a forwarding session.
type ConnPair struct {
Ctx context.Context
Cancel context.CancelFunc
Remote net.Conn
Tunnel net.Conn
Logger *slog.Logger
}
// DialTunnel does the actual remote dialing.
func (cp *ConnPair) DialTunnel(n point_c.Net, dstPort uint16) bool {
// Prepare dialer that will preserve remote ip
remote := cp.Remote.RemoteAddr().(*net.TCPAddr).IP
d := n.Dialer(remote, 0)
// Dial in the tunnel
rc, err := d.Dial(cp.Ctx, &net.TCPAddr{IP: n.LocalAddr(), Port: int(dstPort)})
if err != nil {
cp.Logger.Error("failed to dial", "remote", remote, "local", n.LocalAddr(), "port", dstPort, "err", err)
// Don't leak context, close remote connection
cp.Cancel()
// Remote might be temporarily down, don't kill everything because of one problem
return false
}
cp.Tunnel = rc
// Close tunnel connection when context is canceled
context.AfterFunc(cp.Ctx, func() { cp.Tunnel.Close() })
return true
}
// DialRemoteLoop is responsible for dialing the receiver.
func DialRemoteLoop(n point_c.Net, dstPort uint16, pairs <-chan *ConnPair, dialed chan<- *ConnPair) {
var wg simplewg.Wg
// Wait for any senders on pairs to finish
defer wg.Wait()
for c := range pairs {
select {
case <-c.Ctx.Done():
connPairPool.Put(c)
c.Cancel()
default:
c := c
wg.Go(func() {
if c.DialTunnel(n, dstPort) {
dialed <- c
}
})
}
}
}
// StartCopyLoop manages starting the copy for both TCP stream directions.
func StartCopyLoop(pairs <-chan *ConnPair, copyFn func(done func(), logger *slog.Logger, dst io.Writer, src io.Reader)) {
var wg simplewg.Wg
defer wg.Wait()
for p := range pairs {
select {
case <-p.Ctx.Done():
connPairPool.Put(p)
p.Cancel()
default:
c := p
var pwg sync.WaitGroup
pwg.Add(2)
wg.Go(func() { defer pwg.Done(); copyFn(c.Cancel, c.Logger, c.Remote, c.Tunnel) })
wg.Go(func() { defer pwg.Done(); copyFn(c.Cancel, c.Logger, c.Tunnel, c.Remote) })
go func() {
defer connPairPool.Put(c)
pwg.Wait()
}()
}
}
}
// TcpCopy is the low level function that does the actual copying of TCP traffic. It only copies the stream in one direction e.g. src->dst or dst->src.
func TcpCopy(done func(), logger *slog.Logger, dst io.Writer, src io.Reader, buf []byte) {
defer done()
if _, err := io.CopyBuffer(dst, src, buf); err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
logger.Error("error copying data between connections", "error", err)
}
}
// Package forward manages network forwarders.
package forward
import (
"encoding/json"
"errors"
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/point-c/caddy/module/point-c"
"github.com/point-c/caddy/pkg/caddyreg"
"github.com/point-c/caddy/pkg/configvalues"
"github.com/point-c/caddy/pkg/lifecycler"
)
func init() {
caddyreg.R[*Forward]()
}
var (
_ point_c.NetOp = (*Forward)(nil)
_ caddy.Provisioner = (*Forward)(nil)
_ caddy.CleanerUpper = (*Forward)(nil)
_ caddy.Module = (*Forward)(nil)
_ caddyfile.Unmarshaler = (*Forward)(nil)
)
type (
// Forward manages forwarders for internet traffic.
Forward struct {
ForwardsRaw []json.RawMessage `json:"forwards,omitempty" caddy:"namespace=point-c.op.forward inline_key=forward"`
Hosts configvalues.HostnamePair `json:"hosts"`
lf lifecycler.LifeCycler[*ForwardNetworks]
}
// ForwardProto is implemented by modules in the "point-c.op.forward" namespace.
ForwardProto = lifecycler.LifeCyclable[*ForwardNetworks]
// ForwardNetworks contains the networks that have their traffic forwarded.
ForwardNetworks struct{ Src, Dst point_c.Net }
)
// Provision implements [caddy.Provisioner].
func (f *Forward) Provision(ctx caddy.Context) error {
return f.lf.Provision(ctx, &lifecycler.ProvisionInfo{
StructPointer: f,
FieldName: "ForwardsRaw",
Raw: &f.ForwardsRaw,
})
}
// Start implements [NetOp].
func (f *Forward) Start(lookup point_c.NetLookup) error {
check := func(name string, n *point_c.Net) error {
if v, ok := lookup.Lookup(name); ok {
*n = v
return nil
}
return fmt.Errorf("host %q not found", name)
}
var fn ForwardNetworks
if err := errors.Join(
check(f.Hosts.Value().Left, &fn.Src),
check(f.Hosts.Value().Right, &fn.Dst),
); err != nil {
return err
}
f.lf.SetValue(&fn)
return f.lf.Start()
}
// Cleanup implements [caddy.CleanerUpper].
func (f *Forward) Cleanup() error { return f.lf.Cleanup() }
// CaddyModule implements [caddy.Module].
func (f *Forward) CaddyModule() caddy.ModuleInfo {
return caddyreg.Info[Forward, *Forward]("point-c.op.forward")
}
// UnmarshalCaddyfile unmarshals the caddyfile.
// ```
//
// point-c netops {
// forward <src network name>:<dst network name> {
// <submodule name> <submodule config>
// }
// }
//
// ```
func (f *Forward) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return f.lf.UnmarshalCaddyfile(d, &lifecycler.CaddyfileInfo{
ModuleID: []string{"point-c", "op", "forward"},
Raw: &f.ForwardsRaw,
SubModuleSpecifier: "forward",
ParseVerbLine: &f.Hosts,
})
}
// Package listener allows Caddy to listen on arbitrary networks.
package listener
import (
"errors"
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/point-c/caddy/module/merge-listener-wrapper"
point_c2 "github.com/point-c/caddy/module/point-c"
"github.com/point-c/caddy/pkg/caddyreg"
"github.com/point-c/caddy/pkg/configvalues"
"net"
)
func init() {
caddyreg.R[*Listener]()
}
var (
_ caddy.Provisioner = (*Listener)(nil)
_ net.Listener = (*Listener)(nil)
_ caddy.Module = (*Listener)(nil)
_ caddyfile.Unmarshaler = (*Listener)(nil)
_ merge_listener_wrapper.ListenerProvider = (*Listener)(nil)
)
// Listener allows a caddy server to listen on a point-c network.
type Listener struct {
Name configvalues.Hostname `json:"name"`
Port configvalues.Port `json:"port"`
ln net.Listener
}
// Provision implements [caddy.Provisioner].
func (p *Listener) Provision(ctx caddy.Context) error {
m, err := ctx.App("point-c")
if err != nil {
return err
}
n, ok := m.(point_c2.NetLookup).Lookup(p.Name.Value())
if !ok {
return fmt.Errorf("point-c net %q does not exist", p.Name.Value())
}
ln, err := n.Listen(&net.TCPAddr{IP: n.LocalAddr(), Port: int(p.Port.Value())})
if err != nil {
return err
}
p.ln = ln
return nil
}
// Accept implements [net.Listener].
func (p *Listener) Accept() (net.Conn, error) { return p.ln.Accept() }
// Close implements [net.Listener].
func (p *Listener) Close() error { return p.ln.Close() }
// Addr implements [net.Listener].
func (p *Listener) Addr() net.Addr { return p.ln.Addr() }
// CaddyModule implements [caddy.Module].
func (*Listener) CaddyModule() caddy.ModuleInfo {
return caddyreg.Info[Listener, *Listener]("caddy.listeners.merge.point-c")
}
// Start implement [ListenerProvider].
func (p *Listener) Start(fn func(net.Listener)) error { fn(p); return nil }
// UnmarshalCaddyfile unmarshals the caddyfile.
// ```
//
// {
// servers :443 {
// listener_wrappers {
// merge {
// # this is the actual listener definition
// point-c <network name> <port to expose>
// }
// # make sure tls goes after otherwise encryption will be dropped
// tls
// }
// }
// }
//
// ```
func (p *Listener) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
var name, port string
if !d.Args(&name, &port) {
return d.ArgErr()
}
if err := errors.Join(
p.Name.UnmarshalText([]byte(name)),
p.Port.UnmarshalText([]byte(port)),
); err != nil {
return err
}
}
return nil
}
// Package merge_listener_wrapper allows Caddy to listen on multiple listeners.
package merge_listener_wrapper
import (
"encoding/json"
"errors"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/point-c/caddy/pkg/caddyreg"
"github.com/point-c/caddy/pkg/channel-listener"
"github.com/point-c/caddy/pkg/lifecycler"
"net"
)
func init() {
caddyreg.R[*MergeWrapper]()
}
var (
_ caddy.Provisioner = (*MergeWrapper)(nil)
_ caddy.CleanerUpper = (*MergeWrapper)(nil)
_ caddy.ListenerWrapper = (*MergeWrapper)(nil)
_ caddy.Module = (*MergeWrapper)(nil)
_ caddyfile.Unmarshaler = (*MergeWrapper)(nil)
)
type (
// MergeWrapper loads multiple [net.Listener]s and aggregates their [net.Conn]s into a single [net.Listener].
// It allows caddy to accept connections from multiple sources.
MergeWrapper struct {
// ListenerRaw is a slice of JSON-encoded data representing listener configurations.
// These configurations are used to create the actual net.Listener instances.
// Listeners should implement [net.Listener] and be in the 'caddy.listeners.merge.listeners' namespace.
ListenerRaw []json.RawMessage `json:"listeners" caddy:"namespace=caddy.listeners.merge inline_key=listener"`
// listeners is a slice of net.Listener instances created based on the configurations
// provided in ListenerRaw. These listeners are the actual network listeners that
// will be accepting connections.
listeners []net.Listener
// conns is a channel for net.Conn instances. Connections accepted by any of the
// listeners in the 'listeners' slice are sent through this channel.
// This channel is passed to the constructor of [channel_listener.Listener].
conns chan net.Conn
lf lifecycler.LifeCycler[func(net.Listener)]
}
// ListenerProvider is implemented by modules in the "caddy.listeners.merge" namespace.
ListenerProvider lifecycler.LifeCyclable[func(net.Listener)]
)
// CaddyModule implements [caddy.Module].
func (p *MergeWrapper) CaddyModule() caddy.ModuleInfo {
return caddyreg.Info[MergeWrapper, *MergeWrapper]("caddy.listeners.merge")
}
// Provision implements [caddy.Provisioner].
// It loads the listeners from their configs and asserts them to [net.Listener].
// Any failed assertions will cause a panic.
func (p *MergeWrapper) Provision(ctx caddy.Context) error {
p.conns = make(chan net.Conn)
p.lf.SetValue(func(ln net.Listener) { p.listeners = append(p.listeners, ln) })
if err := p.lf.Provision(ctx, &lifecycler.ProvisionInfo{
StructPointer: p,
FieldName: "ListenerRaw",
Raw: &p.ListenerRaw,
}); err != nil {
return err
}
return p.lf.Start()
}
// Cleanup implements [caddy.CleanerUpper].
// All wrapped listeners are closed and the struct is cleared.
func (p *MergeWrapper) Cleanup() (err error) {
for _, ln := range p.listeners {
err = errors.Join(err, ln.Close())
}
err = errors.Join(err, p.lf.Cleanup())
*p = MergeWrapper{}
return
}
// WrapListener implements [caddy.ListenerWrapper].
// The listener passed in is closed by [MergeWrapper] during cleanup.
func (p *MergeWrapper) WrapListener(ls net.Listener) net.Listener {
p.listeners = append(p.listeners, ls)
cl := channel_listener.New(p.conns, ls.Addr())
for _, ls := range p.listeners {
go listen(ls, p.conns, cl.Done(), cl.CloseWithErr)
}
return cl
}
// listen manages incoming network connections on a given listener.
// It sends accepted connections to the 'conns' channel. When a
// signal is sent to the 'done' channel any accepted connections not passed on are closed and ignored.
// In case of an error during accepting a connection, it calls the 'finish' function with the error.
func listen(ls net.Listener, conns chan<- net.Conn, done <-chan struct{}, finish func(error) error) {
for {
c, err := ls.Accept()
if err != nil {
// If one connection errors on Accept, pass the error on and close all other connections.
// Only the first error from an Accept will be passed on.
_ = finish(err)
return
}
select {
case <-done:
// The connection has been closed, close the received connection and ignore it.
_ = c.Close()
continue
case conns <- c:
// Connection has been accepted
}
}
}
// UnmarshalCaddyfile implements [caddyfile.Unmarshaler].
// Must have at least one listener to aggregate with the wrapped listener.
// `tls` should come specifically after any `merge` directives.
//
// ```
//
// http caddyfile:
// {
// servers :443 {
// listener_wrappers {
// merge {
// <submodule name> <submodule config>
// }
// tls
// }
// }
// }
//
// ```
func (p *MergeWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return p.lf.UnmarshalCaddyfile(d, &lifecycler.CaddyfileInfo{
ModuleID: []string{"caddy", "listeners", "merge"},
Raw: &p.ListenerRaw,
SubModuleSpecifier: "listener",
})
}
// Package module allows importing all modules at once.
package module
import (
_ "github.com/point-c/caddy/module/forward"
_ "github.com/point-c/caddy/module/forward-tcp"
_ "github.com/point-c/caddy/module/listener"
_ "github.com/point-c/caddy/module/merge-listener-wrapper"
_ "github.com/point-c/caddy/module/point-c"
_ "github.com/point-c/caddy/module/rand"
_ "github.com/point-c/caddy/module/stub-listener"
_ "github.com/point-c/caddy/module/sysnet"
_ "github.com/point-c/caddy/module/wg"
)
func init() { _ = 1 }
// Package point_c allows Caddy to manage networks.
package point_c
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/point-c/caddy/pkg/caddyreg"
"github.com/point-c/caddy/pkg/configvalues"
"github.com/point-c/caddy/pkg/lifecycler"
"net"
)
const (
CaddyfilePointCName = "point-c"
)
func init() {
caddyreg.R[*Pointc]()
httpcaddyfile.RegisterGlobalOption(CaddyfilePointCName, configvalues.CaddyfileUnmarshaler[Pointc, *Pointc](CaddyfilePointCName))
}
var (
_ caddy.Provisioner = (*Pointc)(nil)
_ caddy.Module = (*Pointc)(nil)
_ caddy.App = (*Pointc)(nil)
_ caddyfile.Unmarshaler = (*Pointc)(nil)
_ caddy.CleanerUpper = (*Pointc)(nil)
_ NetLookup = (*Pointc)(nil)
)
type (
// RegisterFunc registers a unique name to a [Net] tunnel.
// Since ip addresses may be arbitrary depending on what the application is doing in the tunnel, names are used as lookup.
// This also helps with configuration, so that users don't need to remember ip addresses.
RegisterFunc func(string, Net) error
// Net is a peer in the networking stack. If it has a local address [Net.LocalAddress] should return a non-nil value.
Net interface {
// Listen listens on the given address with the TCP protocol.
Listen(addr *net.TCPAddr) (net.Listener, error)
// ListenPacket listens on the given address with the UDP protocol.
ListenPacket(addr *net.UDPAddr) (net.PacketConn, error)
// Dialer returns a [Dialer] with a given local address. If the network does not support arbitrary remote addresses this value can be ignored.
Dialer(laddr net.IP, port uint16) Dialer
// LocalAddr is the local address of the net interface. If it does not have one, return nil.
LocalAddr() net.IP
}
Dialer interface {
// Dial dials a remote address with the TCP protocol.
Dial(context.Context, *net.TCPAddr) (net.Conn, error)
// DialPacket dials a remote address with the UDP protocol.
DialPacket(*net.UDPAddr) (net.PacketConn, error)
}
// Network is implemented by modules in the "point-c.net" namespace.
Network = lifecycler.LifeCyclable[RegisterFunc]
// NetOp is implemented by modules in the "point-c.op" namespace.
NetOp = lifecycler.LifeCyclable[NetLookup]
)
// NetLookup is implemented by [Pointc].
type NetLookup interface {
Lookup(string) (Net, bool)
}
// Pointc allows usage of networks through a [net]-ish interface.
type Pointc struct {
NetworksRaw []json.RawMessage `json:"networks,omitempty" caddy:"namespace=point-c.net inline_key=type"`
NetOps []json.RawMessage `json:"net-ops,omitempty" caddy:"namespace=point-c.op inline_key=op"`
lf lifecycler.LifeCycler[RegisterFunc]
ops lifecycler.LifeCycler[NetLookup]
net map[string]Net
}
// CaddyModule implements [caddy.Module].
func (*Pointc) CaddyModule() caddy.ModuleInfo {
return caddyreg.Info[Pointc, *Pointc]("point-c")
}
// Register adds a new network to the [Pointc] instance.
// The 'key' parameter is a unique identifier for the network.
// On success, the network is registered with the [Pointc] instance.
func (pc *Pointc) Register(key string, n Net) error {
if _, ok := pc.net[key]; ok {
return fmt.Errorf("network %q already exists", key)
}
pc.net[key] = n
return nil
}
// Provision implements [caddy.Provisioner].
func (pc *Pointc) Provision(ctx caddy.Context) error {
pc.net = map[string]Net{}
pc.ops.SetValue(pc)
pc.lf.SetValue(pc.Register)
if err := pc.lf.Provision(ctx, &lifecycler.ProvisionInfo{
StructPointer: pc,
FieldName: "NetworksRaw",
Raw: &pc.NetworksRaw,
}); err != nil {
return err
}
if err := pc.lf.Start(); err != nil {
return err
}
if err := pc.ops.Provision(ctx, &lifecycler.ProvisionInfo{
StructPointer: pc,
FieldName: "NetOps",
Raw: &pc.NetOps,
}); err != nil {
return err
}
return pc.ops.Start()
}
// Start implements [caddy.App].
func (pc *Pointc) Start() error { return nil }
// Stop implements [caddy.App].
func (pc *Pointc) Stop() error { return nil }
// Cleanup implements [caddy.CleanerUpper].
func (pc *Pointc) Cleanup() error { return errors.Join(pc.lf.Cleanup(), pc.ops.Cleanup()) }
// Lookup gets a [Net] by its declared name.
func (pc *Pointc) Lookup(name string) (Net, bool) {
n, ok := pc.net[name]
return n, ok
}
// UnmarshalCaddyfile unmarshals a submodules from a caddyfile.
// The `netops` modifier causes the modules to be loaded as netops.
//
// ```
// {
// point-c [netops] {
// <submodule name> <submodule config>
// }
// }
// ```
func (pc *Pointc) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
if v := d.Val(); !d.NextArg() && v == "point-c" {
return pc.lf.UnmarshalCaddyfile(d.NewFromNextSegment(), &lifecycler.CaddyfileInfo{
ModuleID: []string{"point-c", "net"},
Raw: &pc.NetworksRaw,
SubModuleSpecifier: "type",
})
} else if v := d.Val(); v == "netops" {
return pc.ops.UnmarshalCaddyfile(d.NewFromNextSegment(), &lifecycler.CaddyfileInfo{
ModuleID: []string{"point-c", "op"},
Raw: &pc.NetOps,
SubModuleSpecifier: "op",
})
} else {
return fmt.Errorf("unrecognized verb %q", v)
}
}
return nil
}
// Package rand contains a Caddy handler that allows random data to be returned.
package rand
import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/point-c/caddy/pkg/caddyreg"
"io"
"math/rand"
"net/http"
"strconv"
"time"
)
func init() {
caddyreg.R[*Rand]()
httpcaddyfile.RegisterHandlerDirective("rand", func(httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { return new(Rand), nil })
}
var (
_ caddy.Module = (*Rand)(nil)
_ caddyhttp.MiddlewareHandler = (*Rand)(nil)
_ caddyfile.Unmarshaler = (*Rand)(nil)
)
// Rand struct represents the custom Caddy HTTP handler for generating random data.
type Rand struct{}
// UnmarshalCaddyfile implements [caddyfile.Unmarshaler].
func (r *Rand) UnmarshalCaddyfile(*caddyfile.Dispenser) error { return nil }
// CaddyModule implemnts [caddy.Module].
func (r *Rand) CaddyModule() caddy.ModuleInfo {
return caddyreg.Info[Rand, *Rand]("http.handlers.rand")
}
// ServeHTTP handles sending random data to the client. The following headers can be optionally set to modify the generation of data:
// - `Rand-Seed`: int64 seed value for the random data generator.
// - `Rand-Size`: int64 size in bytes of data to generate. A value less than zero represents an infinite stream.
func (r *Rand) ServeHTTP(resp http.ResponseWriter, req *http.Request, _ caddyhttp.Handler) (err error) {
if err = req.Body.Close(); err == nil {
if h := NewHeaderValues(req.Header); h.Size() < 0 {
_, err = io.Copy(resp, NewRand(h.Seed()))
} else {
_, err = io.Copy(resp, io.LimitReader(NewRand(h.Seed()), h.Size()))
}
}
return
}
// NewRand function creates a new random data generator.
var NewRand = func(seed int64) io.Reader { return rand.New(rand.NewSource(seed)) }
type (
// HeaderValues struct holds the seed and size values extracted from headers.
HeaderValues struct {
seed HeaderValue[HeaderSeed]
size HeaderValue[HeaderSize]
}
HeaderValue[K HeaderKey] int64
)
// NewHeaderValues creates a new [HeaderValues] instance from HTTP headers.
func NewHeaderValues(headers http.Header) *HeaderValues {
var h HeaderValues
h.seed.GetValue(headers)
h.size.GetValue(headers)
return &h
}
// Seed returns the seed value.
func (h *HeaderValues) Seed() int64 { return int64(h.seed) }
// Size returns the size value
func (h *HeaderValues) Size() int64 { return int64(h.size) }
// GetValue extracts a value from the HTTP headers.
func (h *HeaderValue[K]) GetValue(headers http.Header) {
var k K
*h = HeaderValue[K](k.Default())
n, err := strconv.ParseInt(headers.Get(k.Key()), 10, 64)
if err == nil {
*h = HeaderValue[K](n)
}
}
type (
HeaderSeed struct{}
HeaderSize struct{}
HeaderKey interface {
Key() string
Default() int64
}
)
var nowSrc = time.Now
// Key returns `Rand-Seed`.
func (HeaderSeed) Key() string { return "Rand-Seed" }
// Default returns the current unix micro time.
func (HeaderSeed) Default() int64 { return nowSrc().UnixMicro() }
// Key return `Rand-Size`.
func (HeaderSize) Key() string { return "Rand-Size" }
// Default returns -1.
func (HeaderSize) Default() int64 { return -1 }
// Package stub_listener is a Caddy network that prevents caddy from listening on the host.
package stub_listener
import (
"context"
"github.com/caddyserver/caddy/v2"
"github.com/point-c/caddy/pkg/channel-listener"
"net"
)
// NetworkStubName is the matcher for the network protocol. The ':' suffix is required.
const NetworkStubName = "stub:"
func init() {
caddy.RegisterNetwork(NetworkStubName, StubListener)
}
// StubListener creates a stub network listener. This listener does not accept
// actual network connections but instead blocks on Accept calls until Close is called.
// It can be used as a base listener when only tunnel listeners are required.
func StubListener(_ context.Context, _, addr string, _ net.ListenConfig) (any, error) {
return channel_listener.New(make(<-chan net.Conn), stubAddr(addr)), nil
}
// stubAddr implements [net.Addr] for [StubListener].
type stubAddr string
// Network always returns "stub".
func (stubAddr) Network() string { return "stub" }
// String return [stubAddr] as a string.
func (d stubAddr) String() string { return string(d) }
// Package sysnet is a point-c network for the host network.
package sysnet
import (
"context"
"errors"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/point-c/caddy/module/point-c"
"github.com/point-c/caddy/pkg/caddyreg"
"github.com/point-c/caddy/pkg/configvalues"
"io"
"net"
)
var (
_ caddy.Module = (*Sysnet)(nil)
_ caddy.Provisioner = (*Sysnet)(nil)
_ caddy.CleanerUpper = (*Sysnet)(nil)
_ caddyfile.Unmarshaler = (*Sysnet)(nil)
_ point_c.Network = (*Sysnet)(nil)
_ point_c.Net = (*Sysnet)(nil)
)
func init() { caddyreg.R[*Sysnet]() }
// Sysnet is a point-c network that can dial and listen on the host system.
type Sysnet struct {
Hostname configvalues.Hostname `json:"hostname"`
DialAddr configvalues.ResolvedIP `json:"dial-addr"`
Local configvalues.ResolvedIP `json:"local"`
ctx context.Context
cancel context.CancelFunc
}
// Provision implements [caddy.Provision].
func (s *Sysnet) Provision(c caddy.Context) error {
s.ctx, s.cancel = context.WithCancel(c)
return nil
}
// Cleanup implements [caddy.CleanerUpper].
func (s *Sysnet) Cleanup() error { s.cancel(); return nil }
// LocalAddr returns the address this module is configured with.
func (s *Sysnet) LocalAddr() net.IP { return s.Local.Value() }
// Start implements [Network]. Is registers this module with the given hostname.
func (s *Sysnet) Start(fn point_c.RegisterFunc) error { return fn(s.Hostname.Value(), s) }
// CaddyModule implements [caddy.Module].
func (s *Sysnet) CaddyModule() caddy.ModuleInfo {
return caddyreg.Info[Sysnet, *Sysnet]("point-c.net.system")
}
// UnmarshalCaddyfile implements [caddyfule.Unmarshaler].
//
// point-c {
// system <network name> <dial ip or hostname> <local ip or hostname>
// }
func (s *Sysnet) UnmarshalCaddyfile(d *caddyfile.Dispenser) (err error) {
unmarshalers := []func(*caddyfile.Dispenser) error{
s.Hostname.UnmarshalCaddyfile,
s.DialAddr.UnmarshalCaddyfile,
s.Local.UnmarshalCaddyfile,
}
for d.Next() {
for len(unmarshalers) > 0 && d.NextArg() {
if v := d.Val(); v == "" || v == "system" {
continue
} else if err := unmarshalers[0](d); err != nil {
return err
}
unmarshalers = unmarshalers[1:]
}
}
for i := range unmarshalers {
switch i {
case 0:
err = errors.Join(err, errors.New("local address not set"))
case 1:
err = errors.Join(err, errors.New("dial address not set"))
case 2:
err = errors.Join(err, errors.New("hostname not set"))
}
}
return
}
// Listen listens on the given address using TCP.
func (s *Sysnet) Listen(addr *net.TCPAddr) (net.Listener, error) {
return CaddyListen[net.Listener](s.ctx, addr)
}
// ListenPacket listens on the given address using UDP.
func (s *Sysnet) ListenPacket(addr *net.UDPAddr) (net.PacketConn, error) {
return CaddyListen[net.PacketConn](s.ctx, addr)
}
// CaddyListen helps with listening on an address using Caddy's method.
// The listener is type asserted to T. An error will be returned if the assertion fails.
func CaddyListen[T any](ctx context.Context, addr net.Addr) (v T, err error) {
var na caddy.NetworkAddress
na, err = caddy.ParseNetworkAddress(addr.Network() + "/" + addr.String())
if err != nil {
return
}
var ln any
ln, err = na.Listen(ctx, 0, net.ListenConfig{})
if err != nil {
return
}
l, ok := ln.(T)
if !ok {
err = errors.New("invalid listener type")
if cl, ok := ln.(io.Closer); ok {
err = errors.Join(err, cl.Close())
}
return
}
return l, nil
}
// SysDialer allows dialing TCP and UDP connections on the system.
type SysDialer struct {
ctx context.Context
local net.IP
port uint16
}
// Dialer returns a [SysDialer] ready to dial on the given address and port.
func (s *Sysnet) Dialer(_ net.IP, port uint16) point_c.Dialer {
return &SysDialer{ctx: s.ctx, local: s.DialAddr.Value(), port: port}
}
// Dial dials the given address using TCP.
func (s *SysDialer) Dial(ctx context.Context, addr *net.TCPAddr) (net.Conn, error) {
ctx, cancel := context.WithCancel(ctx)
context.AfterFunc(s.ctx, cancel)
return (&net.Dialer{LocalAddr: &net.TCPAddr{IP: s.local, Port: int(s.port)}}).DialContext(ctx, "tcp", addr.String())
}
// DialPacket dials the given address using UDP.
func (s *SysDialer) DialPacket(addr *net.UDPAddr) (net.PacketConn, error) {
ln, err := (&net.Dialer{LocalAddr: &net.UDPAddr{IP: s.local, Port: int(s.port)}}).DialContext(s.ctx, "udp", addr.String())
if err != nil {
return nil, err
}
return ln.(net.PacketConn), nil
}
// Package wg is a point-c network for a WireGuard tunnel.
package wg
import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
pointc "github.com/point-c/caddy/module/point-c"
"github.com/point-c/caddy/module/wg/internal"
"github.com/point-c/caddy/pkg/configvalues"
"github.com/point-c/wgapi/wgconfig"
)
var (
_ caddy.Module = (*Client)(nil)
_ caddy.Provisioner = (*Client)(nil)
_ caddy.CleanerUpper = (*Client)(nil)
_ pointc.Network = (*Client)(nil)
)
func init() {
caddy.RegisterModule(new(Client))
}
func (*Client) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "point-c.net.wgclient",
New: func() caddy.Module { return new(Client) },
}
}
// Client is a basic wireguard client.
type (
Client struct {
Name configvalues.Hostname `json:"name"`
Endpoint configvalues.UDPAddr `json:"endpoint"`
IP configvalues.IP `json:"ip"`
Private configvalues.PrivateKey `json:"private"`
Public configvalues.PublicKey `json:"public"`
Preshared configvalues.PresharedKey `json:"preshared"`
wg internal.Wg
}
)
// UnmarshalCaddyfile unmarshals a config in caddyfile form.
//
// {
// point-c {
// wgclient <name> {
// ip <tunnel ip>
// endpoint <server address/ip>
// private <client private key>
// public <server public key>
// shared <shared key>
// }
// }
func (c *Client) UnmarshalCaddyfile(d *caddyfile.Dispenser) (err error) {
return c.wg.UnmarshalCaddyfile(d, c.Name.UnmarshalCaddyfile, internal.CaddyfileKeyValues{
"ip": c.IP.UnmarshalCaddyfile,
"endpoint": c.Endpoint.UnmarshalCaddyfile,
"private": c.Private.UnmarshalCaddyfile,
"public": c.Public.UnmarshalCaddyfile,
"shared": c.Preshared.UnmarshalCaddyfile,
})
}
func (c *Client) Start(fn pointc.RegisterFunc) error {
cfg := wgconfig.Client{
Private: c.Private.Value(),
Public: c.Public.Value(),
PreShared: c.Preshared.Value(),
Endpoint: *c.Endpoint.Value(),
}
cfg.DefaultPersistentKeepAlive()
cfg.AllowAllIPs()
return c.wg.Start(fn, map[string]*internal.Net{c.Name.Value(): {IP: c.IP.Value()}}, &cfg)
}
func (c *Client) Cleanup() error { return c.wg.Cleanup() }
func (c *Client) Provision(ctx caddy.Context) error { return c.wg.Provision(ctx) }
package internal
import (
"context"
pointc "github.com/point-c/caddy/module/point-c"
"github.com/point-c/wg"
"golang.zx2c4.com/wireguard/conn"
"net"
)
type (
Dialer struct{ WgDialer }
WgDialer interface {
DialTCP(ctx context.Context, addr *net.TCPAddr) (net.Conn, error)
DialUDP(addr *net.UDPAddr) (net.PacketConn, error)
}
WgNet interface {
Listen(addr *net.TCPAddr) (net.Listener, error)
ListenPacket(addr *net.UDPAddr) (net.PacketConn, error)
Dialer(laddr net.IP, port uint16) *wg.Dialer
}
)
func (c *Dialer) Dial(ctx context.Context, addr *net.TCPAddr) (net.Conn, error) {
return c.DialTCP(ctx, addr)
}
func (c *Dialer) DialPacket(addr *net.UDPAddr) (net.PacketConn, error) {
return c.DialUDP(addr)
}
type Net struct {
Net WgNet
IP net.IP
}
func (c *Net) Listen(addr *net.TCPAddr) (net.Listener, error) { return c.Net.Listen(addr) }
func (c *Net) LocalAddr() net.IP { return c.IP }
func (c *Net) ListenPacket(addr *net.UDPAddr) (net.PacketConn, error) {
return c.Net.ListenPacket(addr)
}
func (c *Net) Dialer(laddr net.IP, port uint16) pointc.Dialer {
return &Dialer{c.Net.Dialer(laddr, port)}
}
type TestBind struct{}
func (TestBind) Send(bufs [][]byte, ep conn.Endpoint) error { return nil }
func (TestBind) ParseEndpoint(s string) (conn.Endpoint, error) { return nil, nil }
func (TestBind) BatchSize() int { return 0 }
func (TestBind) Close() error { return nil }
func (TestBind) SetMark(uint32) error { return nil }
func (TestBind) Open(uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) {
return nil, 0, nil
}
package internal
import (
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
pointc "github.com/point-c/caddy/module/point-c"
"github.com/point-c/wg"
"github.com/point-c/wgapi"
"github.com/point-c/wgevents"
"go.mrchanchal.com/zaphandler"
"io"
"log/slog"
)
type Wg struct {
Wg io.Closer
Logger *slog.Logger
}
type (
CaddyfileKeyValues map[string]UnmarshalCaddyfileFn
UnmarshalCaddyfileFn func(*caddyfile.Dispenser) error
)
func (w *Wg) UnmarshalCaddyfile(d *caddyfile.Dispenser, name UnmarshalCaddyfileFn, m CaddyfileKeyValues) (err error) {
for d.Next() {
err = name(d)
for nesting := d.Nesting(); d.NextBlock(nesting); {
if err != nil {
return
}
key := d.Val()
if !d.NextArg() {
return d.ArgErr()
}
value, ok := m[key]
if !ok {
return fmt.Errorf("unrecognized option %q", key)
}
err = value(d)
}
}
return
}
func UnmarshalCaddyfileNesting[T any](s *[]*T, fn func(*T) (UnmarshalCaddyfileFn, CaddyfileKeyValues)) UnmarshalCaddyfileFn {
return func(d *caddyfile.Dispenser) (err error) {
var v T
name, m := fn(&v)
err = name(d)
for nesting := d.Nesting(); d.NextBlock(nesting); {
if err != nil {
return
}
key := d.Val()
if !d.NextArg() {
return d.ArgErr()
}
value, ok := m[key]
if !ok {
return fmt.Errorf("unrecognized option %q", key)
}
err = value(d)
}
if err == nil {
*s = append(*s, &v)
}
return
}
}
func (w *Wg) Cleanup() error { return w.Wg.Close() }
func (w *Wg) Provision(ctx caddy.Context) error {
w.Logger = slog.New(zaphandler.New(ctx.Logger()))
return nil
}
func (w *Wg) Start(fn pointc.RegisterFunc, m map[string]*Net, cfg wgapi.Configurable) (err error) {
for k, v := range m {
if err := fn(k, v); err != nil {
return err
}
}
var n *wg.Net
w.Wg, err = wg.New(
wg.OptionConfig(cfg),
wg.OptionLogger(wgevents.Events(func(e wgevents.Event) { e.Slog(w.Logger) })),
wg.OptionNetDevice(&n),
)
if err == nil {
for _, v := range m {
v.Net = n
}
}
return
}
package wg
import (
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
pointc "github.com/point-c/caddy/module/point-c"
"github.com/point-c/caddy/module/wg/internal"
"github.com/point-c/caddy/pkg/configvalues"
"github.com/point-c/wgapi/wgconfig"
)
var (
_ caddy.Module = (*Server)(nil)
_ caddy.Provisioner = (*Server)(nil)
_ caddy.CleanerUpper = (*Server)(nil)
_ pointc.Network = (*Server)(nil)
)
func init() {
caddy.RegisterModule(new(Server))
}
func (*Server) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "point-c.net.wgserver",
New: func() caddy.Module { return new(Server) },
}
}
// Server is a basic wireguard server.
type (
Server struct {
Name configvalues.Hostname `json:"hostname"`
IP configvalues.IP `json:"ip"`
ListenPort configvalues.Port `json:"listen-port"`
Private configvalues.PrivateKey `json:"private"`
Peers []*ServerPeer `json:"peers"`
wg internal.Wg
}
ServerPeer struct {
Name configvalues.Hostname `json:"hostname"`
Public configvalues.PublicKey `json:"public"`
PresharedKey configvalues.PresharedKey `json:"preshared"`
IP configvalues.IP `json:"ip"`
}
)
// UnmarshalCaddyfile unmarshals a config in caddyfile form.
//
// {
// point-c {
// wgserver <name> {
// ip <server ip>
// port <server port to listen on>
// private <server private key>
// peer <name> {
// ip <client ip>
// public <client public key>
// shared <shared key>
// }
// }
// }
func (c *Server) UnmarshalCaddyfile(d *caddyfile.Dispenser) (err error) {
return c.wg.UnmarshalCaddyfile(d, c.Name.UnmarshalCaddyfile, internal.CaddyfileKeyValues{
"ip": c.IP.UnmarshalCaddyfile,
"port": c.ListenPort.UnmarshalCaddyfile,
"private": c.Private.UnmarshalCaddyfile,
"peer": internal.UnmarshalCaddyfileNesting[ServerPeer](&c.Peers, func(s *ServerPeer) (internal.UnmarshalCaddyfileFn, internal.CaddyfileKeyValues) {
return s.Name.UnmarshalCaddyfile, internal.CaddyfileKeyValues{
"ip": s.IP.UnmarshalCaddyfile,
"public": s.Public.UnmarshalCaddyfile,
"shared": s.PresharedKey.UnmarshalCaddyfile,
}
}),
})
}
func (c *Server) Start(fn pointc.RegisterFunc) error {
m := map[string]*internal.Net{c.Name.Value(): {IP: c.IP.Value()}}
cfg := wgconfig.Server{
Private: c.Private.Value(),
ListenPort: c.ListenPort.Value(),
}
for _, peer := range c.Peers {
if _, ok := m[peer.Name.Value()]; ok {
return fmt.Errorf("hostname %q already declared in config", peer.Name.Value())
}
cfg.AddPeer(peer.Public.Value(), peer.PresharedKey.Value(), peer.IP.Value())
m[peer.Name.Value()] = &internal.Net{IP: peer.IP.Value()}
}
return c.wg.Start(fn, m, &cfg)
}
func (c *Server) Cleanup() error { return c.wg.Cleanup() }
func (c *Server) Provision(ctx caddy.Context) error { return c.wg.Provision(ctx) }
// Package caddyreg contains basic helpers to assist with creating Caddy modules.
package caddyreg
import (
"github.com/caddyserver/caddy/v2"
"strings"
)
// R registers the provided caddy module.
func R[T caddy.Module]() { caddy.RegisterModule(*new(T)) }
// Info helps generate the [caddy.ModuleInfo] by automatically filling in the [caddy.ModuleInfo.New] field.
// The id parameter is set to the ID field of the [caddy.ModuleInfo].
func Info[T any, I interface {
*T
caddy.Module
}](id ...string) caddy.ModuleInfo {
return caddy.ModuleInfo{ID: caddy.ModuleID(strings.Join(id, ".")), New: func() caddy.Module { return (any(new(T))).(caddy.Module) }}
}
// Package channel_listener contains a listener that is able to pass arbitrary connections through a channel.
package channel_listener
import (
"net"
"sync/atomic"
)
// Listener is an implementation of a network listener that accepts connections
// from a channel. This allows for a flexible way to handle incoming connections.
type Listener struct {
addr net.Addr // addr represents the network address of the listener.
c <-chan net.Conn // c is the incoming [net.Conn]s.
done chan struct{} // done is used to signal the closing of the listener.
// closeErr safely stores the error that occurs upon closing the listener.
// Only the error from the first call to [Listener.CloseWithErr] will be set.
// [Listener.CloseWithErr] will set the error to [net.ErrClosed] if called with nil.
closeErr atomic.Pointer[error]
}
// New creates a new instance of Listener.
// [net.Conn] from the channel in are passed to [Listener.Accept] calls.
// addr is passed through to [Listener.Addr].
func New(in <-chan net.Conn, addr net.Addr) *Listener {
cl := &Listener{
c: in,
done: make(chan struct{}),
addr: addr,
}
return cl
}
// Accept waits for and returns the next connection from the channel.
// If the listener is closed, it returns the error used to close.
func (d *Listener) Accept() (net.Conn, error) {
for {
select {
case <-d.done:
// If the listener is closed, return the error stored in closeErr.
return nil, *d.closeErr.Load()
case c, ok := <-d.c:
// Retrieve the next connection from the channel.
if !ok {
// If the channel is closed, close listener and return the error
d.Close()
return nil, *d.closeErr.Load()
}
return c, nil
}
}
}
// Close closes the listener and signals any goroutines in [Listener.Accept] to stop waiting.
// It calls [Listener.CloseWithErr] with nil.
func (d *Listener) Close() error { return d.CloseWithErr(nil) }
// CloseWithErr allows closing the listener with a specific error.
// This error will be returned by [Listener.Accept] when the listener is closed.
// Only the error from the first time this is called will be returned.
// If the passed err is nil, the error used to close is [net.ErrClosed].
func (d *Listener) CloseWithErr(err error) error {
if err == nil {
err = net.ErrClosed
}
if d.closeErr.CompareAndSwap(nil, &err) {
close(d.done)
}
return nil
}
// Addr returns the address of the listener
func (d *Listener) Addr() net.Addr { return d.addr }
// Done returns a channel that's closed when the listener is closed.
func (d *Listener) Done() <-chan struct{} { return d.done }
package configvalues
import (
"encoding/json"
"fmt"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
)
type caddyfileUnmarshaler[T any] interface {
*T
caddyfile.Unmarshaler
}
// CaddyfileUnmarshaler returns a function that unmarshals Caddyfile configuration into a specific type.
// It works with types T whose pointer implements the [caddyfile.Unmarshaler] interface.
// The function handles both the initialization of a new configuration object and the resumption
// of a partially-unmarshaled configuration. This is useful if the [caddyfile.App] manages a list of submodules
// and whose [caddyfile.Unmarshaler] will mainly just append to that list.
func CaddyfileUnmarshaler[T any, TP caddyfileUnmarshaler[T]](name string) func(d *caddyfile.Dispenser, resume any) (any, error) {
return func(d *caddyfile.Dispenser, resume any) (any, error) {
var v T
// If there is an existing configuration to resume from, try to unmarshal it.
if resume != nil {
j, ok := resume.(httpcaddyfile.App)
if !ok {
// Type assertion failed
return nil, fmt.Errorf("not a %T", j)
} else if j.Name != name {
// Path mismatch, somehow resuming mismatched configs
return nil, fmt.Errorf("expected app with name %q, got %q", name, j.Name)
}
// Unmarshal the JSON data into the configuration object.
if err := json.Unmarshal(j.Value, &v); err != nil {
return nil, err
}
}
// Unmarshal the Caddyfile data into the configuration object.
// Generic parameters ensure that the pointer of T implements [caddyfile.CaddyfileUnmarshaler].
if err := any(&v).(caddyfile.Unmarshaler).UnmarshalCaddyfile(d); err != nil {
return nil, err
}
// Return the configuration wrapped in a [httpcaddyfile.App].
return httpcaddyfile.App{Name: name, Value: caddyconfig.JSON(&v, nil)}, nil
}
}
package configvalues
import (
"encoding"
"encoding/json"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"strconv"
"sync"
)
type (
// Value is an interface for types that can unmarshal text and return a value.
Value[V any] interface {
encoding.TextUnmarshaler
Value() V
Reset()
}
valueConstraint[V, T any] interface {
*T
Value[V]
}
// CaddyTextUnmarshaler is a generic struct for unmarshaling text into a value.
// It stores the unmarshaled value and the original text representation.
CaddyTextUnmarshaler[V, T any, TP valueConstraint[V, T]] struct {
value T
set bool
original string
}
)
// MarshalText marshals the CaddyTextUnmarshaler back to text.
// It returns the original text representation.
func (c CaddyTextUnmarshaler[V, T, TP]) MarshalText() (text []byte, err error) {
return []byte(c.original), nil
}
var caddyReplacer = sync.OnceValue(caddy.NewReplacer)
// UnmarshalText unmarshals text into the [CaddyTextUnmarshaler]'s value.
// It uses Caddy's replacer for variable expansion in the text before unmarshaling.
// The value and the stored text are reset when this is called, even if unmarshalling fails.
func (c *CaddyTextUnmarshaler[V, T, TP]) UnmarshalText(text []byte) error {
c.original = string(text)
v := any(&c.value).(Value[V])
v.Reset()
text = []byte(caddyReplacer().ReplaceAll(c.original, ""))
err := any(&c.value).(encoding.TextUnmarshaler).UnmarshalText(text)
c.set = err == nil
if !c.set {
v.Reset()
}
return err
}
// MarshalJSON marshals the [CaddyTextUnmarshaler] into JSON.
// It quotes the text if it's not valid JSON.
func (c CaddyTextUnmarshaler[V, T, TP]) MarshalJSON() (text []byte, err error) {
text = []byte(c.original)
if !c.set {
return []byte("null"), nil
} else if !json.Valid(text) {
text = []byte(strconv.Quote(string(text)))
}
return
}
// UnmarshalJSON unmarshals JSON into the [CaddyTextUnmarshaler]'s value.
// It handles JSON strings and unmarshals them as text.
func (c *CaddyTextUnmarshaler[V, T, TP]) UnmarshalJSON(text []byte) error {
if string(text) == "null" {
any(&c.value).(Value[V]).Reset()
c.original = "null"
return nil
} else if s := ""; json.Unmarshal(text, &s) == nil {
text = []byte(s)
}
return c.UnmarshalText(text)
}
// Value returns the underlying value of the [CaddyTextUnmarshaler].
func (c *CaddyTextUnmarshaler[V, T, TP]) Value() V {
return any(&c.value).(Value[V]).Value()
}
// UnmarshalCaddyfile reads the next arg as a string and unmarshalls it
func (c *CaddyTextUnmarshaler[V, T, TP]) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
var s string
for s == "" {
s = d.Val()
if s == "" {
if !d.NextArg() {
return d.ArgErr()
}
}
}
return c.UnmarshalText([]byte(s))
}
package configvalues
import (
"bytes"
"encoding"
"encoding/binary"
"errors"
"github.com/point-c/wgapi"
"golang.org/x/exp/constraints"
"net"
"strconv"
"unsafe"
)
// ValueBool handles unmarshalling bool values.
type ValueBool bool
// UnmarshalText parses the bool with [strconv.ParseBool] internally.
func (b *ValueBool) UnmarshalText(text []byte) error {
bb, err := strconv.ParseBool(string(text))
if err != nil {
return err
}
*b = ValueBool(bb)
return nil
}
// Value returns the underlying bool value of ValueBool.
func (b *ValueBool) Value() bool {
return bool(*b)
}
// Reset sets the value to false.
func (b *ValueBool) Reset() { *b = false }
// ValueString handles unmarshalling string values.
type ValueString string
// UnmarshalText just sets the value to string(b).
func (s *ValueString) UnmarshalText(b []byte) error {
*s = ValueString(b)
return nil
}
// Value returns the underlying string value of ValueString.
func (s *ValueString) Value() string {
return string(*s)
}
// Reset sets the value to the empty string
func (s *ValueString) Reset() { *s = "" }
// ValueUnsigned is a generic type for unmarshalling an unsigned number.
// N must be an unsigned type (e.g., uint, uint32).
type ValueUnsigned[N constraints.Unsigned] struct{ V N }
// UnmarshalText parses the uint with [strconv.ParseUint] internally.
func (n *ValueUnsigned[N]) UnmarshalText(b []byte) error {
var size int
switch any(n.V).(type) {
// uintptr and uint report -8 with binary.Size
case uintptr, uint:
size = int(unsafe.Sizeof(n.V))
default:
size = binary.Size(n.V)
}
i, err := strconv.ParseUint(string(b), 10, size*8)
if err != nil {
return err
}
n.V = N(i)
return nil
}
// Value returns the underlying unsigned number of ValueUnsigned.
func (n *ValueUnsigned[N]) Value() N {
return n.V
}
// Reset sets this value to 0.
func (n *ValueUnsigned[N]) Reset() { n.V = 0 }
// ValueUDPAddr handles unmarshalling a [net.UDPAddr].
type ValueUDPAddr net.UDPAddr
// UnmarshalText implements the unmarshaling of text data into a UDP address.
// It resolves the text using [net.ResolveUDPAddr].
func (addr *ValueUDPAddr) UnmarshalText(text []byte) error {
a, err := net.ResolveUDPAddr("udp", string(text))
if err != nil {
return err
}
*addr = (ValueUDPAddr)(*a)
return nil
}
// Value returns the underlying net.UDPAddr of ValueUDPAddr.
func (addr *ValueUDPAddr) Value() *net.UDPAddr {
return (*net.UDPAddr)(addr)
}
// Reset sets this value to an empty UDPAddr.
func (addr *ValueUDPAddr) Reset() { *addr = ValueUDPAddr{} }
// ValueIP handles unmarshalling [net.IP].
type ValueIP net.IP
// UnmarshalText implements the unmarshaling of text data into an IP address.
// It delegates to the [encoding.TextUnmarshaler] implementation of [net.IP].
func (ip *ValueIP) UnmarshalText(text []byte) error {
return ((*net.IP)(ip)).UnmarshalText(text)
}
// Value returns the underlying net.IP of ValueIP.
func (ip *ValueIP) Value() net.IP {
return net.IP(*ip)
}
// Reset sets this value to nil.
func (ip *ValueIP) Reset() { *ip = nil }
// ValueResolvedIP handles resolving and unmarshalling [net.IP].
type ValueResolvedIP net.IP
// UnmarshalText implements the resolving of a hostname into an IP address.
// It delegates to the [encoding.TextUnmarshaler] implementation of [net.IP].
// If the host has multiple IP addresses, the first one is used.
func (ip *ValueResolvedIP) UnmarshalText(text []byte) error {
ips, err := net.LookupIP(string(text))
if err != nil || len(ips) == 0 {
return errors.Join(err, &net.DNSError{Err: "no ip for host found", Name: string(text), IsNotFound: true})
}
*ip = ValueResolvedIP(ips[0])
return nil
}
// Value returns the underlying net.IP that was resolved.
func (ip *ValueResolvedIP) Value() net.IP { return net.IP(*ip) }
// Reset sets this value to nil.
func (ip *ValueResolvedIP) Reset() { *ip = nil }
// ValuePair represents a structured combination of `<value>:<value>` pairs.
type ValuePair[V any, T any, TP valueConstraint[V, T]] struct {
left, right CaddyTextUnmarshaler[V, T, TP]
}
// UnmarshalText unmarshals a `<value>:<value>` pair.
func (pp *ValuePair[V, T, TP]) UnmarshalText(b []byte) error {
left, right, ok := bytes.Cut(b, []byte{':'})
if !ok {
return errors.New("not a pair value")
}
return errors.Join(pp.left.UnmarshalText(left), pp.right.UnmarshalText(right))
}
// Value returns the pair's base values.
func (pp *ValuePair[V, T, TP]) Value() *PairValue[V] {
return &PairValue[V]{
Left: pp.left.Value(),
Right: pp.right.Value(),
}
}
// Reset resets the pair values to their zero values.
func (pp *ValuePair[V, T, TP]) Reset() {
for _, e := range []*CaddyTextUnmarshaler[V, T, TP]{&pp.left, &pp.right} {
*e = CaddyTextUnmarshaler[V, T, TP]{}
}
}
type valueKey[K wgapi.PrivateKey | wgapi.PublicKey | wgapi.PresharedKey] struct{ K K }
func (wgk *valueKey[K]) UnmarshalText(text []byte) error {
return any(&wgk.K).(encoding.TextUnmarshaler).UnmarshalText(text)
}
func (wgk *valueKey[K]) Value() K {
return wgk.K
}
func (wgk *valueKey[K]) Reset() { *wgk = valueKey[K]{} }
// Package lifecycler helps Caddy modules manage the life cycle of submodules.
package lifecycler
import (
"encoding/json"
"errors"
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"reflect"
"strings"
)
var (
_ json.Unmarshaler = (*LifeCycler[any])(nil)
_ json.Marshaler = (*LifeCycler[any])(nil)
_ caddy.CleanerUpper = (*LifeCycler[any])(nil)
)
type (
// LifeCycler assists with loading submodules and maintaining their lifecycle.
LifeCycler[T any] struct {
V T // Value passed to submodules
Modules []LifeCyclable[T] // Unstarted submodules
Started []LifeCyclable[T] // Started submodules
}
// ProvisionInfo is used to help with caddy provisioning.
ProvisionInfo struct {
StructPointer any // Pointer to the struct to load config into
FieldName string // Name of the field that config will be loaded into
Raw *[]json.RawMessage // Pointer to the array that config will be loaded into
}
// CaddyfileInfo is used to help unmarshal a caddyfile.
CaddyfileInfo struct {
ModuleID []string // module.id.path. with dot at end if required
Raw *[]json.RawMessage // Where the raw config of submodules will be appended to
SubModuleSpecifier string // The specifier used by caddy to determine the submodule.
ParseVerbLine caddyfile.Unmarshaler // An optional [caddyfile.Unmarshaler]. If specified will be used to decode the second item on the first line of the config.
}
// LifeCyclable is a submodule that can be started.
LifeCyclable[T any] interface {
caddy.Module
Start(T) error
}
)
// UnmarshalJSON returns an error preventing [LifeCycler] from being used in configuration.
func (*LifeCycler[T]) UnmarshalJSON([]byte) error { return errors.New("do not unmarshal") }
// MarshalJSON returns an error preventing [LifeCycler] from being used in configuration.
func (*LifeCycler[T]) MarshalJSON() ([]byte, error) { return nil, errors.New("do not marshal") }
// Provision loads submodules from the config. `info` must not be nil and the `Raw` field must be set.
func (l *LifeCycler[T]) Provision(ctx caddy.Context, info *ProvisionInfo) error {
if info == nil || info.Raw == nil {
return errors.New("no info")
}
if *info.Raw != nil {
// Load submodules
val, err := ctx.LoadModule(info.StructPointer, info.FieldName)
if err != nil {
return fmt.Errorf("failed to provision field %q: %w", info.FieldName, err)
}
*info.Raw = nil
raw := val.([]any)
l.Modules = make([]LifeCyclable[T], len(raw))
l.Started = make([]LifeCyclable[T], 0, len(raw))
for i, v := range raw {
// Assert submodule to correct type
vv, ok := v.(LifeCyclable[T])
if !ok {
return fmt.Errorf("expected type LifeCyclable[%s], got type %T", func() (s string) {
defer func() {
// Swallow reflect panics
recover()
if s == "" {
s = "<unknown type>"
}
}()
return reflect.TypeOf(new(T)).Elem().Name()
}(), v)
}
l.Modules[i] = vv
}
}
return nil
}
// SetValue sets the internal value that will be passed to submodules.
func (l *LifeCycler[T]) SetValue(v T) { l.V = v }
// Start starts submodules and passes the stored value into them.
func (l *LifeCycler[T]) Start() error {
// Loop through each module. Use standard iterator to capture module number.
for i := 0; len(l.Modules) > 0; i++ {
// Start the module in a separate function to capture panics.
if err := func(op LifeCyclable[T]) (err error) {
defer func() {
// Capture revcover
if r := recover(); r != nil {
err = errors.Join(err, fmt.Errorf("recovered panic starting %[2]s[%[1]d]: %[3]v", i, op.CaddyModule().ID, r))
}
// Add information to errors
if err != nil {
err = errors.Join(err, fmt.Errorf("failed to start %[2]s[%[1]d]", i, op.CaddyModule().ID))
}
}()
// Try to start
return op.Start(l.V)
}(l.Modules[0]); err != nil {
return err
}
// Module started, keep track of it.
l.Started = append(l.Started, l.Modules[0])
l.Modules = l.Modules[1:]
}
// All modules are started
l.Modules = nil
// Don't need this value anymore
l.V = *new(T)
return nil
}
// Cleanup clears the lifecycler struct to help the GC.
// Any caddy submodules will be cleaned up by caddy.
func (l *LifeCycler[T]) Cleanup() (err error) {
*l = LifeCycler[T]{}
return
}
// UnmarshalCaddyfile unmarshals a module from a caddyfile.
// `info` must not be nil and must have the `raw` and `SubModuleSpecifier` fields set.
//
// {
// <verb> [rest of line] {
// <submodule name> <submodule config>
// }
// }
func (l *LifeCycler[T]) UnmarshalCaddyfile(d *caddyfile.Dispenser, info *CaddyfileInfo) error {
if info == nil || info.Raw == nil || info.SubModuleSpecifier == "" {
return errors.New("not enough information to unmarshal caddyfile")
}
for d.Next() {
if info.ParseVerbLine != nil {
if !d.NextArg() {
return d.ArgErr()
}
if err := info.ParseVerbLine.UnmarshalCaddyfile(d); err != nil {
return err
}
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
modName := d.Val()
v, err := caddyfile.UnmarshalModule(d, strings.Join(info.ModuleID, ".")+"."+modName)
if err != nil {
return err
}
*info.Raw = append(*info.Raw, caddyconfig.JSONModuleObject(v, info.SubModuleSpecifier, modName, nil))
}
}
return nil
}
// Package test_caddy provides mock types and utilities for testing caddy modules.
package test_caddy
import (
"context"
"encoding/json"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/google/uuid"
"github.com/point-c/caddy/pkg/lifecycler"
"github.com/stretchr/testify/require"
"reflect"
"testing"
"unsafe"
)
// NewCaddyContext returns an instantiated caddy context.
// This differs from [caddy.NewContext] in that the context's config is set and the `apps` map is initialized.
// This is required to load other apps in provision for some tests.
//
// UNSTABLE: Heavy use of reflect and unsafe pointers to access unexported struct fields.
func NewCaddyContext(t testing.TB, base context.Context, cfg caddy.Config) (caddy.Context, context.CancelFunc) {
ctx := caddy.Context{Context: base}
// Set unexposed 'cfg' field
f, ok := reflect.TypeOf(ctx).FieldByName("cfg")
require.True(t, ok)
cfgPtr := uintptr(reflect.ValueOf(&ctx).UnsafePointer()) + f.Offset
*(**caddy.Config)(unsafe.Pointer(cfgPtr)) = &cfg
// Initialize 'apps' map
f, ok = reflect.TypeOf(cfg).FieldByName("apps")
require.True(t, ok)
appsPtr := uintptr(reflect.ValueOf(&cfg).UnsafePointer()) + f.Offset
*(*map[string]caddy.App)(unsafe.Pointer(appsPtr)) = map[string]caddy.App{}
// Return new context
return caddy.NewContext(ctx)
}
var (
_ caddy.Module = (*TestModule[any])(nil)
_ caddyfile.Unmarshaler = (*TestModule[any])(nil)
_ caddy.Provisioner = (*TestModule[any])(nil)
_ caddy.Validator = (*TestModule[any])(nil)
_ lifecycler.LifeCyclable[any] = (*TestModule[any])(nil)
_ caddy.CleanerUpper = (*TestModule[any])(nil)
_ json.Unmarshaler = (*TestModule[any])(nil)
_ json.Marshaler = (*TestModule[any])(nil)
)
// TestModule is a mock base caddy module. It implements the functions in the caddy lifecycle.
// It also implements [lifecycler.LifeCyclable] and the json marshalling methods.
type TestModule[T any] struct {
t testing.TB
ID string `json:"-"`
Module caddy.ModuleID `json:"-"`
New func() caddy.Module
UnmarshalCaddyfileFn func(*caddyfile.Dispenser) error `json:"-"`
ProvisionerFn func(caddy.Context) error `json:"-"`
ValidateFn func() error `json:"-"`
CleanupFn func() error `json:"-"`
StartFn func(T) error `json:"-"`
MarshalJSONFn func() ([]byte, error) `json:"-"`
UnmarshalJSONFn func(b []byte) error `json:"-"`
}
// NewTestModule creates and initializes a new instance of [TestModule].
func NewTestModule[T any, Parent caddy.Module](t testing.TB, p *Parent, fn func(Parent) *TestModule[T], module string) {
id := "test" + uuid.NewString()
*fn(*p) = TestModule[T]{
t: t,
ID: id,
Module: caddy.ModuleID(module + id),
New: func() caddy.Module { return *p },
}
}
// MarshalJSON attempts to call MarshalJSONFn. If MarshalJSONFn is not set, an empty struct is marshalled and returned instead.
func (t *TestModule[T]) MarshalJSON() ([]byte, error) {
if t.MarshalJSONFn != nil {
return t.MarshalJSONFn()
}
return json.Marshal(struct{}{})
}
// UnmarshalJSON attempts to call UnmarshalJSONFn. If UnmarshalJSONFn is not set, nil is returned.
func (t *TestModule[T]) UnmarshalJSON(b []byte) error {
if t.UnmarshalJSONFn != nil {
return t.UnmarshalJSONFn(b)
}
return nil
}
// Register registers this module with caddy.
func (t *TestModule[T]) Register() { caddy.RegisterModule(t) }
// CaddyModule returns the [caddy.ModuleInfo] for this module.
// The New method returns this instance everytime.
func (t *TestModule[T]) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: t.Module,
New: func() caddy.Module { return t },
}
}
// Provision attempts to call ProvisionerFn. If ProvisionerFn is not set, nil is returned.
func (t *TestModule[T]) Provision(ctx caddy.Context) error {
if t.ProvisionerFn != nil {
return t.ProvisionerFn(ctx)
}
return nil
}
// Validate attempts to call ValidateFn. If ValidateFn is not set, nil is returned.
func (t *TestModule[T]) Validate() error {
if t.ValidateFn != nil {
return t.ValidateFn()
}
return nil
}
// Start attempts to call StartFn. If StartFn is not set, nil is returned.
func (t *TestModule[T]) Start(v T) error {
if t.StartFn != nil {
return t.StartFn(v)
}
return nil
}
// Cleanup attempts to call CleanupFn. If CleanupFn is not set, nil is returned.
func (t *TestModule[T]) Cleanup() error {
if t.CleanupFn != nil {
return t.CleanupFn()
}
return nil
}
// UnmarshalCaddyfile attempts to call UnmarshalCaddyfileFn. If UnmarshalCaddyfileFn is not set, nil is returned.
func (t *TestModule[T]) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if t.UnmarshalCaddyfileFn != nil {
return t.UnmarshalCaddyfileFn(d)
}
return nil
}
package test_caddy
import (
"errors"
"net"
"testing"
"time"
)
var _ net.Conn = (*TestConn)(nil)
// TestConn is a mock [net.Conn].
type TestConn struct {
t testing.TB
ReadFn func([]byte) (int, error)
WriteFn func([]byte) (int, error)
CloseFn func() error
LocalAddrFn func() net.Addr
RemoteAddrFn func() net.Addr
SetDeadlineFn func(time.Time) error
SetReadDeadlineFn func(time.Time) error
SetWriteDeadlineFn func(time.Time) error
}
// NewTestConn creates and initializes a new instance of [TestConn].
func NewTestConn(t testing.TB) *TestConn {
return &TestConn{t: t}
}
// Read attempts to call ReadFn. If ReadFn is not set, an error is returned.
func (tc *TestConn) Read(b []byte) (int, error) {
if tc.ReadFn != nil {
return tc.ReadFn(b)
}
return 0, errors.New("read not implemented")
}
// Write attempts to call WriteFn. If WriteFn is not set, an error is returned.
func (tc *TestConn) Write(b []byte) (int, error) {
if tc.WriteFn != nil {
return tc.WriteFn(b)
}
return 0, errors.New("write not implemented")
}
// Close attempts to call CloseFn. If CloseFn is not set, nil is returned.
func (tc *TestConn) Close() error {
if tc.CloseFn != nil {
return tc.CloseFn()
}
return nil
}
// LocalAddr attempts to call LocalAddrFn. If LocalAddrFn is not set, nil is returned.
func (tc *TestConn) LocalAddr() net.Addr {
if tc.LocalAddrFn != nil {
return tc.LocalAddrFn()
}
return &net.TCPAddr{}
}
// RemoteAddr attempts to call RemoteAddrFn. If RemoteAddrFn is not set, a pointer to a [net.TCPAddr] is returned..
func (tc *TestConn) RemoteAddr() net.Addr {
if tc.RemoteAddrFn != nil {
return tc.RemoteAddrFn()
}
return &net.TCPAddr{}
}
// SetDeadline attempts to call SetDeadlineFn. If SetDeadlineFn is not set, nil is returned.
func (tc *TestConn) SetDeadline(t time.Time) error {
if tc.SetDeadlineFn != nil {
return tc.SetDeadlineFn(t)
}
return nil
}
// SetReadDeadline attempts to call SetReadDeadlineFn. If SetReadDeadlineFn is not set, nil is returned.
func (tc *TestConn) SetReadDeadline(t time.Time) error {
if tc.SetReadDeadlineFn != nil {
return tc.SetReadDeadlineFn(t)
}
return nil
}
// SetWriteDeadline attempts to call SetWriteDeadlineFn. If SetWriteDeadlineFn is not set, nil is returned.
func (tc *TestConn) SetWriteDeadline(t time.Time) error {
if tc.SetWriteDeadlineFn != nil {
return tc.SetWriteDeadlineFn(t)
}
return nil
}
package test_caddy
import (
"context"
"errors"
pointc "github.com/point-c/caddy/module/point-c"
"net"
"testing"
)
var _ pointc.Dialer = (*TestDialer)(nil)
// TestDialer is a mock [pointc.Dialer].
type TestDialer struct {
t testing.TB
DialFn func(context.Context, *net.TCPAddr) (net.Conn, error)
DialPacketFn func(*net.UDPAddr) (net.PacketConn, error)
}
// NewTestDialer creates and initializes a new instance of [TestDialer].
func NewTestDialer(t testing.TB) *TestDialer {
return &TestDialer{t: t}
}
// Dial attempts to call DialFn. If DialFn is not set, an error is returned.
func (td *TestDialer) Dial(ctx context.Context, addr *net.TCPAddr) (net.Conn, error) {
if td.DialFn != nil {
return td.DialFn(ctx, addr)
}
return nil, errors.New("dial not implemented")
}
// DialPacket attempts to call DialPacketFn. If DialPacketFn is not set, an error is returned.
func (td *TestDialer) DialPacket(addr *net.UDPAddr) (net.PacketConn, error) {
if td.DialPacketFn != nil {
return td.DialPacketFn(addr)
}
return nil, errors.New("dialPacket not implemented")
}
package test_caddy
import (
"errors"
"net"
"testing"
)
var _ net.Listener = (*TestListener)(nil)
// TestListener is a mock [net.Listener].
type TestListener struct {
t testing.TB
AcceptFn func() (net.Conn, error) `json:"-"`
CloseFn func() error `json:"-"`
AddrFn func() net.Addr `json:"-"`
}
// NewTestListener creates and initializes a new instance of [TestListener].
func NewTestListener(t testing.TB) *TestListener {
return &TestListener{t: t}
}
// Accept attempts to call AcceptFn. If AcceptFn is not set, an error is returned.
func (tl *TestListener) Accept() (net.Conn, error) {
if tl.AcceptFn != nil {
return tl.AcceptFn()
}
return nil, errors.New("accept not implemented")
}
// Close attempts to call CloseFn. If CloseFn is not set, nil is returned.
func (tl *TestListener) Close() error {
if tl.CloseFn != nil {
return tl.CloseFn()
}
return nil
}
// Addr attempts to call AddrFn. If AddrFn is not set, a pointer to a [net.TCPAddr] is returned.
func (tl *TestListener) Addr() net.Addr {
if tl.AddrFn != nil {
return tl.AddrFn()
}
return &net.TCPAddr{}
}
// TestListenerModule is a mock [net.Listener] that can also be used as a Caddy module.
type TestListenerModule[T any] struct {
TestListener
TestModule[T]
}
// NewTestListenerModule creates and initializes a new instance of [TestListenerModule].
func NewTestListenerModule[T any](t testing.TB) (v *TestListenerModule[T]) {
defer NewTestModule[T, *TestListenerModule[T]](t, &v, func(v *TestListenerModule[T]) *TestModule[T] { return &v.TestModule }, "caddy.listeners.merge.")
return &TestListenerModule[T]{TestListener: *NewTestListener(t)}
}
package test_caddy
import (
pointc "github.com/point-c/caddy/module/point-c"
"testing"
)
var _ pointc.NetLookup = (*TestNetLookup)(nil)
// TestNetLookup implements [pointc.NetLookup].
type TestNetLookup struct {
t testing.TB
LookupFn func(string) (pointc.Net, bool)
}
// NewTestNetLookup creates and initializes a new instance of [TestNetLookup].
func NewTestNetLookup(t testing.TB) *TestNetLookup {
return &TestNetLookup{t: t}
}
// Lookup attempts to call LookupFn. If LookupFn is not set (nil, false) is returned.
func (tnl *TestNetLookup) Lookup(name string) (pointc.Net, bool) {
if tnl.LookupFn != nil {
return tnl.LookupFn(name)
}
return nil, false
}
package test_caddy
import (
"errors"
pointc "github.com/point-c/caddy/module/point-c"
"net"
"testing"
)
var _ pointc.Net = (*TestNet)(nil)
// TestNet is a mock point-c network net module.
type TestNet struct {
t testing.TB
ListenFn func(*net.TCPAddr) (net.Listener, error)
ListenPacketFn func(*net.UDPAddr) (net.PacketConn, error)
DialerFn func(net.IP, uint16) pointc.Dialer
LocalAddrFn func() net.IP
}
// NewTestNet creates and initializes a new instance of [TestNet].
func NewTestNet(t testing.TB) *TestNet {
return &TestNet{t: t}
}
// Listen attempts to call ListenFn. If ListenFn is not set, an error is returned.
func (tn *TestNet) Listen(addr *net.TCPAddr) (net.Listener, error) {
if tn.ListenFn != nil {
return tn.ListenFn(addr)
}
return nil, errors.New("listen not implemented")
}
// ListenPacket attempts to call ListenPacketFn. If ListenPacketFn is not set, an error is returned.
func (tn *TestNet) ListenPacket(addr *net.UDPAddr) (net.PacketConn, error) {
if tn.ListenPacketFn != nil {
return tn.ListenPacketFn(addr)
}
return nil, errors.New("ListenPacket not implemented")
}
// Dialer attempts to call DialerFn. If DialerFn is not set, [NewTestDialer] is used to create a value.
func (tn *TestNet) Dialer(laddr net.IP, port uint16) pointc.Dialer {
if tn.DialerFn != nil {
return tn.DialerFn(laddr, port)
}
return NewTestDialer(tn.t)
}
// LocalAddr attempts to call LocalAddrFn. If LocalAddrFn is not set, an error is returned.
func (tn *TestNet) LocalAddr() net.IP {
if tn.LocalAddrFn != nil {
return tn.LocalAddrFn()
}
return net.IPv4zero
}
package test_caddy
import (
pointc "github.com/point-c/caddy/module/point-c"
"testing"
)
var _ pointc.NetOp = (*TestNetOp)(nil)
// TestNetOp is a mock point-c network operation.
type TestNetOp struct{ TestModule[pointc.NetLookup] }
// NewTestNetOp creates and initializes a new instance of [TestNetOp].
func NewTestNetOp(t testing.TB) (v *TestNetOp) {
defer NewTestModule[pointc.NetLookup, *TestNetOp](t, &v, func(v *TestNetOp) *TestModule[pointc.NetLookup] { return &v.TestModule }, "point-c.op.")
return &TestNetOp{}
}
package test_caddy
import (
pointc "github.com/point-c/caddy/module/point-c"
"testing"
)
var _ pointc.Network = (*TestNetwork)(nil)
// TestNetwork is a mock point-c network.
type TestNetwork struct {
TestModule[pointc.RegisterFunc]
}
// NewTestNetwork creates and initializes a new instance of [TestNetwork].
func NewTestNetwork(t testing.TB) (v *TestNetwork) {
defer NewTestModule[pointc.RegisterFunc, *TestNetwork](t, &v, func(v *TestNetwork) *TestModule[pointc.RegisterFunc] { return &v.TestModule }, "point-c.net.")
return &TestNetwork{}
}