Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
disintegrator opened this issue May 11, 2020 · 3 comments · Fixed by #3397
Closed

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

disintegrator opened this issue May 11, 2020 · 3 comments · Fixed by #3397
Labels
feature ⚙️ New feature or request good first issue 🐤 Good for newcomers question ❔ Help is being requested

Comments

@disintegrator
Copy link
Contributor

disintegrator commented May 11, 2020

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???
}
@mholt mholt added feature ⚙️ New feature or request good first issue 🐤 Good for newcomers labels May 11, 2020
@mholt
Copy link
Member

mholt commented May 11, 2020

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!

@mholt mholt added the question ❔ Help is being requested label May 11, 2020
@francislavoie
Copy link
Member

francislavoie commented May 11, 2020

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

@disintegrator
Copy link
Contributor Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature ⚙️ New feature or request good first issue 🐤 Good for newcomers question ❔ Help is being requested
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants