Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions cmd/tokenizer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package main

import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"flag"
"fmt"
Expand Down Expand Up @@ -109,6 +111,25 @@ func runServe() {
opts = append(opts, tokenizer.WithFlysrcParser(parser))
}

// MITM CA certificate for HTTPS interception
mitmCertPath := os.Getenv("MITM_CA_CERT_PATH")
mitmKeyPath := os.Getenv("MITM_CA_KEY_PATH")
if mitmCertPath != "" && mitmKeyPath != "" {
cert, err := tls.LoadX509KeyPair(mitmCertPath, mitmKeyPath)
if err != nil {
logrus.WithError(err).Fatal("failed to load MITM CA certificate")
}
// Parse the x509 certificate - goproxy needs this for TLSConfigFromCA
if cert.Leaf == nil && len(cert.Certificate) > 0 {
cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
if err != nil {
logrus.WithError(err).Fatal("failed to parse MITM CA certificate")
}
}
opts = append(opts, tokenizer.MitmCACert(cert))
logrus.Info("HTTPS MITM enabled - clients must trust the CA certificate")
}

tkz := tokenizer.NewTokenizer(key, opts...)

if len(os.Getenv("DEBUG")) != 0 {
Expand Down Expand Up @@ -218,12 +239,17 @@ Flags:

Configuration — tokenizer is configured using the following environment variables:

OPEN_KEY - Hex encoded curve25519 private key. You can provide 32
random, hex encoded bytes. The log output will contain
the associated public key.
LISTEN_ADDRESS - The host:port address to listen at. Default: ":8080"
FILTERED_HEADERS - Comma separated list of headers to filter from client
requests.
OPEN_KEY - Hex encoded curve25519 private key. You can provide 32
random, hex encoded bytes. The log output will contain
the associated public key.
LISTEN_ADDRESS - The host:port address to listen at. Default: ":8080"
FILTERED_HEADERS - Comma separated list of headers to filter from client
requests.
MITM_CA_CERT_PATH - Path to CA certificate for HTTPS MITM proxying.
When set along with MITM_CA_KEY_PATH, enables interception
of HTTPS CONNECT requests to inject credentials.
Clients must trust this CA certificate.
MITM_CA_KEY_PATH - Path to CA private key for HTTPS MITM proxying.
`)
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/alecthomas/assert/v2 v2.11.0
github.com/aws/aws-sdk-go-v2 v1.30.3
github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027
github.com/icholy/replace v0.6.0
github.com/sirupsen/logrus v1.9.3
github.com/superfly/flysrc-go v0.0.3
github.com/superfly/macaroon v0.2.14-0.20240819201738-61a02aa53648
Expand All @@ -19,7 +20,6 @@ require (
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/icholy/replace v0.6.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/sys v0.38.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027/go.mod h1:Ro8st/El
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
Expand All @@ -22,6 +24,7 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/icholy/replace v0.6.0 h1:EBiD2pGqZIOJAbEaf/5GVRaD/Pmbb4n+K3LrBdXd4dw=
github.com/icholy/replace v0.6.0/go.mod h1:zzi8pxElj2t/5wHHHYmH45D+KxytX/t4w3ClY5nlK+g=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down Expand Up @@ -62,4 +65,5 @@ golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgw
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
55 changes: 47 additions & 8 deletions tokenizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"io"
"net"
"net/http"
"net/url"
"strings"
"syscall"
"time"
Expand Down Expand Up @@ -48,6 +49,10 @@ type tokenizer struct {
// OpenProxy dictates whether requests without any sealed secrets are allowed.
OpenProxy bool

// MitmEnabled enables HTTPS MITM proxying with CA certificate.
// When enabled, the proxy can intercept HTTPS connections to inject credentials.
MitmEnabled bool

// RequireFlySrc will reject requests without a fly-src when set.
RequireFlySrc bool

Expand Down Expand Up @@ -77,6 +82,20 @@ func OpenProxy() Option {
}
}

// MitmCACert configures the CA certificate for HTTPS MITM proxying.
// When configured, the proxy can intercept HTTPS CONNECT requests and
// inject credentials into them. Clients must trust this CA certificate.
func MitmCACert(cert tls.Certificate) Option {
return func(t *tokenizer) {
t.MitmEnabled = true
goproxy.GoproxyCa = cert
goproxy.OkConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(&cert)}
goproxy.MitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(&cert)}
goproxy.HTTPMitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectHTTPMitm, TLSConfig: goproxy.TLSConfigFromCA(&cert)}
goproxy.RejectConnect = &goproxy.ConnectAction{Action: goproxy.ConnectReject, TLSConfig: nil}
}
}

// RequireFlySrc specifies that requests without a fly-src will be rejected.
func RequireFlySrc() Option {
return func(t *tokenizer) {
Expand Down Expand Up @@ -168,7 +187,7 @@ func NewTokenizer(openKey string, opts ...Option) *tokenizer {
})

proxy.Tr = &http.Transport{
Dial: dialFunc(tkz.tokenizerHostnames),
Dial: tkz.dialFunc(),
// probably not necessary, but I don't want to worry about desync/smuggling
DisableKeepAlives: true,
}
Expand Down Expand Up @@ -217,7 +236,7 @@ func (t *tokenizer) HandleConnect(host string, ctx *goproxy.ProxyCtx) (*goproxy.
}

_, port, _ := strings.Cut(host, ":")
if port == "443" {
if port == "443" && !t.MitmEnabled {
pud.connLog.Warn("attempt to proxy to https downstream")
ctx.Resp = errorResponse(ErrBadRequest)
return goproxy.RejectConnect, ""
Expand All @@ -236,6 +255,11 @@ func (t *tokenizer) HandleConnect(host string, ctx *goproxy.ProxyCtx) (*goproxy.
pud.connectProcessors = connectProcessors
ctx.UserData = pud

// For HTTPS (port 443) with MITM enabled, use MitmConnect to do TLS interception
// For HTTP tunnels, use HTTPMitmConnect to read plaintext HTTP
if port == "443" && t.MitmEnabled {
return goproxy.MitmConnect, host
}
return goproxy.HTTPMitmConnect, host
}

Expand Down Expand Up @@ -268,7 +292,7 @@ func (t *tokenizer) HandleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*ht

if !ok || !pud.requestStart.IsZero() || pud.reqLog != nil {
logrus.Warn("bad proxyUserData")
return nil, errorResponse(ErrInternal)
return req, errorResponse(ErrInternal)
}

pud.requestStart = time.Now()
Expand All @@ -283,7 +307,7 @@ func (t *tokenizer) HandleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*ht
if err != nil {
if t.RequireFlySrc {
pud.reqLog.Warn(err.Error())
return nil, errorResponse(ErrBadRequest)
return req, errorResponse(ErrBadRequest)
}
} else {
pud.reqLog = pud.reqLog.WithFields(logrus.Fields{
Expand All @@ -309,15 +333,15 @@ func (t *tokenizer) HandleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*ht

if len(processors) == 0 && !t.OpenProxy {
pud.reqLog.Warn("no processors")
return nil, errorResponse(ErrBadRequest)
return req, errorResponse(ErrBadRequest)
}

pud.reqLog = pud.reqLog.WithField("processors", len(processors))

for _, processor := range processors {
if err := processor(req); err != nil {
pud.reqLog.WithError(err).Warn("run processor")
return nil, errorResponse(ErrBadRequest)
return req, errorResponse(ErrBadRequest)
}
}

Expand Down Expand Up @@ -459,11 +483,20 @@ func errorResponse(err error) *http.Response {
}

return &http.Response{
Status: http.StatusText(status),
ProtoMajor: 1,
ProtoMinor: 1,
StatusCode: status,
Body: io.NopCloser(bytes.NewReader([]byte(err.Error()))),
Header: make(http.Header),
Request: &http.Request{
Method: "CONNECT",
URL: &url.URL{},
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
},
}
}

Expand All @@ -475,7 +508,8 @@ func errorResponse(err error) *http.Response {
// our proxy can't do passthrough TLS.
// - It forces the upstream connection to be TLS. We want the actual upstream
// connection to be over TLS because security.
func dialFunc(badAddrs []string) func(string, string) (net.Conn, error) {
func (t *tokenizer) dialFunc() func(string, string) (net.Conn, error) {
badAddrs := t.tokenizerHostnames
_, fdaaNet, err := net.ParseCIDR("fdaa::/8")
if err != nil {
panic(err)
Expand Down Expand Up @@ -527,7 +561,12 @@ func dialFunc(badAddrs []string) func(string, string) (net.Conn, error) {
}
switch port {
case "443":
return nil, fmt.Errorf("%w: proxied request must be HTTP", ErrBadRequest)
if !t.MitmEnabled {
return nil, fmt.Errorf("%w: proxied request must be HTTP", ErrBadRequest)
}
addr = fmt.Sprintf("%s:%s", hostname, port)

return netDialer.Dial(network, addr)
case "80", "":
port = "443"
}
Expand Down