headscale/app.go

697 lines
18 KiB
Go
Raw Normal View History

2020-06-21 10:32:08 +00:00
package headscale
import (
"context"
"crypto/tls"
"errors"
2020-06-21 10:32:08 +00:00
"fmt"
"io"
"net"
"net/http"
2021-10-22 16:55:14 +00:00
"net/url"
2021-02-21 22:54:15 +00:00
"os"
2021-11-02 21:46:15 +00:00
"os/signal"
"sort"
"strings"
"sync"
2021-11-02 21:46:15 +00:00
"syscall"
"time"
2020-06-21 10:32:08 +00:00
2021-10-18 19:27:52 +00:00
"github.com/coreos/go-oidc/v3/oidc"
2020-06-21 10:32:08 +00:00
"github.com/gin-gonic/gin"
2021-11-13 08:39:04 +00:00
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
2021-11-13 08:39:04 +00:00
"github.com/patrickmn/go-cache"
zerolog "github.com/philip-bui/grpc-zerolog"
zl "github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/soheilhy/cmux"
ginprometheus "github.com/zsais/go-gin-prometheus"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
2021-11-13 08:39:04 +00:00
"golang.org/x/oauth2"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
2021-07-04 19:40:46 +00:00
"gorm.io/gorm"
"inet.af/netaddr"
2021-02-20 22:57:06 +00:00
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
2020-06-21 10:32:08 +00:00
)
const (
AuthPrefix = "Bearer "
Postgres = "postgresql"
Sqlite = "sqlite3"
updateInterval = 5000
HTTPReadTimeout = 30 * time.Second
2021-11-15 19:18:14 +00:00
requestedExpiryCacheExpiration = time.Minute * 5
requestedExpiryCacheCleanupInterval = time.Minute * 10
2021-11-15 19:18:14 +00:00
errUnsupportedDatabase = Error("unsupported DB")
errUnsupportedLetsEncryptChallengeType = Error(
"unknown value for Lets Encrypt challenge type",
)
)
// Config contains the initial Headscale configuration.
2020-06-21 10:32:08 +00:00
type Config struct {
ServerURL string
Addr string
EphemeralNodeInactivityTimeout time.Duration
IPPrefix netaddr.IPPrefix
BaseDomain string
2020-06-21 10:32:08 +00:00
2021-10-22 16:55:14 +00:00
DERP DERPConfig
DBtype string
DBpath string
2020-06-21 10:32:08 +00:00
DBhost string
DBport int
DBname string
DBuser string
DBpass string
TLSLetsEncryptListen string
TLSLetsEncryptHostname string
TLSLetsEncryptCacheDir string
TLSLetsEncryptChallengeType string
TLSCertPath string
TLSKeyPath string
2021-08-24 06:09:47 +00:00
ACMEURL string
ACMEEmail string
2021-08-24 06:09:47 +00:00
DNSConfig *tailcfg.DNSConfig
UnixSocket string
2021-10-31 09:40:43 +00:00
2021-10-18 19:27:52 +00:00
OIDC OIDCConfig
2021-10-08 09:43:52 +00:00
CLI CLIConfig
2020-06-21 10:32:08 +00:00
}
2021-10-18 19:27:52 +00:00
type OIDCConfig struct {
Issuer string
ClientID string
ClientSecret string
MatchMap map[string]string
2020-06-21 10:32:08 +00:00
}
2021-10-22 16:55:14 +00:00
type DERPConfig struct {
URLs []url.URL
Paths []string
AutoUpdate bool
UpdateFrequency time.Duration
}
type CLIConfig struct {
Address string
APIKey string
Insecure bool
Timeout time.Duration
}
// Headscale represents the base app of the service.
2020-06-21 10:32:08 +00:00
type Headscale struct {
cfg Config
2021-07-04 19:40:46 +00:00
db *gorm.DB
2020-06-21 10:32:08 +00:00
dbString string
dbType string
dbDebug bool
publicKey *key.MachinePublic
privateKey *key.MachinePrivate
2021-10-22 16:55:14 +00:00
DERPMap *tailcfg.DERPMap
2021-07-03 15:31:32 +00:00
aclPolicy *ACLPolicy
aclRules []tailcfg.FilterRule
2021-07-03 15:31:32 +00:00
2021-08-19 17:19:26 +00:00
lastStateChange sync.Map
2021-10-08 09:43:52 +00:00
oidcProvider *oidc.Provider
oauth2Config *oauth2.Config
oidcStateCache *cache.Cache
requestedExpiryCache *cache.Cache
2020-06-21 10:32:08 +00:00
}
// NewHeadscale returns the Headscale app.
2020-06-21 10:32:08 +00:00
func NewHeadscale(cfg Config) (*Headscale, error) {
privKey := key.NewMachine()
2020-06-21 10:32:08 +00:00
pubKey := privKey.Public()
var dbString string
switch cfg.DBtype {
case Postgres:
2021-11-13 08:36:45 +00:00
dbString = fmt.Sprintf(
"host=%s port=%d dbname=%s user=%s password=%s sslmode=disable",
cfg.DBhost,
cfg.DBport,
cfg.DBname,
cfg.DBuser,
cfg.DBpass,
)
case Sqlite:
dbString = cfg.DBpath
default:
2021-11-15 19:18:14 +00:00
return nil, errUnsupportedDatabase
}
requestedExpiryCache := cache.New(
requestedExpiryCacheExpiration,
requestedExpiryCacheCleanupInterval,
)
app := Headscale{
cfg: cfg,
dbType: cfg.DBtype,
dbString: dbString,
privateKey: &privKey,
publicKey: &pubKey,
aclRules: tailcfg.FilterAllowAll, // default allowall
requestedExpiryCache: requestedExpiryCache,
2020-06-21 10:32:08 +00:00
}
2021-07-04 11:24:05 +00:00
err := app.initDB()
2020-06-21 10:32:08 +00:00
if err != nil {
return nil, err
}
2021-07-04 19:40:46 +00:00
2021-10-18 19:27:52 +00:00
if cfg.OIDC.Issuer != "" {
err = app.initOIDC()
2021-10-08 09:43:52 +00:00
if err != nil {
return nil, err
}
2021-10-18 19:27:52 +00:00
}
2021-10-16 14:31:37 +00:00
if app.cfg.DNSConfig != nil && app.cfg.DNSConfig.Proxied { // if MagicDNS
2021-11-14 17:03:21 +00:00
magicDNSDomains := generateMagicDNSRootDomains(
app.cfg.IPPrefix,
2021-11-13 08:36:45 +00:00
)
// we might have routes already from Split DNS
if app.cfg.DNSConfig.Routes == nil {
app.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver)
}
2021-10-10 10:43:41 +00:00
for _, d := range magicDNSDomains {
app.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil
}
}
return &app, nil
2020-06-21 10:32:08 +00:00
}
// Redirect to our TLS url.
func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) {
target := h.cfg.ServerURL + req.URL.RequestURI()
http.Redirect(w, req, target, http.StatusFound)
}
2021-08-12 19:45:40 +00:00
// expireEphemeralNodes deletes ephemeral machine records that have not been
// seen for longer than h.cfg.EphemeralNodeInactivityTimeout.
2021-08-12 19:45:40 +00:00
func (h *Headscale) expireEphemeralNodes(milliSeconds int64) {
ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond)
for range ticker.C {
h.expireEphemeralNodesWorker()
}
}
func (h *Headscale) expireEphemeralNodesWorker() {
namespaces, err := h.ListNamespaces()
if err != nil {
2021-08-05 17:11:26 +00:00
log.Error().Err(err).Msg("Error listing namespaces")
return
}
for _, namespace := range namespaces {
machines, err := h.ListMachinesInNamespace(namespace.Name)
if err != nil {
2021-11-13 08:36:45 +00:00
log.Error().
Err(err).
Str("namespace", namespace.Name).
2021-11-13 08:36:45 +00:00
Msg("Error listing machines in namespace")
return
}
for _, machine := range machines {
if machine.AuthKey != nil && machine.LastSeen != nil &&
machine.AuthKey.Ephemeral &&
time.Now().
After(machine.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) {
2021-11-13 08:36:45 +00:00
log.Info().
Str("machine", machine.Name).
2021-11-13 08:36:45 +00:00
Msg("Ephemeral client removed from database")
err = h.db.Unscoped().Delete(machine).Error
if err != nil {
2021-10-22 16:55:14 +00:00
log.Error().
Err(err).
Str("machine", machine.Name).
2021-10-22 16:55:14 +00:00
Msg("🤮 Cannot delete ephemeral machine from the database")
}
}
}
h.setLastStateChangeToNow(namespace.Name)
}
}
// WatchForKVUpdates checks the KV DB table for requests to perform tailnet upgrades
// This is a way to communitate the CLI with the headscale server.
func (h *Headscale) watchForKVUpdates(milliSeconds int64) {
ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond)
for range ticker.C {
h.watchForKVUpdatesWorker()
}
}
func (h *Headscale) watchForKVUpdatesWorker() {
h.checkForNamespacesPendingUpdates()
// more functions will come here in the future
}
func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (interface{}, error) {
// Check if the request is coming from the on-server client.
// This is not secure, but it is to maintain maintainability
// with the "legacy" database-based client
// It is also neede for grpc-gateway to be able to connect to
// the server
client, _ := peer.FromContext(ctx)
2021-11-13 08:36:45 +00:00
log.Trace().
Caller().
Str("client_address", client.Addr.String()).
2021-11-13 08:36:45 +00:00
Msg("Client is trying to authenticate")
meta, ok := metadata.FromIncomingContext(ctx)
if !ok {
2021-11-13 08:36:45 +00:00
log.Error().
Caller().
Str("client_address", client.Addr.String()).
2021-11-13 08:36:45 +00:00
Msg("Retrieving metadata is failed")
2021-11-14 15:46:09 +00:00
2021-11-13 08:36:45 +00:00
return ctx, status.Errorf(
codes.InvalidArgument,
"Retrieving metadata is failed",
)
}
authHeader, ok := meta["authorization"]
if !ok {
2021-11-13 08:36:45 +00:00
log.Error().
Caller().
Str("client_address", client.Addr.String()).
2021-11-13 08:36:45 +00:00
Msg("Authorization token is not supplied")
2021-11-14 15:46:09 +00:00
2021-11-13 08:36:45 +00:00
return ctx, status.Errorf(
codes.Unauthenticated,
"Authorization token is not supplied",
)
}
token := authHeader[0]
if !strings.HasPrefix(token, AuthPrefix) {
log.Error().
Caller().
Str("client_address", client.Addr.String()).
Msg(`missing "Bearer " prefix in "Authorization" header`)
2021-11-14 15:46:09 +00:00
2021-11-13 08:36:45 +00:00
return ctx, status.Error(
codes.Unauthenticated,
`missing "Bearer " prefix in "Authorization" header`,
)
}
// TODO(kradalby): Implement API key backend:
// - Table in the DB
// - Key name
// - Encrypted
// - Expiry
//
// Currently all other than localhost traffic is unauthorized, this is intentional to allow
// us to make use of gRPC for our CLI, but not having to implement any of the remote capabilities
// and API key auth
2021-11-13 08:36:45 +00:00
return ctx, status.Error(
codes.Unauthenticated,
"Authentication is not implemented yet",
)
2021-11-14 17:44:37 +00:00
// if strings.TrimPrefix(token, AUTH_PREFIX) != a.Token {
// log.Error().Caller().Str("client_address", p.Addr.String()).Msg("invalid token")
// return ctx, status.Error(codes.Unauthenticated, "invalid token")
// }
// return handler(ctx, req)
}
func (h *Headscale) httpAuthenticationMiddleware(ctx *gin.Context) {
log.Trace().
Caller().
Str("client_address", ctx.ClientIP()).
Msg("HTTP authentication invoked")
authHeader := ctx.GetHeader("authorization")
if !strings.HasPrefix(authHeader, AuthPrefix) {
log.Error().
Caller().
Str("client_address", ctx.ClientIP()).
Msg(`missing "Bearer " prefix in "Authorization" header`)
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
ctx.AbortWithStatus(http.StatusUnauthorized)
// TODO(kradalby): Implement API key backend
// Currently all traffic is unauthorized, this is intentional to allow
// us to make use of gRPC for our CLI, but not having to implement any of the remote capabilities
// and API key auth
//
// if strings.TrimPrefix(authHeader, AUTH_PREFIX) != a.Token {
// log.Error().Caller().Str("client_address", c.ClientIP()).Msg("invalid token")
// c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error", "unauthorized"})
// return
// }
// c.Next()
}
// ensureUnixSocketIsAbsent will check if the given path for headscales unix socket is clear
// and will remove it if it is not.
func (h *Headscale) ensureUnixSocketIsAbsent() error {
// File does not exist, all fine
if _, err := os.Stat(h.cfg.UnixSocket); errors.Is(err, os.ErrNotExist) {
return nil
}
2021-11-14 15:46:09 +00:00
return os.Remove(h.cfg.UnixSocket)
}
// Serve launches a GIN server with the Headscale API.
2020-06-21 10:32:08 +00:00
func (h *Headscale) Serve() error {
var err error
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
err = h.ensureUnixSocketIsAbsent()
if err != nil {
panic(err)
}
socketListener, err := net.Listen("unix", h.cfg.UnixSocket)
if err != nil {
panic(err)
}
2021-11-02 21:46:15 +00:00
// Handle common process-killing signals so we can gracefully shut down:
sigc := make(chan os.Signal, 1)
2021-11-02 21:49:19 +00:00
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
2021-11-02 21:46:15 +00:00
go func(c chan os.Signal) {
// Wait for a SIGINT or SIGKILL:
sig := <-c
log.Printf("Caught signal %s: shutting down.", sig)
// Stop listening (and unlink the socket if unix type):
socketListener.Close()
// And we're done:
os.Exit(0)
}(sigc)
networkListener, err := net.Listen("tcp", h.cfg.Addr)
if err != nil {
panic(err)
}
// Create the cmux object that will multiplex 2 protocols on the same port.
// The two following listeners will be served on the same port below gracefully.
networkMutex := cmux.New(networkListener)
// Match gRPC requests here
grpcListener := networkMutex.MatchWithWriters(
cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"),
2021-11-13 08:36:45 +00:00
cmux.HTTP2MatchHeaderFieldSendSettings(
"content-type",
"application/grpc+proto",
),
)
// Otherwise match regular http requests.
httpListener := networkMutex.Match(cmux.Any())
grpcGatewayMux := runtime.NewServeMux()
// Make the grpc-gateway connect to grpc over socket
grpcGatewayConn, err := grpc.Dial(
h.cfg.UnixSocket,
[]grpc.DialOption{
grpc.WithInsecure(),
2021-10-30 14:29:03 +00:00
grpc.WithContextDialer(GrpcSocketDialer),
}...,
)
if err != nil {
return err
}
// Connect to the gRPC server over localhost to skip
// the authentication.
err = v1.RegisterHeadscaleServiceHandler(ctx, grpcGatewayMux, grpcGatewayConn)
if err != nil {
return err
}
router := gin.Default()
prometheus := ginprometheus.NewPrometheus("gin")
prometheus.Use(router)
router.GET(
2021-11-13 08:36:45 +00:00
"/health",
func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"healthy": "ok"}) },
)
router.GET("/key", h.KeyHandler)
router.GET("/register", h.RegisterWebAPI)
router.POST("/machine/:id/map", h.PollNetMapHandler)
router.POST("/machine/:id", h.RegistrationHandler)
router.GET("/oidc/register/:mkey", h.RegisterOIDC)
router.GET("/oidc/callback", h.OIDCCallback)
router.GET("/apple", h.AppleMobileConfig)
router.GET("/apple/:platform", h.ApplePlatformConfig)
router.GET("/swagger", SwaggerUI)
router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
api := router.Group("/api")
api.Use(h.httpAuthenticationMiddleware)
{
api.Any("/v1/*any", gin.WrapF(grpcGatewayMux.ServeHTTP))
}
router.NoRoute(stdoutHandler)
2021-10-22 16:55:14 +00:00
// Fetch an initial DERP Map before we start serving
h.DERPMap = GetDERPMap(h.cfg.DERP)
if h.cfg.DERP.AutoUpdate {
derpMapCancelChannel := make(chan struct{})
defer func() { derpMapCancelChannel <- struct{}{} }()
go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel)
}
// I HATE THIS
go h.watchForKVUpdates(updateInterval)
go h.expireEphemeralNodes(updateInterval)
httpServer := &http.Server{
Addr: h.cfg.Addr,
Handler: router,
ReadTimeout: HTTPReadTimeout,
// Go does not handle timeouts in HTTP very well, and there is
// no good way to handle streaming timeouts, therefore we need to
// keep this at unlimited and be careful to clean up connections
// https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/#aboutstreaming
WriteTimeout: 0,
}
if zl.GlobalLevel() == zl.TraceLevel {
zerolog.RespLog = true
} else {
zerolog.RespLog = false
}
grpcOptions := []grpc.ServerOption{
grpc.UnaryInterceptor(
grpc_middleware.ChainUnaryServer(
h.grpcAuthenticationInterceptor,
zerolog.NewUnaryServerInterceptor(),
),
),
}
tlsConfig, err := h.getTLSSettings()
if err != nil {
log.Error().Err(err).Msg("Failed to set up TLS configuration")
return err
}
if tlsConfig != nil {
httpServer.TLSConfig = tlsConfig
grpcOptions = append(grpcOptions, grpc.Creds(credentials.NewTLS(tlsConfig)))
}
grpcServer := grpc.NewServer(grpcOptions...)
// Start the local gRPC server without TLS and without authentication
grpcSocket := grpc.NewServer(zerolog.UnaryInterceptor())
v1.RegisterHeadscaleServiceServer(grpcServer, newHeadscaleV1APIServer(h))
v1.RegisterHeadscaleServiceServer(grpcSocket, newHeadscaleV1APIServer(h))
reflection.Register(grpcServer)
reflection.Register(grpcSocket)
errorGroup := new(errgroup.Group)
errorGroup.Go(func() error { return grpcSocket.Serve(socketListener) })
2021-10-31 16:34:20 +00:00
// TODO(kradalby): Verify if we need the same TLS setup for gRPC as HTTP
errorGroup.Go(func() error { return grpcServer.Serve(grpcListener) })
2021-10-31 16:19:38 +00:00
if tlsConfig != nil {
errorGroup.Go(func() error {
2021-10-31 16:19:38 +00:00
tlsl := tls.NewListener(httpListener, tlsConfig)
2021-11-14 15:46:09 +00:00
2021-10-31 16:19:38 +00:00
return httpServer.Serve(tlsl)
})
} else {
errorGroup.Go(func() error { return httpServer.Serve(httpListener) })
2021-10-31 16:19:38 +00:00
}
errorGroup.Go(func() error { return networkMutex.Serve() })
2021-11-13 08:36:45 +00:00
log.Info().
Msgf("listening and serving (multiplexed HTTP and gRPC) on: %s", h.cfg.Addr)
return errorGroup.Wait()
}
func (h *Headscale) getTLSSettings() (*tls.Config, error) {
2021-11-14 16:51:34 +00:00
var err error
if h.cfg.TLSLetsEncryptHostname != "" {
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
2021-11-13 08:36:45 +00:00
log.Warn().
Msg("Listening with TLS but ServerURL does not start with https://")
}
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(h.cfg.TLSLetsEncryptHostname),
Cache: autocert.DirCache(h.cfg.TLSLetsEncryptCacheDir),
Client: &acme.Client{
DirectoryURL: h.cfg.ACMEURL,
},
Email: h.cfg.ACMEEmail,
}
2021-11-14 17:44:37 +00:00
switch h.cfg.TLSLetsEncryptChallengeType {
case "TLS-ALPN-01":
// Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737)
// The RFC requires that the validation is done on port 443; in other words, headscale
// must be reachable on port 443.
return certManager.TLSConfig(), nil
2021-11-14 17:44:37 +00:00
case "HTTP-01":
// Configuration via autocert with HTTP-01. This requires listening on
// port 80 for the certificate validation in addition to the headscale
// service, which can be configured to run on any other port.
go func() {
2021-08-05 17:11:26 +00:00
log.Fatal().
Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, certManager.HTTPHandler(http.HandlerFunc(h.redirect)))).
2021-08-05 17:11:26 +00:00
Msg("failed to set up a HTTP server")
}()
return certManager.TLSConfig(), nil
2021-11-14 17:44:37 +00:00
default:
2021-11-15 19:18:14 +00:00
return nil, errUnsupportedLetsEncryptChallengeType
}
} else if h.cfg.TLSCertPath == "" {
if !strings.HasPrefix(h.cfg.ServerURL, "http://") {
2021-08-05 17:11:26 +00:00
log.Warn().Msg("Listening without TLS but ServerURL does not start with http://")
}
2021-11-14 16:51:34 +00:00
return nil, err
} else {
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
2021-08-05 17:11:26 +00:00
log.Warn().Msg("Listening with TLS but ServerURL does not start with https://")
}
2021-11-15 18:31:52 +00:00
tlsConfig := &tls.Config{
ClientAuth: tls.RequireAnyClientCert,
NextProtos: []string{"http/1.1"},
Certificates: make([]tls.Certificate, 1),
MinVersion: tls.VersionTLS12,
}
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLSCertPath, h.cfg.TLSKeyPath)
return tlsConfig, err
}
2020-06-21 10:32:08 +00:00
}
2021-08-19 17:19:26 +00:00
func (h *Headscale) setLastStateChangeToNow(namespace string) {
now := time.Now().UTC()
lastStateUpdate.WithLabelValues("", "headscale").Set(float64(now.Unix()))
2021-08-19 17:19:26 +00:00
h.lastStateChange.Store(namespace, now)
}
func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
times := []time.Time{}
for _, namespace := range namespaces {
if wrapped, ok := h.lastStateChange.Load(namespace); ok {
lastChange, _ := wrapped.(time.Time)
times = append(times, lastChange)
}
2021-08-19 17:19:26 +00:00
}
sort.Slice(times, func(i, j int) bool {
return times[i].After(times[j])
})
log.Trace().Msgf("Latest times %#v", times)
if len(times) == 0 {
return time.Now().UTC()
} else {
return times[0]
}
}
func stdoutHandler(ctx *gin.Context) {
body, _ := io.ReadAll(ctx.Request.Body)
log.Trace().
Interface("header", ctx.Request.Header).
Interface("proto", ctx.Request.Proto).
Interface("url", ctx.Request.URL).
Bytes("body", body).
Msg("Request did not match")
}