Caddy: Question: How do I write a module and directive to wrap net.Listener

Created on 11 May 2020  路  3Comments  路  Source: caddyserver/caddy

_Thank you so much for this project. I started playing with it over the weekend and decided to build custom modules and directives to simulate failures. It's made me excited to write code in my personal time all over again!_

Background

I want to build a suite of modules that plug in to Caddy and allow it to inject failures for the purpose of testing failure recovery. For example, I would deploy a Caddy reverse proxy in front of some API and add directives to delay the response or rate limit agressively.

Question

The current problem I'm facing is that I've written a module that implements caddy.ListenerWrapper and injects a long pause before accepting connections. I want to have it controlled by a directive like so miasma_tarpit 10s. I can't seem to understand how to use httpcaddyfile.RegisterDirective to register the directive. In particular, I'm not understanding what sort of []ConfigValue I should be returning. I'm not sure what value for Class I should pass. Can you help understand what I'm doing wrong?

Code

The module:

package tarpit

import (
    "context"
    "fmt"
    "math/rand"
    "net"
    "time"

    "github.com/caddyserver/caddy/v2"
    "go.uber.org/zap"
)

func init() {
    caddy.RegisterModule(Tarpit{})
    httpcaddyfile.RegisterDirective("miasma_tarpit", parseCaddyfile)
}


type tarpitKey int

// Tarpit is a Caddy module that alters response times
type Tarpit struct {
    rootContext context.Context
    random      *rand.Rand
    logger      *zap.Logger

    // TODO
    Delay time.Duration `json:"delay,omitempty"`
}

var (
    _ caddy.Module          = (*Tarpit)(nil)
    _ caddy.Provisioner     = (*Tarpit)(nil)
    _ caddy.Validator       = (*Tarpit)(nil)
    _ caddy.ListenerWrapper = (*Tarpit)(nil)
)

// CaddyModule returns the Caddy module information.
func (Tarpit) CaddyModule() caddy.ModuleInfo {
    return caddy.ModuleInfo{
        ID:  "caddy.listeners.miasma_tarpit",
        New: func() caddy.Module { return new(Tarpit) },
    }
}

// Provision initializes the internal state of the Tarpit module
func (t *Tarpit) Provision(ctx caddy.Context) error {
    fmt.Println("PROVISION")
    t.rootContext = context.WithValue(ctx, tarpitKey(0), nil)
    t.logger = ctx.Logger(t)

    return nil
}

// Validate ensures the configuration for the Tarpit module is valid
func (t *Tarpit) Validate() error {
    fmt.Println("VALIDATE")
    if t.logger == nil {
        return fmt.Errorf("logger cannot be nil")
    }

    if t.Delay <= 0 {
        return fmt.Errorf("tarpit delay must be great than zero: %v", t.Delay)
    }

    return nil
}

func (t *Tarpit) WrapListener(listener net.Listener) net.Listener {
    return &tarpitListener{
        rootContext: t.rootContext,
        underlying:  listener,
        delay:       t.Delay,
    }
}

type tarpitListener struct {
    rootContext context.Context
    underlying  net.Listener
    delay       time.Duration
}

func (l *tarpitListener) Accept() (net.Conn, error) {
    select {
    case <-l.rootContext.Done():
        return nil, context.Canceled
    case <-time.After(l.delay):
        return l.underlying.Accept()
    }
}

func (l *tarpitListener) Close() error {
    return l.underlying.Close()
}

func (l *tarpitListener) Addr() net.Addr {
    return l.underlying.Addr()
}

func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
    var tarpit Tarpit

    if !h.Next() {
        return nil, h.ArgErr()
    }

    _, err := h.ExtractMatcherSet()
    if err != nil {
        return nil, err
    }

    if !h.Next() {
        return nil, h.ArgErr()
    }

    args := h.RemainingArgs()

    fmt.Println(args)

    if len(args) != 1 {
        return nil, h.ArgErr()
    }

    delay, err := time.ParseDuration(args[0])
    if err != nil {
        return nil, err
    }

    tarpit.Delay = delay

    // what do I return???
}
feature request good first issue question

Most helpful comment

@mholt @francislavoie, thanks for the tips! Makes sense to me. I'm having a go at a PR right now!

All 3 comments

Ah yes, this is not very well-documented yet. I'm actually planning on expanding the "Extending Caddy" part of the docs as soon as I get a chance!

Basically, the config values you return there each belong to some class. They get dumped into a pile, and then later when the Caddyfile adapter is assembling the JSON config, it picks values out from a relevant class at each part of the config.

Currently, I don't think the Caddyfile adapter has a class for listener wrappers, so we'd need to add that. Not a big deal, just needs a pull request :) The class name could be listener_wrapper or something like that. Then the Caddyfile adapter would have to—when it is building the HTTP server—just look in the pile for config values of class listener_wrapper and JSON-encode them.

Does that make sense? Sounds like a cool project!

Specifically, one of the places in the code that would need to be modified is here:

https://github.com/caddyserver/caddy/blob/master/caddyconfig/httpcaddyfile/httptype.go#L328-L549

Look for the parts that use sblock.pile, you'd probably need to add a sblock.pile["listener_wrapper"] pile

@mholt @francislavoie, thanks for the tips! Makes sense to me. I'm having a go at a PR right now!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

wayneashleyberry picture wayneashleyberry  路  3Comments

aeroxy picture aeroxy  路  3Comments

lorddaedra picture lorddaedra  路  3Comments

treviser picture treviser  路  3Comments

crvv picture crvv  路  3Comments