Checkpoint

This commit is contained in:
2025-10-01 15:37:55 +02:00
parent 4a60059ff2
commit 03352e3312
31 changed files with 2611 additions and 384 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
# SQLite3 database file
*.db
# Log files
*.log
# Backup files
*~

206
cmd/styx/config.go Normal file
View File

@@ -0,0 +1,206 @@
package main
import (
"crypto/tls"
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsimple"
"git.maze.io/maze/styx/dataset"
"git.maze.io/maze/styx/internal/cryptutil"
"git.maze.io/maze/styx/logger"
"git.maze.io/maze/styx/policy"
"git.maze.io/maze/styx/proxy"
)
type Config struct {
Proxy ProxyConfig `hcl:"proxy,block"`
Policy []PolicyConfig `hcl:"policy,block"`
Data DataConfig `hcl:"data,block"`
}
func (c Config) Proxies(log logger.Structured) ([]*proxy.Proxy, error) {
policies := make(map[string]*policy.Policy)
for _, pc := range c.Policy {
p, err := policy.New(pc.Path, pc.Package)
if err != nil {
return nil, fmt.Errorf("policy %s: %w", pc.Name, err)
}
policies[pc.Name] = p
}
var (
onRequest []proxy.RequestHandler
onResponse []proxy.ResponseHandler
)
for _, name := range c.Proxy.On.Request {
log.Value("policy", name).Debug("Resolving request policy")
p, ok := policies[name]
if !ok {
return nil, fmt.Errorf("on request: no policy named %q", name)
}
onRequest = append(onRequest, policy.NewRequestHandler(p))
}
for _, name := range c.Proxy.On.Response {
log.Value("policy", name).Debug("Resolving response policy")
p, ok := policies[name]
if !ok {
return nil, fmt.Errorf("on response: no policy named %q", name)
}
onResponse = append(onResponse, policy.NewResponseHandler(p))
}
var proxies []*proxy.Proxy
for _, pc := range c.Proxy.Port {
log.Value("port", pc.Listen).Debug("Configuring proxy port")
p, err := pc.Proxy()
if err != nil {
return nil, err
}
p.OnRequest = append(p.OnRequest, onRequest...)
p.OnResponse = append(p.OnResponse, onResponse...)
proxies = append(proxies, p)
}
return proxies, nil
}
type ProxyConfig struct {
Port []PortConfig `hcl:"port,block"`
Upstream []string `hcl:"upstream"`
On ProxyPolicyConfig `hcl:"on,block"`
}
type PortConfig struct {
Listen string `hcl:"port,label"`
TLS *PortTLSConfig `hcl:"tls,block"`
Transparent int `hcl:"transparent,optional"`
Name string `hcl:"name,optional"`
}
type PortTLSConfig struct {
Cert string `hcl:"cert"`
Key string `hcl:"key,optional"`
CA string `hcl:"ca,optional"`
}
func (c PortConfig) Proxy() (*proxy.Proxy, error) {
p := proxy.New()
if c.Transparent > 0 {
p.OnConnect = append(p.OnConnect, proxy.Transparent(c.Transparent))
} else if c.TLS != nil {
cert, err := cryptutil.LoadTLSCertificate(c.TLS.Cert, c.TLS.Key)
if err != nil {
return nil, err
}
config := new(tls.Config)
config.Certificates = []tls.Certificate{cert}
if c.TLS.CA != "" {
roots, err := cryptutil.LoadRoots(c.TLS.CA)
if err != nil {
return nil, err
}
config.RootCAs = roots
}
p.OnConnect = append(p.OnConnect, proxy.TLS(config))
}
return p, nil
}
type ProxyPolicyConfig struct {
Intercept []string `hcl:"intercept,optional"`
Request []string `hcl:"request,optional"`
Response []string `hcl:"response,optional"`
}
type PolicyConfig struct {
Name string `hcl:"name,label"`
Path string `hcl:"path"`
Package string `hcl:"package,optional"`
}
type DataConfig struct {
Path string `hcl:"path,optional"`
Domains []DomainDataConfig `hcl:"domain,block"`
Networks []NetworkDataConfig `hcl:"network,block"`
}
func (c DataConfig) Configure() error {
for _, dc := range c.Domains {
if err := dc.Configure(); err != nil {
return fmt.Errorf("error setting up domain data %q: %w", dc.Name, err)
}
}
for _, nc := range c.Networks {
if err := nc.Configure(); err != nil {
return fmt.Errorf("error setting up network data %q: %w", nc.Name, err)
}
}
return nil
}
type DomainDataConfig struct {
Name string `hcl:"name,label"`
Type string `hcl:"type"`
Body hcl.Body `hcl:",remain"`
}
func (c DomainDataConfig) Configure() error {
switch c.Type {
case "", "list":
var justTheList struct {
List []string `hcl:"list"`
}
if diag := gohcl.DecodeBody(c.Body, nil, &justTheList); diag.HasErrors() {
return diag
}
dataset.Domains[c.Name] = dataset.NewDomainList(justTheList.List...)
default:
return fmt.Errorf("unknown type %q", c.Type)
}
return nil
}
type NetworkDataConfig struct {
Name string `hcl:"name,label"`
Type string `hcl:"type"`
Body hcl.Body `hcl:",remain"`
}
func (c NetworkDataConfig) Configure() error {
switch c.Type {
case "", "list":
var justTheList struct {
List []string `hcl:"list"`
}
if diag := gohcl.DecodeBody(c.Body, nil, &justTheList); diag.HasErrors() {
return diag
}
list, err := dataset.NewNetworkTree(justTheList.List...)
if err != nil {
return err
}
dataset.Networks[c.Name] = list
default:
return fmt.Errorf("unknown type %q", c.Type)
}
return nil
}
func Load(name string) (*Config, error) {
config := new(Config)
if err := hclsimple.DecodeFile(name, nil, config); err != nil {
return nil, err
}
return config, nil
}

View File

@@ -2,84 +2,90 @@ package main
import ( import (
"flag" "flag"
"net"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"github.com/hashicorp/hcl/v2/hclsimple" "git.maze.io/maze/styx/logger"
"git.maze.io/maze/styx/internal/log"
"git.maze.io/maze/styx/proxy" "git.maze.io/maze/styx/proxy"
"git.maze.io/maze/styx/proxy/cache"
"git.maze.io/maze/styx/proxy/match"
"git.maze.io/maze/styx/proxy/mitm"
"git.maze.io/maze/styx/proxy/resolver"
) )
func main() { func main() {
configFlag := flag.String("config", "styx.hcl", "Configuration file") var (
traceFlag := flag.Bool("T", false, "Enable trace level logging") configFile = "styx.hcl"
debugFlag := flag.Bool("D", false, "Enable debug level logging") traceFlag bool
debugFlag bool
)
flag.StringVar(&configFile, "config", configFile, "configuration file")
flag.BoolVar(&traceFlag, "T", traceFlag, "enable trace logging")
flag.BoolVar(&debugFlag, "D", debugFlag, "enable debug logging")
flag.Parse() flag.Parse()
if *traceFlag { log := logger.StandardLog.Value("path", configFile)
log.SetLevel(log.TraceLevel)
} else if *debugFlag { if traceFlag {
log.SetLevel(log.DebugLevel) log.SetLevel(logger.TraceLevel)
} else if debugFlag {
log.SetLevel(logger.DebugLevel)
} }
config, err := load(*configFlag) config, err := Load(configFile)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("") log.Err(err).Fatal("Invalid configuration file")
} }
matchers, err := config.Match.Matchers() if err = config.Data.Configure(); err != nil {
log.Err(err).Fatal("Invalid data configuration")
}
proxies, err := config.Proxies(log)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("") log.Err(err).Fatal("Error configuring proxy ports")
} else if err = config.Proxy.Policy.Configure(matchers); err != nil {
log.Fatal().Err(err).Msg("")
} }
var ca mitm.Authority var (
if config.MITM != nil { errs = make(chan error, 1)
if ca, err = mitm.New(config.MITM); err != nil { done = make(chan struct{}, 1)
log.Fatal().Err(err).Msg("error configuring mitm") sigs = make(chan os.Signal, 1)
)
for i, p := range proxies {
go run(config.Proxy.Port[i].Listen, p, errs)
}
signal.Notify(sigs, syscall.SIGINT, syscall.SIGHUP)
for {
select {
case sig := <-sigs:
switch sig {
case syscall.SIGHUP:
log.Value("signal", sig.String()).Warn("Ignored reload signal ¯\\_(ツ)_/¯")
default:
log.Value("signal", sig.String()).Info("Shutting down on signal")
return
}
case <-done:
log.Info("Shutting down gracefully")
return
case err = <-errs:
log.Err(err).Fatal("Shutting down because of fatal error in proxy")
} }
} }
}
server, err := proxy.New(&config.Proxy, ca) func run(addr string, port *proxy.Proxy, errors chan<- error) {
log := logger.StandardLog.Value("port", addr)
log.Info("Proxy port starting")
l, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("") errors <- err
return
} }
log.Info("Proxy port ready")
if err = server.Start(); err != nil { errors <- port.Serve(l)
log.Fatal().Err(err).Msg("")
}
signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM)
<-signalChannel
server.Close()
}
type Config struct {
DNS *resolver.Config `hcl:"dns,block"`
Proxy proxy.Config `hcl:"proxy,block"`
MITM *mitm.Config `hcl:"mitm,block"`
Cache *cache.Config `hcl:"cache,block"`
Match *match.Config `hcl:"match,block"`
}
func load(name string) (*Config, error) {
config := new(Config)
if err := hclsimple.DecodeFile(name, nil, config); err != nil {
return nil, err
}
if config.DNS != nil {
config.Proxy.Resolver = resolver.New(*config.DNS)
}
return config, nil
} }

View File

@@ -1,4 +1,4 @@
package netutil package dataset
import ( import (
"strings" "strings"

5
dataset/domain_data.go Normal file
View File

@@ -0,0 +1,5 @@
package dataset
var Domains = map[string]*DomainTree{
"example": NewDomainList("example.org", "example.net", "example.com"),
}

View File

@@ -1,4 +1,4 @@
package netutil package dataset
import ( import (
"testing" "testing"

52
dataset/network.go Normal file
View File

@@ -0,0 +1,52 @@
package dataset
import (
"net"
"github.com/yl2chen/cidranger"
)
type NetworkTree struct {
ranger cidranger.Ranger
}
func MustNetworkTree(networks ...string) *NetworkTree {
tree, err := NewNetworkTree(networks...)
if err != nil {
panic(err)
}
return tree
}
func NewNetworkTree(networks ...string) (*NetworkTree, error) {
tree := &NetworkTree{
ranger: cidranger.NewPCTrieRanger(),
}
for _, cidr := range networks {
if err := tree.AddCIDR(cidr); err != nil {
return nil, err
}
}
return tree, nil
}
func (tree *NetworkTree) Add(ipnet *net.IPNet) {
if ipnet == nil {
return
}
tree.ranger.Insert(cidranger.NewBasicRangerEntry(*ipnet))
}
func (tree *NetworkTree) AddCIDR(cidr string) error {
_, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return err
}
tree.ranger.Insert(cidranger.NewBasicRangerEntry(*ipnet))
return nil
}
func (tree *NetworkTree) Contains(ip net.IP) bool {
contains, _ := tree.ranger.Contains(ip)
return contains
}

71
dataset/network_data.go Normal file
View File

@@ -0,0 +1,71 @@
package dataset
var (
bogonsIPv4 = []string{
"9.0.0.0/8", // "This" network
"10.0.0.0/8", // RFC1918 Private-use networks
"100.64.0.0/10", // Carrier-grade NAT
"127.0.0.0/8", // Loopback
"169.254.0.0/16", // Link local
"172.16.0.0/12", // RFC1918 Private-use networks
"192.0.0.0/24", // IETF protocol assignments
"192.0.2.0/24", // TEST-NET-1
"192.168.0.0/16", // RFC1918 Private-use networks
"198.18.0.0/15", // Network interconnect device benchmark testing
"198.51.100.0/24", // TEST-NET-2
"203.0.113.0/24", // TEST-NET-3
"224.0.0.0/4", // Multicast
"240.0.0.0/4", // Reserved for future use
"255.255.255.255/32", // Limited broadcast
}
bogonsIPv6 = []string{
"::/128", // Node-scope unicast unspecified address
"::1/128", // Node-scope unicast loopback address
"::ffff:0:0/96", // IPv4-mapped addresses
"::/96", // IPv4-compatible addresses
"100::/64", // Remotely triggered black hole addresses
"2001:10::/28", // Overlay routable cryptographic hash identifiers (ORCHID)
"2001:db8::/32", // Documentation prefix
"3fff::/20", // Documentation prefix
"fc00::/7", // Unique local addresses (ULA)
"fe80::/10", // Link-local unicast
"fec0::/10", // Site-local unicast (deprecated)
"ff00::/8", // Multicast (Note: ff0e:/16 is global scope and may appear on the global internet.)
"2002::/24", // 6to4 bogon (0.0.0.0/8)
"2002:a00::/24", // 6to4 bogon (10.0.0.0/8)
"2002:7f00::/24", // 6to4 bogon (127.0.0.0/8)
"2002:a9fe::/32", // 6to4 bogon (169.254.0.0/16)
"2002:ac10::/28", // 6to4 bogon (172.16.0.0/12)
"2002:c000::/40", // 6to4 bogon (192.0.0.0/24)
"2002:c000:200::/40", // 6to4 bogon (192.0.2.0/24)
"2002:c0a8::/32", // 6to4 bogon (192.168.0.0/16)
"2002:c612::/31", // 6to4 bogon (198.18.0.0/15)
"2002:c633:6400::/40", // 6to4 bogon (198.51.100.0/24)
"2002:cb00:7100::/40", // 6to4 bogon (203.0.113.0/24)
"2002:e000::/20", // 6to4 bogon (224.0.0.0/4)
"2002:f000::/20", // 6to4 bogon (240.0.0.0/4)
"2002:ffff:ffff::/48", // 6to4 bogon (255.255.255.255/32)
"2001::/40", // Teredo bogon (0.0.0.0/8)
"2001:0:a00::/40", // Teredo bogon (10.0.0.0/8)
"2001:0:7f00::/40", // Teredo bogon (127.0.0.0/8)
"2001:0:a9fe::/48", // Teredo bogon (169.254.0.0/16)
"2001:0:ac10::/44", // Teredo bogon (172.16.0.0/12)
"2001:0:c000::/56", // Teredo bogon (192.0.0.0/24)
"2001:0:c000:200::/56", // Teredo bogon (192.0.2.0/24)
"2001:0:c0a8::/48", // Teredo bogon (192.168.0.0/16)
"2001:0:c612::/47", // Teredo bogon (198.18.0.0/15)
"2001:0:c633:6400::/56", // Teredo bogon (198.51.100.0/24)
"2001:0:cb00:7100::/56", // Teredo bogon (203.0.113.0/24)
"2001:0:e000::/36", // Teredo bogon (224.0.0.0/4)
"2001:0:f000::/36", // Teredo bogon (240.0.0.0/4)
"2001:0:ffff:ffff::/64", // Teredo bogon (255.255.255.255/32)
}
bogons = append(bogonsIPv4, bogonsIPv6...)
)
// Networks contains predefined network lists.
var Networks = map[string]*NetworkTree{
"bogons": MustNetworkTree(bogons...),
"boeong4": MustNetworkTree(bogonsIPv4...),
"bogons6": MustNetworkTree(bogonsIPv6...),
}

71
go.mod
View File

@@ -3,27 +3,74 @@ module git.maze.io/maze/styx
go 1.25.0 go 1.25.0
require ( require (
github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/go-viper/mapstructure/v2 v2.4.0
github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/hcl/v2 v2.24.0
github.com/mattn/go-sqlite3 v1.14.32
github.com/miekg/dns v1.1.68 github.com/miekg/dns v1.1.68
github.com/open-policy-agent/opa v1.9.0
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af
github.com/yl2chen/cidranger v1.0.2
) )
require ( require (
github.com/agext/levenshtein v1.2.1 // indirect github.com/agext/levenshtein v1.2.1 // indirect
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/flatbuffers v25.9.23+incompatible // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/lestrrat-go/dsig v1.0.0 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
github.com/lestrrat-go/jwx/v3 v3.0.11 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/yl2chen/cidranger v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/vektah/gqlparser/v2 v2.5.30 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect
github.com/zclconf/go-cty v1.16.3 // indirect github.com/zclconf/go-cty v1.16.3 // indirect
golang.org/x/mod v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
golang.org/x/net v0.40.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect
golang.org/x/sync v0.14.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect
golang.org/x/sys v0.33.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect
golang.org/x/text v0.25.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect
golang.org/x/tools v0.33.0 // indirect go.opentelemetry.io/proto/otlp v1.8.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
) )

195
go.sum
View File

@@ -1,60 +1,217 @@
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs=
github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w=
github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk=
github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/flatbuffers v25.9.23+incompatible h1:rGZKv+wOb6QPzIdkM2KxhBZCDrA0DeN6DNmRDrqIsQU=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/flatbuffers v25.9.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
github.com/lestrrat-go/jwx/v3 v3.0.11 h1:yEeUGNUuNjcez/Voxvr7XPTYNraSQTENJgtVTfwvG/w=
github.com/lestrrat-go/jwx/v3 v3.0.11/go.mod h1:XSOAh2SiXm0QgRe3DulLZLyt+wUuEdFo81zuKTLcvgQ=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/open-policy-agent/opa v1.9.0 h1:QWFNwbcc29IRy0xwD3hRrMc/RtSersLY1Z6TaID3vgI=
github.com/open-policy-agent/opa v1.9.0/go.mod h1:72+lKmTda0O48m1VKAxxYl7MjP/EWFZu9fxHQK2xihs=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=
github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE=
github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

168
internal/cryptutil/tls.go Normal file
View File

@@ -0,0 +1,168 @@
package cryptutil
import (
"bufio"
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"io"
"net"
"os"
"slices"
"strings"
"git.maze.io/maze/styx/internal/netutil"
"git.maze.io/maze/styx/internal/sliceutil"
"git.maze.io/maze/styx/logger"
)
var (
supportedCipherSuites = tls.CipherSuites()
supportedCipherSuite = make(map[uint16]bool)
supportedVersions = []uint16{
tls.VersionTLS13,
tls.VersionTLS12,
tls.VersionTLS11,
tls.VersionTLS10,
}
)
func init() {
for _, suite := range supportedCipherSuites {
supportedCipherSuite[suite.ID] = true
}
}
func DecodeTLSCertificate(b []byte) (tls.Certificate, error) {
var (
cert tls.Certificate
chain []*x509.Certificate
rest = b
block *pem.Block
err error
)
for {
if block, rest = pem.Decode(rest); block == nil {
break
}
switch block.Type {
case "CERTIFICATE":
cert.Certificate = append(cert.Certificate, block.Bytes)
if x509Cert, err := x509.ParseCertificate(block.Bytes); err != nil {
return tls.Certificate{}, err
} else {
chain = append(chain, x509Cert)
cert.Leaf = x509Cert
}
case "PRIVATE KEY":
if cert.PrivateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil {
return tls.Certificate{}, err
}
case "RSA PRIVATE KEY":
if cert.PrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
return tls.Certificate{}, err
}
case "EC PRIVATE KEY":
if cert.PrivateKey, err = x509.ParseECPrivateKey(block.Bytes); err != nil {
return tls.Certificate{}, err
}
}
}
return cert, nil
}
func LoadTLSCertificate(certFile, keyFile string) (tls.Certificate, error) {
var (
b []byte
err error
)
if strings.Contains(certFile, "-----BEGIN") {
logger.StandardLog.Trace("Loading X.509 certificate")
b = []byte(certFile)
} else {
logger.StandardLog.Value("name", certFile).Trace("Loading X.509 certificate")
if b, err = os.ReadFile(certFile); err != nil {
return tls.Certificate{}, err
}
}
if strings.Contains(keyFile, "-----BEGIN") {
logger.StandardLog.Trace("Loading private key")
b = append(b, []byte(keyFile)...)
} else if keyFile != "" {
logger.StandardLog.Value("name", keyFile).Trace("Loading private key")
var k []byte
if k, err = os.ReadFile(keyFile); err != nil {
return tls.Certificate{}, err
}
b = append(b, k...)
}
return DecodeTLSCertificate(b)
}
// CheckTLSBuffer is like [CheckTLSHandshake] but restores the original buffered reader.
func CheckTLSBuffer(r *bufio.Reader) (bool, error) {
b, err := r.ReadByte()
if err != nil {
return false, err
}
if err = r.UnreadByte(); err != nil {
return false, err
}
return b == 0x16, nil
}
// CheckTLSHandshake checks if the next byte available in r looks like a TLS handshake.
func CheckTLSHandshake(r io.Reader) (bool, error) {
// Peek first byte received in tunneled connection, client initiates the TLS connection or plain HTTP request
b := make([]byte, 1)
if _, err := io.ReadFull(r, b); err != nil {
return false, err
}
// TLS handshake: https://tools.ietf.org/html/rfc5246#section-6.2.1
return b[0] == 0x16, nil
}
// SniffClientHello uses ReadClientHello to sniff the TLS handshake and returns a new [net.Conn] that
// contains the original byte sequences.
func SniffClientHello(c net.Conn) (net.Conn, *tls.ClientHelloInfo, error) {
b := new(bytes.Buffer)
h, err := ReadClientHello(io.TeeReader(c, b))
return netutil.ReaderConn{
Conn: c,
Reader: io.MultiReader(b, c),
}, h, err
}
// ReadClientHello reads a TLS client hello message from the TLS handshake.
func ReadClientHello(r io.Reader) (*tls.ClientHelloInfo, error) {
var hello *tls.ClientHelloInfo
err := tls.Server(netutil.ReadOnlyConn{Reader: r}, &tls.Config{
GetConfigForClient: func(clientHello *tls.ClientHelloInfo) (*tls.Config, error) {
hello = new(tls.ClientHelloInfo)
*hello = *clientHello
return nil, nil
},
}).Handshake()
if hello == nil {
return nil, err
}
return hello, nil
}
// IsSupportedCipherSuite checks if Go can support the cipher suite.
func IsSupportedCipherSuite(id uint16) bool {
return supportedCipherSuite[id]
}
// IsSupportedVersion checks if Go can support the TLS version.
func IsSupportedVersion(version uint16) bool {
return slices.Contains(supportedVersions, version)
}
// OnlySecureCipherSuites removes any cipher suite that isn't supported by Go.
func OnlySecureCipherSuites(ids []uint16) []uint16 {
return sliceutil.Filter(ids, IsSupportedCipherSuite)
}

View File

@@ -18,7 +18,8 @@ import (
"strings" "strings"
"time" "time"
"git.maze.io/maze/styx/internal/log" "git.maze.io/maze/styx/logger"
"github.com/rs/zerolog/log"
) )
// Supported key types. // Supported key types.
@@ -36,6 +37,62 @@ const (
pemTypeAny = "PRIVATE KEY" pemTypeAny = "PRIVATE KEY"
) )
// DecodeRoots loads all PEM encoded certificates from b.
func DecodeRoots(b []byte) (*x509.CertPool, error) {
var (
pool = x509.NewCertPool()
rest = b
block *pem.Block
)
for {
if block, rest = pem.Decode(rest); block == nil {
break
} else if block.Type == pemTypeCert {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
} else if cert.IsCA {
pool.AddCert(cert)
}
}
}
return pool, nil
}
// LoadRoots loads a certificate authority bundle.
func LoadRoots(roots string) (*x509.CertPool, error) {
if strings.Contains(roots, "-----BEGIN CERTIFICATE") {
logger.StandardLog.Trace("Parsing X.509 certificates")
return DecodeRoots([]byte(roots))
}
var b []byte
i, err := os.Stat(roots)
if err != nil {
return nil, err
} else if i.IsDir() {
logger.StandardLog.Value("path", roots).Trace("Loading X.509 certificates from *.crt *.pem")
for _, ext := range []string{"*.crt", "*.pem"} {
var files []string
if files, err = filepath.Glob(filepath.Join(roots, ext)); err != nil {
return nil, err
}
for _, file := range files {
var v []byte
if v, err = os.ReadFile(file); err != nil {
return nil, err
}
b = append(b, v...)
}
}
} else {
logger.StandardLog.Value("path", roots).Trace("Loading X.509 certificates")
if b, err = os.ReadFile(roots); err != nil {
return nil, err
}
}
return DecodeRoots(b)
}
// LoadKeyPair loads a certificate and private key, certdata and keydata can be a PEM encoded block or a file. // LoadKeyPair loads a certificate and private key, certdata and keydata can be a PEM encoded block or a file.
// //
// If [keydata] is empty, then the private key is assumed to be contained in [certdata]. // If [keydata] is empty, then the private key is assumed to be contained in [certdata].
@@ -44,23 +101,23 @@ func LoadKeyPair(certdata, keydata string) (cert *x509.Certificate, key crypto.P
keydata = certdata keydata = certdata
} }
if strings.Contains(certdata, "-----BEGIN "+pemTypeCert) { if strings.Contains(certdata, "-----BEGIN "+pemTypeCert) {
log.Trace().Msg("parsing X.509 certificate") logger.StandardLog.Trace("Parsing X.509 certificate")
if cert, err = decodePEMBCertificate([]byte(certdata)); err != nil { if cert, err = decodePEMBCertificate([]byte(certdata)); err != nil {
return return
} }
} else { } else {
log.Trace().Str("name", certdata).Msg("loading X.509 certificate") logger.StandardLog.Value("name", certdata).Trace("Loading X.509 certificate")
if cert, err = LoadCertificate(certdata); err != nil { if cert, err = LoadCertificate(certdata); err != nil {
return return
} }
} }
if strings.Contains(keydata, pemTypeAny+"-----") { if strings.Contains(keydata, pemTypeAny+"-----") {
log.Trace().Msg("parsing private key") logger.StandardLog.Trace("Parsing private key")
if key, err = decodePEMPrivateKey([]byte(keydata)); err != nil { if key, err = decodePEMPrivateKey([]byte(keydata)); err != nil {
return return
} }
} else if key, err = LoadPrivateKey(keydata); err != nil { } else if key, err = LoadPrivateKey(keydata); err != nil {
log.Trace().Str("name", keydata).Msg("loading private key") logger.StandardLog.Value("name", keydata).Trace("Loading private key")
return return
} }
return return

View File

@@ -1,44 +0,0 @@
package log
import (
"io"
"github.com/rs/zerolog"
)
// Aliases
const (
TraceLevel = zerolog.TraceLevel
DebugLevel = zerolog.DebugLevel
InfoLevel = zerolog.InfoLevel
WarnLevel = zerolog.WarnLevel
FatalLevel = zerolog.FatalLevel
)
// Aliases
type (
Event = zerolog.Event
Logger = zerolog.Logger
)
// Console logger.
var Console = zerolog.New(zerolog.NewConsoleWriter()).With().Timestamp().Logger()
func SetLevel(level zerolog.Level) {
zerolog.SetGlobalLevel(level)
//Console = Console.Level(level)
}
func Trace() *Event { return Console.Trace() }
func Debug() *Event { return Console.Debug() }
func Info() *Event { return Console.Info() }
func Warn() *Event { return Console.Warn() }
func Error() *Event { return Console.Error() }
func Fatal() *Event { return Console.Fatal() }
func Panic() *Event { return Console.Panic() }
func OnCloseError(event *Event, closer io.Closer) {
if err := closer.Close(); err != nil {
event.Err(err).Msg("close failed")
}
}

View File

@@ -29,6 +29,10 @@ func Port(name string) int {
return 0 return 0
} }
if i, err := net.LookupPort("tcp", port); err == nil {
return i
}
// TODO: name resolution for ports? // TODO: name resolution for ports?
i, _ := strconv.Atoi(port) i, _ := strconv.Atoi(port)
return i return i

View File

@@ -0,0 +1,63 @@
package arp
import (
"net"
"sync"
"time"
"github.com/sirupsen/logrus"
)
func init() {
go func() {
t := time.NewTicker(time.Second * 5)
for {
refresh()
<-t.C
}
}()
}
var table sync.Map
func refresh() {
t, err := lookup()
if err != nil {
logrus.StandardLogger().WithError(err).Warn("arp cache refresh failed")
} else {
for k, v := range t {
logrus.StandardLogger().WithFields(logrus.Fields{
"mac": v,
"ip": k,
}).Debug("Updating ARP cache")
table.Store(k, v)
}
}
}
func Get(addr net.Addr) net.HardwareAddr {
if addr == nil {
logrus.StandardLogger().Trace("No address found, can't lookup IP for MAC")
return nil
}
var ip net.IP
switch addr := addr.(type) {
case *net.TCPAddr:
ip = addr.IP
case *net.UDPAddr:
ip = addr.IP
}
if ip == nil {
logrus.StandardLogger().WithField("addr", addr.String()).Trace("No IP address found, can't lookup MAC")
return nil
}
if v, ok := table.Load(ip.String()); ok {
logrus.StandardLogger().WithField("ip", ip.String()).Tracef("%s is at %s", ip, v.(net.HardwareAddr).String())
return v.(net.HardwareAddr)
}
logrus.StandardLogger().WithField("ip", ip.String()).Trace("No MAC address found")
return nil
}

View File

@@ -0,0 +1,32 @@
package arp
import (
"bufio"
"net"
"os"
"strings"
)
func lookup() (map[string]net.HardwareAddr, error) {
f, err := os.Open("/proc/net/arp")
if err != nil {
return nil, err
}
defer func() { _ = f.Close() }()
t := make(map[string]net.HardwareAddr)
s := bufio.NewScanner(f)
for i := 0; s.Scan(); i++ {
if i == 0 {
continue
}
line := strings.Fields(s.Text())
if len(line) < 4 {
continue
}
if mac, err := net.ParseMAC(line[3]); err == nil {
t[line[0]] = mac
}
}
return t, nil
}

View File

@@ -0,0 +1,37 @@
//go:build !linux
// +build !linux
// ^ Linux isn't Unix anyway :P
package arp
import (
"net"
"os/exec"
"strings"
)
func lookup() (map[string]net.HardwareAddr, error) {
data, err := exec.Command("arp", "-an").Output()
if err != nil {
return nil, err
}
t := make(map[string]net.HardwareAddr)
for _, line := range strings.Split(string(data), "\n") {
fields := strings.Fields(line)
if len(fields) < 3 {
continue
}
// strip brackets around IP
ip := strings.ReplaceAll(fields[1], "(", "")
ip = strings.ReplaceAll(ip, ")", "")
if mac, err := net.ParseMAC(fields[3]); err == nil {
t[ip] = mac
}
}
return t, nil
}

93
internal/netutil/conn.go Normal file
View File

@@ -0,0 +1,93 @@
package netutil
import (
"bufio"
"errors"
"io"
"net"
"syscall"
"time"
)
// BufferedConn uses byte buffers for Read and Write operations on a [net.Conn].
type BufferedConn struct {
net.Conn
Reader *bufio.Reader
Writer *bufio.Writer
}
func NewBufferedConn(c net.Conn) *BufferedConn {
if b, ok := c.(*BufferedConn); ok {
return b
}
return &BufferedConn{
Conn: c,
Reader: bufio.NewReader(c),
Writer: bufio.NewWriter(c),
}
}
func (conn BufferedConn) Read(p []byte) (int, error) { return conn.Reader.Read(p) }
func (conn BufferedConn) Write(p []byte) (int, error) { return conn.Writer.Write(p) }
func (conn BufferedConn) Flush() error { return conn.Writer.Flush() }
func (conn BufferedConn) NetConn() net.Conn { return conn.Conn }
// ReaderConn is a [net.Conn] with a separate [io.Reader] to read from.
type ReaderConn struct {
net.Conn
io.Reader
}
func (conn ReaderConn) Read(p []byte) (int, error) { return conn.Reader.Read(p) }
func (conn ReaderConn) NetConn() net.Conn { return conn.Conn }
// ReadOnlyConn only allows reading, all other operations will fail.
type ReadOnlyConn struct {
io.Reader
}
func (conn ReadOnlyConn) Read(p []byte) (int, error) { return conn.Reader.Read(p) }
func (conn ReadOnlyConn) Write(p []byte) (int, error) { return 0, io.ErrClosedPipe }
func (conn ReadOnlyConn) Close() error { return nil }
func (conn ReadOnlyConn) LocalAddr() net.Addr { return nil }
func (conn ReadOnlyConn) RemoteAddr() net.Addr { return nil }
func (conn ReadOnlyConn) SetDeadline(t time.Time) error { return nil }
func (conn ReadOnlyConn) SetReadDeadline(t time.Time) error { return nil }
func (conn ReadOnlyConn) SetWriteDeadline(t time.Time) error { return nil }
func (conn ReadOnlyConn) NetConn() net.Conn {
if c, ok := conn.Reader.(net.Conn); ok {
return c
}
return nil
}
func IsClosing(err error) bool {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) || errors.Is(err, syscall.ECONNRESET) || err.Error() != "proxy: shutdown" {
return true
}
if err, ok := err.(net.Error); ok && err.Timeout() {
return true
}
// log.Debug().Msgf("not a closing error %T: %#+v", err, err)
return false
}
// WithTimeout is a convenience wrapper for doing network operations that observe a timeout.
func WithTimeout(c net.Conn, timeout time.Duration, do func() error) error {
if timeout <= 0 {
return do()
}
if err := c.SetDeadline(time.Now().Add(timeout)); err != nil {
return err
}
if err := do(); err != nil {
_ = c.SetDeadline(time.Time{})
return err
}
if err := c.SetDeadline(time.Time{}); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,70 @@
package sliceutil
// AppendFilter takes two slice, 's' as source and 'd' as destination
// and a predicate function, then applies it to each element of 's',
// when 'p' returns true it appends the element into d, otherwise omit it.
func AppendFilter[T any](s []T, d *[]T, p func(T) bool) {
for _, e := range s {
if p(e) {
*d = append(*d, e)
}
}
}
// AssignFilter takes two slice, 's' as source and 'd' as destination
// and a predicate function, then applies it to each element of 's'.
// 'd' slice will have the same capacity of 's' but starts with 0 length.
// When 'p' returns true it appends the element into d, otherwise omit it.
func AssignFilter[T any](s []T, d *[]T, p func(T) bool) {
if cap(*d) == 0 {
*d = make([]T, 0, len(s))
}
for _, e := range s {
if p(e) {
*d = append(*d, e)
}
}
}
// DisposeFilter takes two slice, 's' as source and 'd' as destination
// and a predicate function, then applies it to each element of 's'.
// 'd' slice will share the exact same memory address of 's'.
// When 'p' returns true it appends the element into d, otherwise omit it.
// Then disposes the 's'. IMPORTANT: 's' cannot be used again.
func DisposeFilter[T any](s []T, d *[]T, p func(T) bool) {
*d = s[:0]
for _, e := range s {
if p(e) {
*d = append(*d, e)
}
}
var NIL T
for i := len(*d); i < len(s); i++ {
s[i] = NIL
}
}
// InPlaceFilter takes a slice and a predicate function, then applies it to each element of 's'.
// When 'p' returns true it assign the value to the last index plus one where p was true, otherwise omit it.
func InPlaceFilter[T any](s *[]T, p func(T) bool) {
i := 0
for _, e := range *s {
if p(e) {
(*s)[i] = e
i++
}
}
*s = (*s)[:i]
}
// Filter takes a slice and a predcate function, then applies it to each element of 's'.
// When 'p' returns true it appends the value to the output slice.
func Filter[T any](s []T, p func(T) bool) []T {
o := make([]T, 0, len(s))
for _, e := range s {
if p(e) {
o = append(o, e)
}
}
return o
}

223
logger/log.go Normal file
View File

@@ -0,0 +1,223 @@
package logger
import "github.com/sirupsen/logrus"
type Level int
const (
TraceLevel Level = -1 + iota
DebugLevel
InfoLevel
WarnLevel
ErrorLevel
PanicLevel
FatalLevel
)
// Logger is a generic logging interface, similar to logrus's [logrus.ValueLogger].
// It is used in Styx for logging, so that users can plug in their own logging implementations.
type Logger interface {
SetLevel(Level)
GetLevel() Level
Trace(...any)
Tracef(string, ...any)
Debug(...any)
Debugf(string, ...any)
Info(...any)
Infof(string, ...any)
Warn(...any)
Warnf(string, ...any)
Error(...any)
Errorf(string, ...any)
Panic(...any)
Panicf(string, ...any)
Fatal(...any)
Fatalf(string, ...any)
}
type Structured interface {
Logger
// Err adds an error to the log entry and returns the new logger.
Err(error) Structured
// Value returns a new logger with the specified Value added to the log entry.
Value(string, any) Structured
// Values returns a new logger with the specified Values added to the log entry.
Values(Values) Structured
}
type Values map[string]any
// Alias.
type V = Values
// StandardLog is the logger used by the package-level exported functions.
var StandardLog = NewStandardLogger()
// SetLogger sets the logger used by the package-level exported functions.
func SetLogger(logger Structured) {
StandardLog = logger
}
// Get returns the logger used by the package-level exported functions.
func Get() Structured {
return StandardLog
}
type standardLogger struct {
*logrus.Logger
}
// NewStandardLogger returns a new Structured logger that wraps the standard logrus logger.
func NewStandardLogger() Structured {
return standardLogger{logrus.StandardLogger()}
}
type standardLoggerEntry struct {
standardLogger
*logrus.Entry
}
func SetLevel(level Level) {
StandardLog.SetLevel(level)
}
func (l standardLogger) SetLevel(level Level) {
switch level {
case TraceLevel:
l.Logger.SetLevel(logrus.TraceLevel)
case DebugLevel:
l.Logger.SetLevel(logrus.DebugLevel)
case InfoLevel:
l.Logger.SetLevel(logrus.InfoLevel)
case WarnLevel:
l.Logger.SetLevel(logrus.WarnLevel)
case ErrorLevel:
l.Logger.SetLevel(logrus.ErrorLevel)
case PanicLevel:
l.Logger.SetLevel(logrus.PanicLevel)
case FatalLevel:
l.Logger.SetLevel(logrus.FatalLevel)
}
}
func GetLevel() Level {
return StandardLog.GetLevel()
}
func (l standardLogger) GetLevel() Level {
switch l.Logger.GetLevel() {
case logrus.TraceLevel:
return TraceLevel
case logrus.DebugLevel:
return DebugLevel
case logrus.InfoLevel:
return InfoLevel
case logrus.WarnLevel:
return WarnLevel
case logrus.ErrorLevel:
return ErrorLevel
case logrus.PanicLevel:
return PanicLevel
case logrus.FatalLevel:
return FatalLevel
default:
return InfoLevel
}
}
func (l standardLogger) Err(err error) Structured {
return standardLoggerEntry{l, l.Logger.WithError(err)}
}
func (l standardLogger) Value(key string, value any) Structured {
return standardLoggerEntry{l, l.Logger.WithField(key, value)}
}
func (l standardLogger) Values(Values Values) Structured {
return standardLoggerEntry{l, l.Logger.WithFields(logrus.Fields(Values))}
}
func (l standardLoggerEntry) Err(err error) Structured {
return standardLoggerEntry{l.standardLogger, l.Entry.WithError(err)}
}
func (l standardLoggerEntry) Value(key string, value any) Structured {
return standardLoggerEntry{l.standardLogger, l.Entry.WithField(key, value)}
}
func (l standardLoggerEntry) Values(Values Values) Structured {
return standardLoggerEntry{l.standardLogger, l.Entry.WithFields(logrus.Fields(Values))}
}
// Trace logs a message at level Trace on the standard logger.
func Trace(args ...any) {
StandardLog.Trace(args...)
}
// Tracef logs a message at level Trace on the standard logger.
func Tracef(format string, args ...any) {
StandardLog.Tracef(format, args...)
}
// Debug logs a message at level Debug on the standard logger.
func Debug(args ...any) {
StandardLog.Debug(args...)
}
// Debugf logs a message at level Debug on the standard logger.
func Debugf(format string, args ...any) {
StandardLog.Debugf(format, args...)
}
// Info logs a message at level Info on the standard logger.
func Info(args ...any) {
StandardLog.Info(args...)
}
// Infof logs a message at level Info on the standard logger.
func Infof(format string, args ...any) {
StandardLog.Infof(format, args...)
}
// Warn logs a message at level Warn on the standard logger.
func Warn(args ...any) {
StandardLog.Warn(args...)
}
// Warnf logs a message at level Warn on the standard logger.
func Warnf(format string, args ...any) {
StandardLog.Warnf(format, args...)
}
// Error logs a message at level Error on the standard logger.
func Error(args ...any) {
StandardLog.Error(args...)
}
// Errorf logs a message at level Error on the standard logger.
func Errorf(format string, args ...any) {
StandardLog.Errorf(format, args...)
}
// Panic logs a message at level Panic on the standard logger.
func Panic(args ...any) {
StandardLog.Panic(args...)
}
// Panicf logs a message at level Panic on the standard logger.
func Panicf(format string, args ...any) {
StandardLog.Panicf(format, args...)
}
// Fatal logs a message at level Fatal on the standard logger then the process will exit.
func Fatal(args ...any) {
StandardLog.Fatal(args...)
}
// Fatalf logs a message at level Fatal on the standard logger then the process will exit.
func Fatalf(format string, args ...any) {
StandardLog.Fatalf(format, args...)
}

179
policy/func.go Normal file
View File

@@ -0,0 +1,179 @@
package policy
import (
"errors"
"fmt"
"net"
"os"
"strconv"
"strings"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/rego"
"github.com/open-policy-agent/opa/v1/types"
"git.maze.io/maze/styx/dataset"
"git.maze.io/maze/styx/logger"
)
var domainContainsDecl = types.NewFunction(
types.Args(
types.Named("list", types.S).Description("Domain list to check against"),
types.Named("name", types.S).Description("Host name to check"),
),
types.Named("result", types.B).Description("`true` if `name` is contained within `list`"),
)
func domainContainsImpl(bc rego.BuiltinContext, listTerm, nameTerm *ast.Term) (*ast.Term, error) {
log := logger.StandardLog.Value("func", "styx.in_domains")
list, err := parseDomainListTerm(listTerm)
if err != nil {
log.Err(err).Debug("Call function failed")
return nil, err
}
name, err := parseStringTerm(nameTerm)
if err != nil {
return nil, err
}
log.Values(logger.Values{
"list": listTerm.Value,
"name": name,
}).Trace("Calling function")
return ast.BooleanTerm(list.Contains(name)), nil
}
var networkContainsDecl = types.NewFunction(
types.Args(
types.Named("list", types.S).Description("Network list to check against"),
types.Named("ip", types.S).Description("IP address to check"),
),
types.Named("result", types.B).Description("`true` if `ip` is contained within `list`"),
)
func networkContainsImpl(bc rego.BuiltinContext, listTerm, ipTerm *ast.Term) (*ast.Term, error) {
log := logger.StandardLog.Value("func", "styx.in_networks")
list, err := parseNetworkListTerm(listTerm)
if err != nil {
log.Err(err).Debug("Call function failed")
return nil, err
}
ip, err := parseIPTerm(ipTerm)
if err != nil {
log.Value("list", listTerm.Value).Err(err).Debug("Call function failed")
return nil, err
}
log.Values(logger.Values{
"list": listTerm.Value,
"ip": ip.String(),
}).Trace("Calling function")
return ast.BooleanTerm(list.Contains(ip)), nil
}
func parseDomainListTerm(term *ast.Term) (*dataset.DomainTree, error) {
nameArg, ok := term.Value.(ast.String)
if !ok {
return nil, errors.New("expected string argument")
}
name := strings.Trim(nameArg.String(), `"`)
fn, ok := dataset.Domains[name]
if !ok {
return nil, fmt.Errorf("no such domain list: %q", name)
}
return fn, nil
}
func parseNetworkListTerm(term *ast.Term) (*dataset.NetworkTree, error) {
nameArg, ok := term.Value.(ast.String)
if !ok {
return nil, errors.New("expected string argument")
}
name := strings.Trim(nameArg.String(), `"`)
fn, ok := dataset.Networks[name]
if !ok {
return nil, fmt.Errorf("no such network list: %q", name)
}
return fn, nil
}
func parseStringTerm(term *ast.Term) (string, error) {
ipArg, ok := term.Value.(ast.String)
if !ok {
return "", errors.New("expected string argument")
}
return strings.Trim(ipArg.String(), `"`), nil
}
func parseIPTerm(term *ast.Term) (net.IP, error) {
ipArg, ok := term.Value.(ast.String)
if !ok {
return nil, errors.New("expected string argument")
}
ip := strings.Trim(ipArg.String(), `"`)
if ip := net.ParseIP(ip); ip != nil {
return ip, nil
}
return nil, fmt.Errorf("invalid IP address %q", ip)
}
type ListReturner func() ([]string, error)
var (
domains = map[string]ListReturner{}
networks = map[string]ListReturner{}
)
func AddDomainList(name string, fn ListReturner) {
domains[name] = fn
}
func AddNetworkList(name string, fn ListReturner) {
networks[name] = fn
}
func listLookupImpl(kind string, m map[string]ListReturner) func(rego.BuiltinContext, *ast.Term) (*ast.Term, error) {
return func(bc rego.BuiltinContext, inf *ast.Term) (*ast.Term, error) {
log := logger.StandardLog.Values(logger.V{
"where": inf.Location.File + ":" + strconv.Itoa(inf.Location.Row) + "," + strconv.Itoa(inf.Location.Col),
"func": kind,
})
nameArg, ok := inf.Value.(ast.String)
if !ok {
return nil, errors.New("expected string argument")
}
name := strings.Trim(nameArg.String(), `"`)
log = log.Value("type", name)
log.Trace("Looking up list in policy")
fn, ok := m[name]
if !ok {
log.Error("No such list exists")
return nil, os.ErrNotExist
}
list, err := fn()
if err != nil {
log.Err(err).Error("Error retrieving list")
return nil, err
}
astList := make([]*ast.Term, 0, len(list))
for _, item := range list {
astList = append(astList, ast.StringTerm(item))
}
log.Tracef("Returning list with %d items", len(astList))
return ast.NewTerm(ast.NewArray(astList...)), nil
}
}

44
policy/handler.go Normal file
View File

@@ -0,0 +1,44 @@
package policy
import (
"net/http"
"git.maze.io/maze/styx/logger"
proxy "git.maze.io/maze/styx/proxy"
)
func NewRequestHandler(p *Policy) proxy.RequestHandler {
log := logger.StandardLog.Value("policy", p.name)
return proxy.RequestHandlerFunc(func(ctx proxy.Context) (*http.Request, *http.Response) {
input := NewInputFromRequest(ctx, ctx.Request())
input.logValues(log).Trace("Running request handler")
result, err := p.Query(input)
if err != nil {
log.Err(err).Error("Error evaulating policy")
return nil, nil
}
r, err := result.Response(ctx)
if err != nil {
log.Err(err).Error("Error generating response")
return nil, nil
}
return nil, r
})
}
func NewResponseHandler(p *Policy) proxy.ResponseHandler {
return proxy.ResponseHandlerFunc(func(ctx proxy.Context) *http.Response {
input := NewInputFromResponse(ctx, ctx.Response())
result, err := p.Query(input)
if err != nil {
logger.StandardLog.Err(err).Error("Error evaulating policy")
return nil
}
r, err := result.Response(ctx)
if err != nil {
logger.StandardLog.Err(err).Error("Error generating response")
return nil
}
return r
})
}

394
policy/input.go Normal file
View File

@@ -0,0 +1,394 @@
package policy
import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"net"
"net/http"
"net/url"
"strconv"
"git.maze.io/maze/styx/internal/netutil"
"git.maze.io/maze/styx/logger"
)
// Input represents the input to the policy query.
type Input struct {
Client *Client `json:"client"`
TLS *TLS `json:"tls"`
Request *Request `json:"request"`
Response *Response `json:"response"`
}
func (i *Input) logValues(log logger.Structured) logger.Structured {
log = i.Client.logValues(log)
log = i.TLS.logValues(log)
log = i.Request.logValues(log)
log = i.Response.logValues(log)
return log
}
func NewInputFromConn(c net.Conn) *Input {
if c == nil {
return new(Input)
}
return &Input{
Client: NewClientFromConn(c),
TLS: NewTLSFromConn(c),
}
}
func NewInputFromRequest(c net.Conn, r *http.Request) *Input {
if r == nil {
return nil
}
input := NewInputFromConn(c)
input.Request = NewRequest(r)
return input
}
func NewInputFromResponse(c net.Conn, r *http.Response) *Input {
if r == nil {
return nil
}
input := NewInputFromConn(c)
input.Response = NewResponse(r)
return input
}
type Client struct {
Network string `json:"network"`
IP string `json:"ip"`
Port int `json:"int"`
}
func (i *Client) logValues(log logger.Structured) logger.Structured {
if i != nil {
log = log.Values(logger.Values{
"client_network": i.Network,
"client_ip": i.IP,
"client_port": i.Port,
})
}
return log
}
func NewClient(network, address string) *Client {
if host, port, err := net.SplitHostPort(address); err == nil {
p, _ := net.LookupPort(network, port)
return &Client{
Network: network,
IP: host,
Port: p,
}
}
return &Client{
Network: network,
IP: address,
}
}
func NewClientFromConn(c net.Conn) *Client {
if c == nil {
return nil
}
return NewClientFromAddr(c.RemoteAddr())
}
func NewClientFromAddr(addr net.Addr) *Client {
switch addr := addr.(type) {
case *net.TCPAddr:
return &Client{
Network: addr.Network(),
IP: addr.IP.String(),
Port: addr.Port,
}
case *net.UDPAddr:
return &Client{
Network: addr.Network(),
IP: addr.IP.String(),
Port: addr.Port,
}
case *net.IPAddr:
return &Client{
Network: addr.Network(),
IP: addr.IP.String(),
}
default:
if host, port, err := net.SplitHostPort(addr.String()); err == nil {
return &Client{
Network: addr.Network(),
IP: host,
Port: func() int { p, _ := net.LookupPort(addr.Network(), port); return p }(),
}
}
return &Client{
Network: addr.Network(),
IP: addr.String(),
}
}
}
type TLS struct {
Version string `json:"version"`
CipherSuite string `json:"cipher_suite"`
ServerName string `json:"server_name"`
Certificates []*Certificate `json:"certificates"`
}
func (i *TLS) logValues(log logger.Structured) logger.Structured {
if i != nil {
cns := make([]string, len(i.Certificates))
for j, cert := range i.Certificates {
cns[j] = cert.Subject.CommonName
}
log = log.Values(logger.Values{
"tls_version": i.Version,
"tls_cipher": i.CipherSuite,
"tls_server_name": i.ServerName,
"tls_certificates": cns,
})
}
return log
}
func NewTLS(state *tls.ConnectionState) *TLS {
if state == nil {
return nil
}
tls := &TLS{
Version: tls.VersionName(state.Version),
CipherSuite: tls.CipherSuiteName(state.CipherSuite),
ServerName: state.ServerName,
}
for _, cert := range state.PeerCertificates {
if cert := NewCertificate(cert); cert != nil {
tls.Certificates = append(tls.Certificates, cert)
}
}
return tls
}
type tlsConnectionStater interface {
ConnectionState() tls.ConnectionState
}
func NewTLSFromConn(c net.Conn) *TLS {
if c == nil {
return nil
}
if s, ok := c.(tlsConnectionStater); ok {
cs := s.ConnectionState()
return NewTLS(&cs)
}
return nil
}
type Certificate struct {
SerialNumber string `json:"serial_number"`
Subject PKIXName `json:"subject"`
Issuer PKIXName `json:"issuer"`
NotBefore int64 `json:"not_before"`
NotAfter int64 `json:"not_after"`
}
func NewCertificate(cert *x509.Certificate) *Certificate {
if cert == nil {
return nil
}
return &Certificate{
SerialNumber: cert.SerialNumber.String(),
Subject: MakePKIXName(cert.Subject),
Issuer: MakePKIXName(cert.Issuer),
NotBefore: cert.NotBefore.UnixNano(),
NotAfter: cert.NotAfter.UnixNano(),
}
}
type PKIXName struct {
CommonName string `json:"cn,omitempty"`
Country string `json:"country,omitempty"`
Organization string `json:"organization,omitempty"`
OrganizationalUnit string `json:"ou,omitempty"`
Locality string `json:"locality,omitempty"`
Province string `json:"province,omitempty"`
StreetAddress string `json:"address,omitempty"`
PostalCode string `json:"postalcode,omitempty"`
}
func MakePKIXName(name pkix.Name) PKIXName {
return PKIXName{
CommonName: name.CommonName,
Country: pick(name.Country...),
Organization: pick(name.Organization...),
OrganizationalUnit: pick(name.OrganizationalUnit...),
Locality: pick(name.Locality...),
Province: pick(name.Province...),
StreetAddress: pick(name.StreetAddress...),
PostalCode: pick(name.PostalCode...),
}
}
// Request represents an HTTP request.
type Request struct {
Method string `json:"method"`
URL *URL `json:"url"`
Proto string `json:"proto"`
Header map[string]string `json:"header"`
Host string `json:"host"`
Port int `json:"port"`
RequestURI string `json:"request_uri"`
}
func (i *Request) logValues(log logger.Structured) logger.Structured {
if i != nil {
log = log.Values(logger.Values{
"request_method": i.Method,
"request_url": i.URL.String(),
"request_proto": i.Proto,
"request_header": i.Header,
"request_host": i.Host,
"request_port": i.Port,
})
}
return log
}
func NewRequest(r *http.Request) *Request {
if r == nil {
return nil
}
header := make(map[string]string)
for key := range r.Header {
header[key] = r.Header.Get(key)
}
host, portName, err := net.SplitHostPort(r.URL.Host)
if err != nil {
host = netutil.Host(r.URL.Host)
portName = "80"
if r.URL.Scheme == "https" || r.URL.Scheme == "wss" || r.TLS != nil {
portName = "443"
}
}
var port int
if port, err = strconv.Atoi(portName); err != nil {
port, _ = net.LookupPort("tcp", portName)
}
return &Request{
Method: r.Method,
URL: NewURL(r.URL),
Proto: r.Proto,
Header: header,
Host: host,
Port: port,
RequestURI: r.RequestURI,
}
}
// Response represents an HTTP response.
type Response struct {
Status string `json:"status"`
StatusCode int `json:"status_code"`
Proto string `json:"proto"`
Header map[string]string `json:"header"`
ContentLength int64 `json:"content_length"`
Close bool `json:"close"`
Request *Request `json:"request"`
TLS *TLS `json:"tls"`
}
func (i *Response) logValues(log logger.Structured) logger.Structured {
if i != nil {
log = log.Values(logger.Values{
"response_status": i.StatusCode,
"response_proto": i.Proto,
"response_header": i.Header,
"response_close": i.Close,
"response_tls": i.TLS != nil,
})
}
return log
}
func NewResponse(r *http.Response) *Response {
if r == nil {
return nil
}
header := make(map[string]string)
for key := range r.Header {
header[key] = r.Header.Get(key)
}
return &Response{
Status: r.Status,
StatusCode: r.StatusCode,
Proto: r.Proto,
Header: header,
ContentLength: r.ContentLength,
Close: r.Close,
Request: NewRequest(r.Request),
TLS: NewTLS(r.TLS),
}
}
type URL struct {
Scheme string `json:"scheme"`
Host string `json:"host"`
Path string `json:"path"`
Query map[string]string `json:"query"`
}
func (i *URL) String() string {
if i == nil {
return "<nil>"
}
s := fmt.Sprintf("%s://%s%s", i.Scheme, i.Host, i.Path)
if len(i.Query) > 0 {
s += "?"
for k, v := range i.Query {
s += k + "=" + url.QueryEscape(v)
}
}
return s
}
func ParseURL(rawurl string) (*URL, error) {
parsed, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
return NewURL(parsed), nil
}
func NewURL(url *url.URL) *URL {
if url == nil {
return nil
}
query := make(map[string]string)
for key, values := range url.Query() {
if len(values) > 0 {
query[key] = values[0]
}
}
return &URL{
Scheme: url.Scheme,
Host: url.Host,
Path: url.Path,
Query: query,
}
}
func pick(values ...string) string {
for _, v := range values {
if v != "" {
return v
}
}
return ""
}

189
policy/policy.go Normal file
View File

@@ -0,0 +1,189 @@
package policy
import (
"bytes"
"context"
"errors"
"fmt"
"html/template"
"io"
"net/http"
"os"
"github.com/go-viper/mapstructure/v2"
"github.com/open-policy-agent/opa/v1/rego"
regoprint "github.com/open-policy-agent/opa/v1/topdown/print"
"git.maze.io/maze/styx/logger"
proxy "git.maze.io/maze/styx/proxy"
)
const DefaultPackageName = "styx"
var ErrNoResult = errors.New("policy: no result")
type Policy struct {
name string
options []func(*rego.Rego)
}
func New(name, pkg string) (*Policy, error) {
p := &Policy{
name: name,
options: newRego(rego.Load([]string{name}, nil), pkg),
}
if _, err := p.Query(&Input{}); err != nil {
return nil, err
}
return p, nil
}
func NewFromString(module, pkg string) (*Policy, error) {
p := &Policy{
name: "<inline>",
options: newRego(rego.Module("styx", module), pkg),
}
if _, err := p.Query(&Input{}); err != nil {
return nil, err
}
return p, nil
}
func newRego(option func(*rego.Rego), pkg string) []func(*rego.Rego) {
if pkg == "" {
pkg = DefaultPackageName
}
return []func(*rego.Rego){
rego.Dump(os.Stderr),
rego.Query("data." + pkg),
rego.Strict(true),
rego.Function2(&rego.Function{
Name: "styx.in_domains",
Decl: domainContainsDecl,
Nondeterministic: true,
}, domainContainsImpl),
rego.Function2(&rego.Function{
Name: "styx.in_networks",
Decl: networkContainsDecl,
Nondeterministic: true,
}, networkContainsImpl),
rego.PrintHook(printHook{}),
option,
}
}
type printHook struct{}
func (printHook) Print(ctx regoprint.Context, message string) error {
logger.StandardLog.Values(logger.Values{
"where": fmt.Sprintf("%s:%d,%d", ctx.Location.File, ctx.Location.Row, ctx.Location.Col),
"from": string(ctx.Location.Text),
}).Debug(message)
return nil
}
type Result struct {
// Reject signals explicit rejection.
Reject int `json:"reject" mapstructure:"reject"`
// Permit signals explicit permission.
Permit *bool `json:"permit" mapstructure:"permit"`
// Redirect to this URL.
Redirect string `json:"redirect" mapstructure:"redirect"`
// Template to render as response body.
Template string `json:"template" mapstructure:"template"`
// Errors contains error messages.
Errors []string `json:"errors" mapstructure:"errors,omitempty"`
}
func (r *Result) Response(ctx proxy.Context) (*http.Response, error) {
for _, text := range r.Errors {
logger.StandardLog.Values(logger.Values{
"id": ctx.ID(),
"client": ctx.RemoteAddr().String(),
}).Err(errors.New(text)).Warn("Error from policy")
}
switch {
case r.Redirect != "":
response := proxy.NewResponse(http.StatusFound, nil, ctx.Request())
response.Header.Set("Server", "styx")
response.Header.Set(proxy.HeaderLocation, r.Redirect)
return response, nil
case r.Template != "":
b := new(bytes.Buffer)
t, err := template.New("policy").ParseFiles(r.Template)
if err != nil {
return nil, err
}
if err = t.Execute(b, map[string]any{"context": ctx}); err != nil {
return nil, err
}
response := proxy.NewResponse(http.StatusFound, io.NopCloser(b), ctx.Request())
response.Header.Set("Server", "styx")
response.Header.Set(proxy.HeaderContentType, "text/html")
return response, nil
case r.Reject > 0:
body := io.NopCloser(bytes.NewBufferString(http.StatusText(r.Reject)))
response := proxy.NewResponse(r.Reject, body, ctx.Request())
response.Header.Set(proxy.HeaderContentType, "text/plain")
return response, nil
case r.Permit != nil && !*r.Permit:
body := io.NopCloser(bytes.NewBufferString(http.StatusText(http.StatusForbidden)))
response := proxy.NewResponse(http.StatusForbidden, body, ctx.Request())
response.Header.Set(proxy.HeaderContentType, "text/plain")
return response, nil
default:
return nil, nil
}
}
func (p *Policy) Query(input *Input) (*Result, error) {
/*
e := json.NewEncoder(os.Stdout)
e.SetIndent("", " ")
e.Encode(doc)
*/
log := logger.StandardLog.Value("policy", p.name)
log.Trace("Evaluating policy")
r := rego.New(append(p.options, rego.Input(input))...)
ctx := context.Background()
/*
query, err := p.rego.PrepareForEval(ctx)
if err != nil {
return nil, err
}
rs, err := query.Eval(ctx, rego.EvalInput(input))
if err != nil {
return nil, err
}
*/
rs, err := r.Eval(ctx)
if err != nil {
return nil, err
}
if len(rs) == 0 || len(rs[0].Expressions) == 0 {
return nil, ErrNoResult
}
result := &Result{}
for _, expr := range rs[0].Expressions {
if m, ok := expr.Value.(map[string]any); ok {
log.Values(m).Trace("Policy result expression")
if err = mapstructure.Decode(m, result); err != nil {
return nil, err
}
}
}
return result, nil
}

View File

@@ -14,8 +14,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"git.maze.io/maze/styx/internal/netutil/arp" "git.maze.io/maze/styx/logger"
"github.com/sirupsen/logrus"
) )
// Context provides convenience functions for the current ongoing HTTP proxy transaction (request). // Context provides convenience functions for the current ongoing HTTP proxy transaction (request).
@@ -71,17 +70,16 @@ func (w *countingWriter) Write(p []byte) (n int, err error) {
type proxyContext struct { type proxyContext struct {
net.Conn net.Conn
id uint64 id uint64
mac net.HardwareAddr cr *countingReader
cr *countingReader br *bufio.Reader
br *bufio.Reader cw *countingWriter
cw *countingWriter transparent int
isTransparent bool transparentTLS bool
isTransparentTLS bool serverName string
serverName string req *http.Request
req *http.Request res *http.Response
res *http.Response idleTimeout time.Duration
idleTimeout time.Duration
} }
// NewContext returns an initialized context for the provided [net.Conn]. // NewContext returns an initialized context for the provided [net.Conn].
@@ -98,7 +96,6 @@ func NewContext(c net.Conn) Context {
return &proxyContext{ return &proxyContext{
Conn: c, Conn: c,
id: binary.BigEndian.Uint64(b), id: binary.BigEndian.Uint64(b),
mac: arp.Get(c.RemoteAddr()),
cr: cr, cr: cr,
br: bufio.NewReader(cr), br: bufio.NewReader(cr),
cw: cw, cw: cw,
@@ -106,26 +103,23 @@ func NewContext(c net.Conn) Context {
} }
} }
func (c *proxyContext) AccessLogEntry() *logrus.Entry { func (c *proxyContext) AccessLogEntry() logger.Structured {
var id [8]byte var id [8]byte
binary.BigEndian.PutUint64(id[:], c.id) binary.BigEndian.PutUint64(id[:], c.id)
entry := AccessLog.WithFields(logrus.Fields{ entry := AccessLog.Values(logger.Values{
"client": c.RemoteAddr().String(), "client": c.RemoteAddr().String(),
"server": c.LocalAddr().String(), "server": c.LocalAddr().String(),
"id": hex.EncodeToString(id[:]), "id": hex.EncodeToString(id[:]),
"bytes_rx": c.BytesRead(), "bytes_rx": c.BytesRead(),
"bytes_tx": c.BytesSent(), "bytes_tx": c.BytesSent(),
}) })
if c.mac != nil {
return entry.WithField("client_mac", c.mac.String())
}
return entry return entry
} }
func (c *proxyContext) LogEntry() *logrus.Entry { func (c *proxyContext) LogEntry() logger.Structured {
var id [8]byte var id [8]byte
binary.BigEndian.PutUint64(id[:], c.id) binary.BigEndian.PutUint64(id[:], c.id)
return ServerLog.WithFields(logrus.Fields{ return ServerLog.Values(logger.Values{
"client": c.RemoteAddr().String(), "client": c.RemoteAddr().String(),
"server": c.LocalAddr().String(), "server": c.LocalAddr().String(),
"id": hex.EncodeToString(id[:]), "id": hex.EncodeToString(id[:]),

View File

@@ -15,7 +15,12 @@ import (
"git.maze.io/maze/styx/internal/netutil" "git.maze.io/maze/styx/internal/netutil"
) )
// Dialer can make outbound connections to upstream servers.
type Dialer interface { type Dialer interface {
// DialContext makes a new connection to the address specified in the [http.Request].
//
// The [http.Request] contains the URL scheme (http, https, ws, wss) and host (with optional port)
// to connect to. The [context.Context] may be used for cancellation and timeouts.
DialContext(context.Context, *http.Request) (net.Conn, error) DialContext(context.Context, *http.Request) (net.Conn, error)
} }
@@ -71,25 +76,38 @@ func (defaultDialer) DialContext(ctx context.Context, req *http.Request) (net.Co
} }
} }
// ConnFilter is called when a new connection has been accepted by the proxy. // ErrorHandler can handle errors that occur during proxying.
type ConnFilter interface { type ErrorHandler interface {
FilterConn(Context) (net.Conn, error) // HandleError handles an error that occurred during proxying. If the method returns a non-nil
// [http.Response], it will be sent to the client as-is. If it returns nil, a generic HTTP 502
// Bad Gateway response will be sent to the client.
//
// The [Context] may be inspected to obtain information about the request that caused the error.
// However, the [Context.Request] and [Context.Response] may be nil depending on when the error
// occurred.
HandleError(Context, error) *http.Response
} }
// ConnFilterFunc is a function that implements the [ConnFilter] interface. // ConnHandler is called when a new connection has been accepted by the proxy.
type ConnFilterFunc func(Context) (net.Conn, error) type ConnHandler interface {
HandleConn(Context) (net.Conn, error)
}
func (f ConnFilterFunc) FilterConn(ctx Context) (net.Conn, error) { // ConnHandlerFunc is a function that implements the [ConnHandler] interface.
type ConnHandlerFunc func(Context) (net.Conn, error)
func (f ConnHandlerFunc) HandleConn(ctx Context) (net.Conn, error) {
return f(ctx) return f(ctx)
} }
// TLS starts a TLS handshake on the accepted connection. // TLS starts a TLS handshake on the accepted connection.
func TLS(certs []tls.Certificate) ConnFilter { func TLS(config *tls.Config) ConnHandler {
return ConnFilterFunc(func(ctx Context) (net.Conn, error) { if config == nil {
s := tls.Server(ctx, &tls.Config{ config = new(tls.Config)
Certificates: certs, }
NextProtos: []string{"http/1.1"}, config.NextProtos = []string{"http/1.1"}
}) return ConnHandlerFunc(func(ctx Context) (net.Conn, error) {
s := tls.Server(ctx, config)
if err := s.Handshake(); err != nil { if err := s.Handshake(); err != nil {
return nil, err return nil, err
} }
@@ -98,8 +116,8 @@ func TLS(certs []tls.Certificate) ConnFilter {
} }
// TLSInterceptor can generate certificates on-the-fly for clients that use a compatible TLS version. // TLSInterceptor can generate certificates on-the-fly for clients that use a compatible TLS version.
func TLSInterceptor(ca ca.CertificateAuthority) ConnFilter { func TLSInterceptor(ca ca.CertificateAuthority) ConnHandler {
return ConnFilterFunc(func(ctx Context) (net.Conn, error) { return ConnHandlerFunc(func(ctx Context) (net.Conn, error) {
s := tls.Server(ctx, &tls.Config{ s := tls.Server(ctx, &tls.Config{
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
ips := []net.IP{net.ParseIP(netutil.Host(ctx.RemoteAddr().String()))} ips := []net.IP{net.ParseIP(netutil.Host(ctx.RemoteAddr().String()))}
@@ -119,26 +137,27 @@ func TLSInterceptor(ca ca.CertificateAuthority) ConnFilter {
// When a new [net.Conn] is made, this function will inspect the initial request packet for a // When a new [net.Conn] is made, this function will inspect the initial request packet for a
// TLS handshake. If a TLS handshake is detected, the connection will make a feaux HTTP CONNECT // TLS handshake. If a TLS handshake is detected, the connection will make a feaux HTTP CONNECT
// request using TLS, if no handshake is detected, it will make a feaux plain HTTP CONNECT request. // request using TLS, if no handshake is detected, it will make a feaux plain HTTP CONNECT request.
func Transparent() ConnFilter { func Transparent(port int) ConnHandler {
return ConnFilterFunc(func(nctx Context) (net.Conn, error) { return ConnHandlerFunc(func(nctx Context) (net.Conn, error) {
ctx, ok := nctx.(*proxyContext) ctx, ok := nctx.(*proxyContext)
if !ok { if !ok {
return nctx, nil return nctx, nil
} }
b := new(bytes.Buffer) b := new(bytes.Buffer)
hello, err := cryptutil.ReadClientHello(io.TeeReader(ctx, b)) hello, err := cryptutil.ReadClientHello(io.TeeReader(netutil.ReadOnlyConn{Reader: ctx.br}, b))
if err != nil { if err != nil {
if _, ok := err.(tls.RecordHeaderError); !ok { if _, ok := err.(tls.RecordHeaderError); !ok {
ctx.LogEntry().WithError(err).WithField("error_type", fmt.Sprintf("%T", err)).Warn("TLS sniff error") ctx.LogEntry().Err(err).Value("error_type", fmt.Sprintf("%T", err)).Warn("TLS sniff error")
return nil, err return nil, err
} }
// Not a TLS connection, moving on to regular HTTP request handling... // Not a TLS connection, moving on to regular HTTP request handling...
ctx.LogEntry().Debug("HTTP connection on transparent port") ctx.LogEntry().Debug("HTTP connection on transparent port")
ctx.isTransparent = true ctx.transparent = port
} else { } else {
ctx.LogEntry().WithField("target", hello.ServerName).Debug("TLS connection on transparent port") ctx.LogEntry().Value("target", hello.ServerName).Debug("TLS connection on transparent port")
ctx.isTransparentTLS = true ctx.transparent = port
ctx.transparentTLS = true
ctx.serverName = hello.ServerName ctx.serverName = hello.ServerName
} }
@@ -149,10 +168,10 @@ func Transparent() ConnFilter {
}) })
} }
// RequestFilter can filter HTTP requests coming to the proxy. // RequestHandler can filter HTTP requests coming to the proxy.
type RequestFilter interface { type RequestHandler interface {
// FilterRequest filters a HTTP request made to the proxy. The current request may be obtained // HandlerRequest filters a HTTP request made to the proxy. The current request may be obtained
// from [Context.Request]. If a previous RequestFilter provided a HTTP response, it is available // from [Context.Request]. If a previous RequestHandler provided a HTTP response, it is available
// from [Context.Response]. // from [Context.Response].
// //
// Modifications to the current request can be made to the Request returned by [Context.Request] // Modifications to the current request can be made to the Request returned by [Context.Request]
@@ -160,35 +179,35 @@ type RequestFilter interface {
// //
// If the filter returns a non-nil [http.Response], then the [Request] will not be proxied to // If the filter returns a non-nil [http.Response], then the [Request] will not be proxied to
// any upstream server(s). // any upstream server(s).
FilterRequest(Context) (*http.Request, *http.Response) HandleRequest(Context) (*http.Request, *http.Response)
} }
// RequestFilterFunc is a function that implements the [RequestFilter] interface. // RequestHandlerFunc is a function that implements the [RequestHandler] interface.
type RequestFilterFunc func(Context) (*http.Request, *http.Response) type RequestHandlerFunc func(Context) (*http.Request, *http.Response)
func (f RequestFilterFunc) FilterRequest(ctx Context) (*http.Request, *http.Response) { func (f RequestHandlerFunc) HandleRequest(ctx Context) (*http.Request, *http.Response) {
return f(ctx) return f(ctx)
} }
// ResponseFilter can filter HTTP responses coming from the proxy. // ResponseHandler can filter HTTP responses coming from the proxy.
type ResponseFilter interface { type ResponseHandler interface {
// FilterResponse filters a HTTP response coming from the proxy. The current response may be // HandlerResponse filters a HTTP response coming from the proxy. The current response may be
// obtained from [Context.Response]. // obtained from [Context.Response].
// //
// Modifications to the current response can be made to the [Response] returned by [Context.Response]. // Modifications to the current response can be made to the [Response] returned by [Context.Response].
FilterResponse(Context) *http.Response HandleResponse(Context) *http.Response
} }
// ResponseFilterFunc is a function that implements the [ResponseFilter] interface. // ResponseHandlerFunc is a function that implements the [ResponseHandler] interface.
type ResponseFilterFunc func(Context) *http.Response type ResponseHandlerFunc func(Context) *http.Response
func (f ResponseFilterFunc) FilterResponse(ctx Context) *http.Response { func (f ResponseHandlerFunc) HandleResponse(ctx Context) *http.Response {
return f(ctx) return f(ctx)
} }
// CleanRequestProxyHeaders removes all headers added by downstream proxies from the [http.Request]. // CleanRequestProxyHeaders removes all headers added by downstream proxies from the [http.Request].
func CleanRequestProxyHeaders() RequestFilter { func CleanRequestProxyHeaders() RequestHandler {
return RequestFilterFunc(func(ctx Context) (*http.Request, *http.Response) { return RequestHandlerFunc(func(ctx Context) (*http.Request, *http.Response) {
if req := ctx.Request(); req != nil { if req := ctx.Request(); req != nil {
cleanProxyHeaders(req.Header) cleanProxyHeaders(req.Header)
} }
@@ -197,8 +216,8 @@ func CleanRequestProxyHeaders() RequestFilter {
} }
// CleanRequestProxyHeaders removes all headers for upstream proxies from the [http.Response]. // CleanRequestProxyHeaders removes all headers for upstream proxies from the [http.Response].
func CleanResponseProxyHeaders() ResponseFilter { func CleanResponseProxyHeaders() ResponseHandler {
return ResponseFilterFunc(func(ctx Context) *http.Response { return ResponseHandlerFunc(func(ctx Context) *http.Response {
if res := ctx.Response(); res != nil { if res := ctx.Response(); res != nil {
cleanProxyHeaders(res.Header) cleanProxyHeaders(res.Header)
} }
@@ -208,8 +227,8 @@ func CleanResponseProxyHeaders() ResponseFilter {
// AddRequestHeaders adds headers to the [http.Request]. Any existing headers with the same // AddRequestHeaders adds headers to the [http.Request]. Any existing headers with the same
// key will remain intact. // key will remain intact.
func AddRequestHeaders(h http.Header) RequestFilter { func AddRequestHeaders(h http.Header) RequestHandler {
return RequestFilterFunc(func(ctx Context) (*http.Request, *http.Response) { return RequestHandlerFunc(func(ctx Context) (*http.Request, *http.Response) {
if req := ctx.Request(); req != nil { if req := ctx.Request(); req != nil {
if req.Header == nil { if req.Header == nil {
req.Header = make(http.Header) req.Header = make(http.Header)
@@ -222,8 +241,8 @@ func AddRequestHeaders(h http.Header) RequestFilter {
// SetRequestHeaders sets headers to the [http.Request]. Any existing headers with the same // SetRequestHeaders sets headers to the [http.Request]. Any existing headers with the same
// key will be removed. // key will be removed.
func SetRequestHeaders(h http.Header) RequestFilter { func SetRequestHeaders(h http.Header) RequestHandler {
return RequestFilterFunc(func(ctx Context) (*http.Request, *http.Response) { return RequestHandlerFunc(func(ctx Context) (*http.Request, *http.Response) {
if req := ctx.Request(); req != nil { if req := ctx.Request(); req != nil {
if req.Header == nil { if req.Header == nil {
req.Header = make(http.Header) req.Header = make(http.Header)
@@ -236,8 +255,8 @@ func SetRequestHeaders(h http.Header) RequestFilter {
// AddResponseHeaders adds headers to the [http.Response]. Any existing headers with the same // AddResponseHeaders adds headers to the [http.Response]. Any existing headers with the same
// key will remain intact. // key will remain intact.
func AddResponseHeaders(h http.Header) ResponseFilter { func AddResponseHeaders(h http.Header) ResponseHandler {
return ResponseFilterFunc(func(ctx Context) *http.Response { return ResponseHandlerFunc(func(ctx Context) *http.Response {
if res := ctx.Response(); res != nil { if res := ctx.Response(); res != nil {
if res.Header == nil { if res.Header == nil {
res.Header = make(http.Header) res.Header = make(http.Header)
@@ -250,8 +269,8 @@ func AddResponseHeaders(h http.Header) ResponseFilter {
// SetResponseHeaders sets headers to the [http.Response]. Any existing headers with the same // SetResponseHeaders sets headers to the [http.Response]. Any existing headers with the same
// key will be removed. // key will be removed.
func SetResponseHeaders(h http.Header) ResponseFilter { func SetResponseHeaders(h http.Header) ResponseHandler {
return ResponseFilterFunc(func(ctx Context) *http.Response { return ResponseHandlerFunc(func(ctx Context) *http.Response {
if res := ctx.Response(); res != nil { if res := ctx.Response(); res != nil {
if res.Header == nil { if res.Header == nil {
res.Header = make(http.Header) res.Header = make(http.Header)

View File

@@ -13,13 +13,14 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"slices" "slices"
"strconv"
"strings" "strings"
"syscall" "syscall"
"time" "time"
"git.maze.io/maze/styx/internal/netutil" "git.maze.io/maze/styx/internal/netutil"
"git.maze.io/maze/styx/logger"
"git.maze.io/maze/styx/stats" "git.maze.io/maze/styx/stats"
"github.com/sirupsen/logrus"
) )
// Common HTTP headers. // Common HTTP headers.
@@ -32,6 +33,7 @@ const (
HeaderForwardedHost = "X-Forwarded-Host" HeaderForwardedHost = "X-Forwarded-Host"
HeaderForwardedPort = "X-Forwarded-Port" HeaderForwardedPort = "X-Forwarded-Port"
HeaderForwardedProto = "X-Forwarded-Proto" HeaderForwardedProto = "X-Forwarded-Proto"
HeaderLocation = "Location"
HeaderRealIP = "X-Real-Ip" HeaderRealIP = "X-Real-Ip"
HeaderUpgrade = "Upgrade" HeaderUpgrade = "Upgrade"
HeaderVia = "Via" HeaderVia = "Via"
@@ -46,43 +48,111 @@ const (
var ( var (
// AccessLog is used for logging requests to the proxy. // AccessLog is used for logging requests to the proxy.
AccessLog = logrus.StandardLogger() AccessLog = logger.Get()
// ServerLog is used for logging server log messages. // ServerLog is used for logging server log messages.
ServerLog = logrus.StandardLogger() ServerLog = logger.Get()
) )
// Proxy implements a HTTP(S) proxy. // Proxy implements a HTTP(S) proxy.
type Proxy struct { type Proxy struct {
rt http.RoundTripper // RoundTripper is used to make outbound HTTP requests. It defaults to a [http.Transport]
dialer map[string]Dialer // with a custom DialContext that uses the configured [Dialer]s.
connFilter []ConnFilter //
requestFilter []RequestFilter // Only override this if you know what you are doing.
responseFilter []ResponseFilter RoundTripper http.RoundTripper
dialTimeout time.Duration
idleTimeout time.Duration // Dialer is a map of protocol names to [Dialer] implementations. The default [Dialer]
webSocketIdleTimeout time.Duration // corresponds to an empty string key.
mux *http.ServeMux //
// Only override the default [Dialer] if you know what you are doing.
Dialer map[string]Dialer
// OnConnect is a list of connection filters that are applied in order when a new
// connection is established.
//
// Connection filters can be used to implement custom authentication, logging,
// rate limiting, etc.
//
// Connection filters are applied before any HTTP request is read from the connection.
//
// Connection filters should return a non-nil error if they want to terminate the
// connection. Returning a non-nil [net.Conn] will replace the existing connection
// with the returned one.
//
// Connection filters should not modify the connection in any way (e.g. wrapping it
// in a TLS connection) as this will interfere with the proxy's ability to read
// HTTP requests from the connection.
//
// Connection filters are executed sequentially in the order they are added.
OnConnect []ConnHandler
// OnRequest is a list of request filters that are applied in order when a new
// HTTP request is read from the connection.
//
// Request filters can be used to modify the request, or to return a response
// directly without forwarding the request to the upstream server.
//
// Request filters should return a non-nil error if they want to terminate the
// connection.
//
// Request filters are executed sequentially in the order they are added.
OnRequest []RequestHandler
// OnResponse is a list of response filters that are applied in order when a
// response is received from the upstream server.
//
// Response filters can be used to modify the response before it is sent to
// the client.
//
// Response filters should return a non-nil error if they want to terminate the
// connection.
//
// Response filters are executed sequentially in the order they are added.
OnResponse []ResponseHandler
// OnError is a list of error handlers that are applied in order when an
// error occurs during request processing.
//
// Error handlers can be used to log errors, or to return a custom response
// to the client.
//
// Error handlers should return a non-nil error if they want to terminate the
// connection.
//
// Error handlers are executed sequentially in the order they are added.
OnError []ErrorHandler
// DialTimeout is the timeout for establishing new connections to upstream servers.
DialTimeout time.Duration
// IdleTimeout is the timeout for idle connections.
IdleTimeout time.Duration
// WebSocketIdleTimeout is the timeout for idle WebSocket connections.
WebSocketIdleTimeout time.Duration
mux *http.ServeMux
} }
// New [Proxy] with somewhat sane defaults. // New [Proxy] with somewhat sane defaults.
func New() *Proxy { func New() *Proxy {
p := &Proxy{ p := &Proxy{
dialer: map[string]Dialer{"": defaultDialer{}}, Dialer: map[string]Dialer{"": defaultDialer{}},
dialTimeout: DefaultDialTimeout, DialTimeout: DefaultDialTimeout,
idleTimeout: DefaultIdleTimeout, IdleTimeout: DefaultIdleTimeout,
webSocketIdleTimeout: DefaultWebSocketIdleTimeout, WebSocketIdleTimeout: DefaultWebSocketIdleTimeout,
mux: http.NewServeMux(), mux: http.NewServeMux(),
} }
// Make sure the roundtripper uses our dialers. // Make sure the roundtripper uses our dialers.
p.rt = &http.Transport{ p.RoundTripper = &http.Transport{
TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper), TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper),
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: time.Second, ExpectContinueTimeout: time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return p.dialer[""].DialContext(ctx, &http.Request{ return p.Dialer[""].DialContext(ctx, &http.Request{
URL: &url.URL{ URL: &url.URL{
Scheme: network, Scheme: network,
Host: addr, Host: addr,
@@ -112,41 +182,17 @@ func (p *Proxy) HandleFunc(pattern string, handler func(http.ResponseWriter, *ht
func (p *Proxy) SetDialer(proto string, dialer Dialer) { func (p *Proxy) SetDialer(proto string, dialer Dialer) {
if dialer == nil { if dialer == nil {
if proto != "" { if proto != "" {
delete(p.dialer, proto) delete(p.Dialer, proto)
} }
} else { } else {
p.dialer[proto] = dialer p.Dialer[proto] = dialer
} }
} }
// AddConnFilter adds a connection filter to the stack.
func (p *Proxy) AddConnFilter(f ConnFilter) {
if f == nil {
return
}
p.connFilter = append(p.connFilter, f)
}
// AddRequestFilter adds a request filter to the stack.
func (p *Proxy) AddRequestFilter(f RequestFilter) {
if f == nil {
return
}
p.requestFilter = append(p.requestFilter, f)
}
// AddResponseFilter adds a response filter to the stack.
func (p *Proxy) AddResponseFilter(f ResponseFilter) {
if f == nil {
return
}
p.responseFilter = append(p.responseFilter, f)
}
func (p *Proxy) dial(ctx context.Context, req *http.Request) (net.Conn, error) { func (p *Proxy) dial(ctx context.Context, req *http.Request) (net.Conn, error) {
d, ok := p.dialer[req.URL.Scheme] d, ok := p.Dialer[req.URL.Scheme]
if !ok { if !ok {
d = p.dialer[""] d = p.Dialer[""]
} }
return d.DialContext(ctx, req) return d.DialContext(ctx, req)
@@ -170,23 +216,32 @@ func (p *Proxy) handle(nc net.Conn) {
err error err error
) )
defer func() { defer func() {
if cerr := ctx.Close(); cerr != nil && err == nil { if r := recover(); r != nil {
if err, ok := r.(error); ok {
ctx.LogEntry().Err(err).Warn("Bug in code, recovered from panic!")
}
_ = nc.Close()
}
}()
defer func() {
if cerr := ctx.Close(); cerr != nil && err == nil && !netutil.IsClosing(cerr) {
err = cerr err = cerr
} }
log := ctx.AccessLogEntry().WithField("duration", time.Since(start)) log := ctx.AccessLogEntry().Value("duration", time.Since(start))
if err != nil && !netutil.IsClosing(err) { if err != nil && !netutil.IsClosing(err) {
log = log.WithError(err) log = log.Err(err)
} }
if req := ctx.Request(); req != nil { if req := ctx.Request(); req != nil {
log = log.WithFields(logrus.Fields{ log = log.Values(logger.Values{
"method": req.Method, "method": req.Method,
"request": req.URL.String(), "request": req.URL.String(),
}) })
} }
if res := ctx.Response(); res != nil { if res := ctx.Response(); res != nil {
//countStatus(res.StatusCode) //countStatus(res.StatusCode)
log.WithFields(logrus.Fields{ log.Values(logger.Values{
"response": res.StatusCode, "response": res.StatusCode,
}).Info(res.Status) }).Info(res.Status)
} else { } else {
@@ -196,38 +251,42 @@ func (p *Proxy) handle(nc net.Conn) {
}() }()
// Propagate timeouts // Propagate timeouts
ctx.SetIdleTimeout(p.idleTimeout) ctx.SetIdleTimeout(p.IdleTimeout)
for _, f := range p.connFilter { for _, f := range p.OnConnect {
fc, err := f.FilterConn(ctx) fc, err := f.HandleConn(ctx)
if err != nil { if err != nil {
ServerLog.WithField("filter", fmt.Sprintf("%T", f)).WithError(err).Warn("error in conn filter") ServerLog.Value("filter", fmt.Sprintf("%T", f)).Err(err).Warn("Error in conn filter")
p.handleError(ctx, err, true)
_ = nc.Close() _ = nc.Close()
return return
} else if fc != nil { } else if fc != nil {
ServerLog.WithField("filter", fmt.Sprintf("%T", f)).Debug("replacing connection from filter") ServerLog.Value("filter", fmt.Sprintf("%T", f)).Debug("Replacing connection from filter")
ctx.Conn = fc ctx.Conn = fc
ctx.br = bufio.NewReader(fc) ctx.br = bufio.NewReader(fc)
} }
} }
for { for {
if ctx.isTransparentTLS { if ctx.transparentTLS {
ctx.req = &http.Request{ ctx.req = &http.Request{
Method: http.MethodConnect, Method: http.MethodConnect,
URL: &url.URL{ URL: &url.URL{Scheme: "tcp", Host: net.JoinHostPort(ctx.serverName, strconv.Itoa(ctx.transparent))},
Scheme: "tcp", Host: net.JoinHostPort(ctx.serverName, strconv.Itoa(ctx.transparent)),
Host: net.JoinHostPort(ctx.serverName, "443"), Proto: "HTTP/1.1",
}, ProtoMajor: 1,
ProtoMinor: 1,
Close: true,
} }
} else if ctx.req, err = http.ReadRequest(ctx.Reader()); err != nil { } else if ctx.req, err = http.ReadRequest(ctx.Reader()); err != nil {
if !(errors.Is(err, io.EOF) || errors.Is(err, syscall.ECONNRESET)) { if !(errors.Is(err, io.EOF) || errors.Is(err, syscall.ECONNRESET)) {
ServerLog.WithError(err).Debug("error reading request") ServerLog.Err(err).Debug("Error reading request")
} }
p.handleError(ctx, err, true)
return return
} }
if ctx.isTransparent { if ctx.transparent > 0 {
// Canonicallize to absolute URL // Canonicallize to absolute URL
if ctx.req.URL.Host == "" { if ctx.req.URL.Host == "" {
ctx.req.URL.Host = ctx.req.Host ctx.req.URL.Host = ctx.req.Host
@@ -235,47 +294,68 @@ func (p *Proxy) handle(nc net.Conn) {
if ctx.req.URL.Scheme == "" { if ctx.req.URL.Scheme == "" {
ctx.req.URL.Scheme = "http" ctx.req.URL.Scheme = "http"
} }
ctx.isTransparent = false ctx.transparent = 0
} }
for _, f := range p.requestFilter { for _, f := range p.OnRequest {
newReq, newRes := f.FilterRequest(ctx) newReq, newRes := f.HandleRequest(ctx)
if newReq != nil { if newReq != nil {
ServerLog.WithFields(logrus.Fields{ ServerLog.Values(logger.Values{
"filter": fmt.Sprintf("%T", f), "filter": fmt.Sprintf("%T", f),
"old_method": ctx.req.Method, "old_method": ctx.req.Method,
"old_url": ctx.req.URL, "old_url": ctx.req.URL,
"new_method": newReq.Method, "new_method": newReq.Method,
"new_url": newReq.URL, "new_url": newReq.URL,
}).Debug("replacing request from filter") }).Debug("Replacing request from filter")
ctx.req = newReq ctx.req = newReq
} }
if newRes != nil { if newRes != nil {
log := ServerLog.WithFields(logrus.Fields{ log := ServerLog.Values(logger.Values{
"filter": fmt.Sprintf("%T", f), "filter": fmt.Sprintf("%T", f),
"response": newRes.StatusCode, "response": newRes.StatusCode,
"status": newRes.Status, "status": newRes.Status,
}) })
log.Debug("replacing response from filter") log.Debug("Replacing response from filter")
ctx.res = newRes ctx.res = newRes
if err = p.writeResponse(ctx); err != nil { if err = p.writeResponse(ctx); err != nil {
log.WithError(err).Warn("error overriding repsonse") if netutil.IsClosing(err) {
return
}
log.Err(err).Warn("Error overriding repsonse")
} }
continue continue
} }
} }
if err = p.handleRequest(ctx); err != nil { if err = p.handleRequest(ctx); err != nil {
p.handleError(ctx, err, true)
return return
} }
// Only once // Only once
if ctx.isTransparent || ctx.isTransparentTLS { if ctx.transparent > 0 || ctx.transparentTLS || ctx.req.Method == http.MethodConnect {
return return
} }
} }
} }
func (p *Proxy) handleError(ctx *proxyContext, err error, sendResponse bool) {
res := ctx.Response()
if res == nil && sendResponse {
res = NewErrorResponse(err, ctx.Request())
}
for _, f := range p.OnError {
if newRes := f.HandleError(ctx, err); newRes != nil {
res = newRes
}
}
if sendResponse && res != nil {
if werr := p.writeResponse(ctx); werr != nil && !netutil.IsClosing(err) {
ServerLog.Err(werr).Warn("Error writing error response")
}
}
}
func (p *Proxy) handleRequest(ctx *proxyContext) (err error) { func (p *Proxy) handleRequest(ctx *proxyContext) (err error) {
switch { switch {
case ctx.req == nil: case ctx.req == nil:
@@ -300,9 +380,9 @@ func (p *Proxy) handleRequest(ctx *proxyContext) (err error) {
} }
} }
func (p *Proxy) applyResponseFilter(ctx *proxyContext) { func (p *Proxy) applyResponseHandler(ctx *proxyContext) {
for _, f := range p.responseFilter { for _, f := range p.OnResponse {
if newRes := f.FilterResponse(ctx); newRes != nil { if newRes := f.HandleResponse(ctx); newRes != nil {
if ctx.res.Body != nil { if ctx.res.Body != nil {
_ = ctx.res.Body.Close() _ = ctx.res.Body.Close()
} }
@@ -346,7 +426,7 @@ func (p *Proxy) serveConnect(ctx *proxyContext) (err error) {
// Most browsers expect to get a 200 OK after firing a HTTP CONNECT request; if the upstream // Most browsers expect to get a 200 OK after firing a HTTP CONNECT request; if the upstream
// encounters any errors, we'll inform the client after reading the HTTP request that follows. // encounters any errors, we'll inform the client after reading the HTTP request that follows.
if !(ctx.isTransparent || ctx.isTransparentTLS) { if !(ctx.transparent > 0 || ctx.transparentTLS) {
if _, err = io.WriteString(ctx, "HTTP/1.1 200 Connection Established\r\n\r\n"); err != nil { if _, err = io.WriteString(ctx, "HTTP/1.1 200 Connection Established\r\n\r\n"); err != nil {
return return
} }
@@ -356,10 +436,10 @@ func (p *Proxy) serveConnect(ctx *proxyContext) (err error) {
case "": case "":
ctx.req.URL.Scheme = "tcp" ctx.req.URL.Scheme = "tcp"
} }
log.WithField("target", ctx.req.URL.String()).Debug("http CONNECT request") log.Value("target", ctx.req.URL.String()).Debugf("%s CONNECT request", ctx.req.Proto)
var ( var (
timeout, cancel = context.WithTimeout(context.Background(), p.dialTimeout) timeout, cancel = context.WithTimeout(context.Background(), p.DialTimeout)
c net.Conn c net.Conn
) )
if c, err = p.dial(timeout, ctx.req); err != nil { if c, err = p.dial(timeout, ctx.req); err != nil {
@@ -373,27 +453,27 @@ func (p *Proxy) serveConnect(ctx *proxyContext) (err error) {
ctx.res = NewResponse(http.StatusOK, nil, ctx.req) ctx.res = NewResponse(http.StatusOK, nil, ctx.req)
srv := NewContext(c).(*proxyContext) srv := NewContext(c).(*proxyContext)
srv.SetIdleTimeout(p.idleTimeout) srv.SetIdleTimeout(p.IdleTimeout)
return p.multiplex(ctx, srv) return p.multiplex(ctx, srv)
} }
func (p *Proxy) serveForward(ctx *proxyContext) (err error) { func (p *Proxy) serveForward(ctx *proxyContext) (err error) {
log := ctx.LogEntry() log := ctx.LogEntry()
log.WithField("target", ctx.req.URL.String()).Debug("http forward request") log.Value("target", ctx.req.URL.String()).Debugf("%s forward request", ctx.req.Proto)
if ctx.res, err = p.rt.RoundTrip(ctx.req); err != nil { if ctx.res, err = p.RoundTripper.RoundTrip(ctx.req); err != nil {
// log.Printf("%s forward request error: %v", ctx, err) // log.Printf("%s forward request error: %v", ctx, err)
ctx.res = NewErrorResponse(err, ctx.req) ctx.res = NewErrorResponse(err, ctx.req)
_ = p.writeResponse(ctx) _ = p.writeResponse(ctx)
_ = ctx.Close() _ = ctx.Close()
return fmt.Errorf("proxy: forward %s error: %w", ctx.req.URL, err) return fmt.Errorf("proxy: forward %s error: %w", ctx.req.URL, err)
} }
p.applyResponseFilter(ctx) p.applyResponseHandler(ctx)
return p.writeResponse(ctx) return p.writeResponse(ctx)
} }
func (p *Proxy) serveWebSocket(ctx *proxyContext) (err error) { func (p *Proxy) serveWebSocket(ctx *proxyContext) (err error) {
log := ctx.LogEntry().WithField("target", ctx.req.URL.String()) log := ctx.LogEntry().Value("target", ctx.req.URL.String())
switch ctx.req.URL.Scheme { switch ctx.req.URL.Scheme {
case "http": case "http":
@@ -402,9 +482,9 @@ func (p *Proxy) serveWebSocket(ctx *proxyContext) (err error) {
ctx.req.URL.Scheme = "wss" ctx.req.URL.Scheme = "wss"
} }
log.Debug("http websocket request") log.Debugf("%s websocket request", ctx.req.Proto)
var ( var (
timeout, cancel = context.WithTimeout(context.Background(), p.dialTimeout) timeout, cancel = context.WithTimeout(context.Background(), p.DialTimeout)
c net.Conn c net.Conn
) )
if c, err = p.dial(timeout, ctx.req); err != nil { if c, err = p.dial(timeout, ctx.req); err != nil {
@@ -417,7 +497,7 @@ func (p *Proxy) serveWebSocket(ctx *proxyContext) (err error) {
cancel() cancel()
srv := NewContext(c).(*proxyContext) srv := NewContext(c).(*proxyContext)
srv.SetIdleTimeout(p.idleTimeout) srv.SetIdleTimeout(p.IdleTimeout)
if err = ctx.req.Write(srv); err != nil { if err = ctx.req.Write(srv); err != nil {
ctx.res = NewErrorResponse(err, ctx.req) ctx.res = NewErrorResponse(err, ctx.req)
_ = p.writeResponse(ctx) _ = p.writeResponse(ctx)
@@ -432,15 +512,15 @@ func (p *Proxy) serveWebSocket(ctx *proxyContext) (err error) {
return fmt.Errorf("proxy: failed to read response from upstream: %w", err) return fmt.Errorf("proxy: failed to read response from upstream: %w", err)
} }
log.WithFields(logrus.Fields{ log.Values(logger.Values{
"response": ctx.res.StatusCode, "response": ctx.res.StatusCode,
"status": ctx.res.Status, "status": ctx.res.Status,
}).Debug("websocket response from upstream") }).Debug("WebSocket response from upstream")
if err = p.writeResponse(ctx); err != nil { if err = p.writeResponse(ctx); err != nil {
_ = ctx.Close() _ = ctx.Close()
return return
} }
ctx.SetIdleTimeout(p.webSocketIdleTimeout) ctx.SetIdleTimeout(p.WebSocketIdleTimeout)
return p.multiplex(ctx, srv) return p.multiplex(ctx, srv)
} }
@@ -471,19 +551,19 @@ func (p *Proxy) multiplex(ctx, srv Context) (err error) {
func (p *Proxy) writeResponse(ctx *proxyContext) (err error) { func (p *Proxy) writeResponse(ctx *proxyContext) (err error) {
res := ctx.Response() res := ctx.Response()
for _, f := range p.responseFilter { for _, f := range p.OnResponse {
if newRes := f.FilterResponse(ctx); newRes != nil { if newRes := f.HandleResponse(ctx); newRes != nil {
log.Printf("filter returned response HTTP %s", newRes.Status) log.Printf("Filter returned response HTTP %s", newRes.Status)
if res.Body != nil { if res.Body != nil {
_ = res.Body.Close() _ = res.Body.Close()
} }
res = newRes res = newRes
} }
} }
ServerLog.WithFields(logrus.Fields{ ServerLog.Values(logger.Values{
"close": res.Close, "close": res.Close,
"header": res.Header, "header": res.Header,
}).Debug("writing response") }).Debug("Writing response")
if err = res.Write(ctx); err != nil { if err = res.Write(ctx); err != nil {
return return
} }

142
styx.hcl
View File

@@ -1,7 +1,26 @@
proxy { proxy {
# TCP listen address # TCP listen address
listen = ":3128" port ":3128" {}
port ":3129" {
tls {
ca = "testdata/ca.crt"
cert = "testdata/ca.crt"
key = "testdata/ca.key"
}
# Transparent proxy for targets on port 80
transparent = 80
}
port ":3130" {
tls {
cert = "testdata/ca.crt"
key = "testdata/ca.key"
}
# Transparent proxy for targets on port 443
transparent = 443
}
# TCP bind address for outgoing connections # TCP bind address for outgoing connections
#bind = "10.42.42.215" #bind = "10.42.42.215"
@@ -12,86 +31,29 @@ proxy {
upstream = [] upstream = []
policy { on {
on intercept { intercept = ["intercept"]
domain = ["sensitive"] request = ["bogons", "childsafe"]
permit = false
}
on request {
source = ["kids"]
domain = ["nsfw"]
permit = false
}
on request {
source = ["kids"]
domain = ["nsfw"]
permit = false
}
on days {
days = "mon-thu,sun"
on time {
time = ["22:00", "06:00"]
on request {
source = ["kids"]
domain = ["social"]
permit = false
}
}
}
} }
} }
dns { policy "intercept" {
# Set the cache size path = "testdata/policy/intercept.rego"
#size = 1024 package = "styx.intercept"
# Set the time to live for positive responses (in seconds)
#ttl = 300
# Set the resolve timeout (in seconds)
#timeout = 10
# Set the DNS servers
#servers = ["1.1.1.1", "8.8.8.8"]
# Disable IPv6
noipv6 = true
} }
mitm { policy "bogons" {
ca { path = "testdata/policy/bogons.rego"
cert = "testdata/ca.crt"
key = "testdata/ca.key"
key_type = "ecc"
days = 1825
organization = "maze.io"
}
key {
type = "rsa"
bits = 2048
}
cache {
#type = "memory"
type = "disk"
path = "testdata/mitm"
expire = 10
}
} }
cache { policy "childsafe" {
type = "memory" path = "testdata/policy/childsafe.rego"
size = 10485760
} }
match { data {
path = "testdata/match" path = "testdata/match"
network "internal" { network "reserved" {
type = "list" type = "list"
list = [ list = [
"0.0.0.0/32", "0.0.0.0/32",
@@ -129,8 +91,25 @@ match {
domain "social" { domain "social" {
type = "list" type = "list"
list = [ list = [
"facebook.com",
"facebook.net",
"fbsbx.com",
"pinterest.com", "pinterest.com",
"reddit.com", "reddit.com",
# TikTok
"isnssdk.com",
"musical.ly",
"musically.app.link",
"musically-alternate.app.link",
"musemuse.cn",
"sgsnssdk.com",
"tiktok.com",
"tiktok.org",
"tiktokcdn.com",
"tiktokcdn-eu.com",
"tiktokv.com",
# X
"twitter.com",
"x.com", "x.com",
# YouTube # YouTube
"googlevideo.com", "googlevideo.com",
@@ -140,15 +119,20 @@ match {
] ]
} }
domain "nsfw" { domain "toxic" {
type = "domains" type = "list"
from = "https://energized.pro/nsfw/domains.txt" list = []
refresh = 43200 # 12h
} }
domain "ads" { #domain "nsfw" {
type = "detect" # type = "domains"
from = "https://small.oisd.nl/dnsmasq" # from = "https://energized.pro/nsfw/domains.txt"
refresh = 12 # refresh = 43200 # 12h
} #}
#
#domain "ads" {
# type = "detect"
# from = "https://small.oisd.nl/dnsmasq"
# refresh = 12
#}
} }

12
testdata/policy/bogons.rego vendored Normal file
View File

@@ -0,0 +1,12 @@
package styx
default permit := true
reject = 404 if {
#some addr in net.lookup_ip_addr(input.http_request.host)
styx.in_networks("bogons", input.http_request.host)
}
errors contains "Bogon destination not allowed" if {
reject == 404
}

56
testdata/policy/childsafe.rego vendored Normal file
View File

@@ -0,0 +1,56 @@
package styx
import input.client as client
import input.request as http_request
# HTTP -> HTTPS redirects for allowed domains
redirect = concat("", ["https://", http_request.host, http_request.path]) if {
_social
http_request.scheme == "http"
}
reject = 403 if {
_childsafe_network
_social
}
reject = 403 if {
_childsafe_network
_toxic
}
# Sensitive domains are always allowed
permit if {
_sensitive
}
permit if {
reject != 0
}
_sensitive if {
styx.in_domains("sensitive", http_request.host)
}
_social if {
styx.in_domains("social", http_request.host)
print("Domain in social", http_request.host)
}
errors contains "Social networking domain not allowed" if {
reject != 0
_social
}
_toxic if {
styx.in_domains("toxic", http_request.host)
}
errors contains "Toxic domain not allowed" if {
reject != 0
_toxic
}
_childsafe_network if {
styx.in_networks("kids", client.ip)
}

21
testdata/policy/intercept.rego vendored Normal file
View File

@@ -0,0 +1,21 @@
package styx.intercept
reject := 403 if {
_target_blocked
}
template := "template/intercepted.html" if {
_target_blocked
}
errors contains "Intercepted" if {
_target_blocked
}
_target_blocked if {
styx.in_domains("bad", input.request.host)
}
_target_blocked if {
styx.in_networks("bogons", input.client.ip)
}