Checkpoint
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# SQLite3 database file
|
||||
*.db
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# Backup files
|
||||
*~
|
206
cmd/styx/config.go
Normal file
206
cmd/styx/config.go
Normal 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
|
||||
}
|
114
cmd/styx/main.go
114
cmd/styx/main.go
@@ -2,84 +2,90 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/hashicorp/hcl/v2/hclsimple"
|
||||
|
||||
"git.maze.io/maze/styx/internal/log"
|
||||
"git.maze.io/maze/styx/logger"
|
||||
"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() {
|
||||
configFlag := flag.String("config", "styx.hcl", "Configuration file")
|
||||
traceFlag := flag.Bool("T", false, "Enable trace level logging")
|
||||
debugFlag := flag.Bool("D", false, "Enable debug level logging")
|
||||
var (
|
||||
configFile = "styx.hcl"
|
||||
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()
|
||||
|
||||
if *traceFlag {
|
||||
log.SetLevel(log.TraceLevel)
|
||||
} else if *debugFlag {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log := logger.StandardLog.Value("path", configFile)
|
||||
|
||||
if traceFlag {
|
||||
log.SetLevel(logger.TraceLevel)
|
||||
} else if debugFlag {
|
||||
log.SetLevel(logger.DebugLevel)
|
||||
}
|
||||
|
||||
config, err := load(*configFlag)
|
||||
config, err := Load(configFile)
|
||||
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 {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
} else if err = config.Proxy.Policy.Configure(matchers); err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
log.Err(err).Fatal("Error configuring proxy ports")
|
||||
}
|
||||
|
||||
var ca mitm.Authority
|
||||
if config.MITM != nil {
|
||||
if ca, err = mitm.New(config.MITM); err != nil {
|
||||
log.Fatal().Err(err).Msg("error configuring mitm")
|
||||
}
|
||||
var (
|
||||
errs = make(chan error, 1)
|
||||
done = make(chan struct{}, 1)
|
||||
sigs = make(chan os.Signal, 1)
|
||||
)
|
||||
|
||||
for i, p := range proxies {
|
||||
go run(config.Proxy.Port[i].Listen, p, errs)
|
||||
}
|
||||
|
||||
server, err := proxy.New(&config.Proxy, ca)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
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
|
||||
}
|
||||
|
||||
if err = server.Start(); err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
case <-done:
|
||||
log.Info("Shutting down gracefully")
|
||||
return
|
||||
|
||||
case err = <-errs:
|
||||
log.Err(err).Fatal("Shutting down because of fatal error in proxy")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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 {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
|
||||
if config.DNS != nil {
|
||||
config.Proxy.Resolver = resolver.New(*config.DNS)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
log.Info("Proxy port ready")
|
||||
errors <- port.Serve(l)
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package netutil
|
||||
package dataset
|
||||
|
||||
import (
|
||||
"strings"
|
5
dataset/domain_data.go
Normal file
5
dataset/domain_data.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package dataset
|
||||
|
||||
var Domains = map[string]*DomainTree{
|
||||
"example": NewDomainList("example.org", "example.net", "example.com"),
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package netutil
|
||||
package dataset
|
||||
|
||||
import (
|
||||
"testing"
|
52
dataset/network.go
Normal file
52
dataset/network.go
Normal 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
71
dataset/network_data.go
Normal 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
71
go.mod
@@ -3,27 +3,74 @@ module git.maze.io/maze/styx
|
||||
go 1.25.0
|
||||
|
||||
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/mattn/go-sqlite3 v1.14.32
|
||||
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/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af
|
||||
github.com/yl2chen/cidranger v1.0.2
|
||||
)
|
||||
|
||||
require (
|
||||
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/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/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||
github.com/lestrrat-go/dsig v1.0.0 // 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/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
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.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
195
go.sum
@@ -1,60 +1,217 @@
|
||||
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
|
||||
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/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/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.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/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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/flatbuffers v25.9.23+incompatible h1:rGZKv+wOb6QPzIdkM2KxhBZCDrA0DeN6DNmRDrqIsQU=
|
||||
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||
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/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.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.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.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/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
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/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/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/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
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/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/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/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/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
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.6.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.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
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 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.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
168
internal/cryptutil/tls.go
Normal 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)
|
||||
}
|
@@ -18,7 +18,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.maze.io/maze/styx/internal/log"
|
||||
"git.maze.io/maze/styx/logger"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Supported key types.
|
||||
@@ -36,6 +37,62 @@ const (
|
||||
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.
|
||||
//
|
||||
// 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
|
||||
}
|
||||
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 {
|
||||
return
|
||||
}
|
||||
} 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 {
|
||||
return
|
||||
}
|
||||
}
|
||||
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 {
|
||||
return
|
||||
}
|
||||
} 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
|
||||
|
@@ -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")
|
||||
}
|
||||
}
|
@@ -29,6 +29,10 @@ func Port(name string) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
if i, err := net.LookupPort("tcp", port); err == nil {
|
||||
return i
|
||||
}
|
||||
|
||||
// TODO: name resolution for ports?
|
||||
i, _ := strconv.Atoi(port)
|
||||
return i
|
||||
|
63
internal/netutil/arp/arp.go
Normal file
63
internal/netutil/arp/arp.go
Normal 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
|
||||
}
|
32
internal/netutil/arp/arp_linux.go
Normal file
32
internal/netutil/arp/arp_linux.go
Normal 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
|
||||
}
|
37
internal/netutil/arp/arp_unix.go
Normal file
37
internal/netutil/arp/arp_unix.go
Normal 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
93
internal/netutil/conn.go
Normal 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
|
||||
}
|
70
internal/sliceutil/filter.go
Normal file
70
internal/sliceutil/filter.go
Normal 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
223
logger/log.go
Normal 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
179
policy/func.go
Normal 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
44
policy/handler.go
Normal 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
394
policy/input.go
Normal 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
189
policy/policy.go
Normal 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(®o.Function{
|
||||
Name: "styx.in_domains",
|
||||
Decl: domainContainsDecl,
|
||||
Nondeterministic: true,
|
||||
}, domainContainsImpl),
|
||||
rego.Function2(®o.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
|
||||
}
|
@@ -14,8 +14,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"git.maze.io/maze/styx/internal/netutil/arp"
|
||||
"github.com/sirupsen/logrus"
|
||||
"git.maze.io/maze/styx/logger"
|
||||
)
|
||||
|
||||
// Context provides convenience functions for the current ongoing HTTP proxy transaction (request).
|
||||
@@ -72,12 +71,11 @@ func (w *countingWriter) Write(p []byte) (n int, err error) {
|
||||
type proxyContext struct {
|
||||
net.Conn
|
||||
id uint64
|
||||
mac net.HardwareAddr
|
||||
cr *countingReader
|
||||
br *bufio.Reader
|
||||
cw *countingWriter
|
||||
isTransparent bool
|
||||
isTransparentTLS bool
|
||||
transparent int
|
||||
transparentTLS bool
|
||||
serverName string
|
||||
req *http.Request
|
||||
res *http.Response
|
||||
@@ -98,7 +96,6 @@ func NewContext(c net.Conn) Context {
|
||||
return &proxyContext{
|
||||
Conn: c,
|
||||
id: binary.BigEndian.Uint64(b),
|
||||
mac: arp.Get(c.RemoteAddr()),
|
||||
cr: cr,
|
||||
br: bufio.NewReader(cr),
|
||||
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
|
||||
binary.BigEndian.PutUint64(id[:], c.id)
|
||||
entry := AccessLog.WithFields(logrus.Fields{
|
||||
entry := AccessLog.Values(logger.Values{
|
||||
"client": c.RemoteAddr().String(),
|
||||
"server": c.LocalAddr().String(),
|
||||
"id": hex.EncodeToString(id[:]),
|
||||
"bytes_rx": c.BytesRead(),
|
||||
"bytes_tx": c.BytesSent(),
|
||||
})
|
||||
if c.mac != nil {
|
||||
return entry.WithField("client_mac", c.mac.String())
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func (c *proxyContext) LogEntry() *logrus.Entry {
|
||||
func (c *proxyContext) LogEntry() logger.Structured {
|
||||
var id [8]byte
|
||||
binary.BigEndian.PutUint64(id[:], c.id)
|
||||
return ServerLog.WithFields(logrus.Fields{
|
||||
return ServerLog.Values(logger.Values{
|
||||
"client": c.RemoteAddr().String(),
|
||||
"server": c.LocalAddr().String(),
|
||||
"id": hex.EncodeToString(id[:]),
|
||||
|
115
proxy/handler.go
115
proxy/handler.go
@@ -15,7 +15,12 @@ import (
|
||||
"git.maze.io/maze/styx/internal/netutil"
|
||||
)
|
||||
|
||||
// Dialer can make outbound connections to upstream servers.
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
type ConnFilter interface {
|
||||
FilterConn(Context) (net.Conn, error)
|
||||
// ErrorHandler can handle errors that occur during proxying.
|
||||
type ErrorHandler interface {
|
||||
// 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.
|
||||
type ConnFilterFunc func(Context) (net.Conn, error)
|
||||
// ConnHandler is called when a new connection has been accepted by the proxy.
|
||||
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)
|
||||
}
|
||||
|
||||
// TLS starts a TLS handshake on the accepted connection.
|
||||
func TLS(certs []tls.Certificate) ConnFilter {
|
||||
return ConnFilterFunc(func(ctx Context) (net.Conn, error) {
|
||||
s := tls.Server(ctx, &tls.Config{
|
||||
Certificates: certs,
|
||||
NextProtos: []string{"http/1.1"},
|
||||
})
|
||||
func TLS(config *tls.Config) ConnHandler {
|
||||
if config == nil {
|
||||
config = new(tls.Config)
|
||||
}
|
||||
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 {
|
||||
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.
|
||||
func TLSInterceptor(ca ca.CertificateAuthority) ConnFilter {
|
||||
return ConnFilterFunc(func(ctx Context) (net.Conn, error) {
|
||||
func TLSInterceptor(ca ca.CertificateAuthority) ConnHandler {
|
||||
return ConnHandlerFunc(func(ctx Context) (net.Conn, error) {
|
||||
s := tls.Server(ctx, &tls.Config{
|
||||
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
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
|
||||
// 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.
|
||||
func Transparent() ConnFilter {
|
||||
return ConnFilterFunc(func(nctx Context) (net.Conn, error) {
|
||||
func Transparent(port int) ConnHandler {
|
||||
return ConnHandlerFunc(func(nctx Context) (net.Conn, error) {
|
||||
ctx, ok := nctx.(*proxyContext)
|
||||
if !ok {
|
||||
return nctx, nil
|
||||
}
|
||||
|
||||
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 _, 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
|
||||
}
|
||||
// Not a TLS connection, moving on to regular HTTP request handling...
|
||||
ctx.LogEntry().Debug("HTTP connection on transparent port")
|
||||
ctx.isTransparent = true
|
||||
ctx.transparent = port
|
||||
} else {
|
||||
ctx.LogEntry().WithField("target", hello.ServerName).Debug("TLS connection on transparent port")
|
||||
ctx.isTransparentTLS = true
|
||||
ctx.LogEntry().Value("target", hello.ServerName).Debug("TLS connection on transparent port")
|
||||
ctx.transparent = port
|
||||
ctx.transparentTLS = true
|
||||
ctx.serverName = hello.ServerName
|
||||
}
|
||||
|
||||
@@ -149,10 +168,10 @@ func Transparent() ConnFilter {
|
||||
})
|
||||
}
|
||||
|
||||
// RequestFilter can filter HTTP requests coming to the proxy.
|
||||
type RequestFilter interface {
|
||||
// FilterRequest 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
|
||||
// RequestHandler can filter HTTP requests coming to the proxy.
|
||||
type RequestHandler interface {
|
||||
// HandlerRequest filters a HTTP request made to the proxy. The current request may be obtained
|
||||
// from [Context.Request]. If a previous RequestHandler provided a HTTP response, it is available
|
||||
// from [Context.Response].
|
||||
//
|
||||
// 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
|
||||
// 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.
|
||||
type RequestFilterFunc func(Context) (*http.Request, *http.Response)
|
||||
// RequestHandlerFunc is a function that implements the [RequestHandler] interface.
|
||||
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)
|
||||
}
|
||||
|
||||
// ResponseFilter can filter HTTP responses coming from the proxy.
|
||||
type ResponseFilter interface {
|
||||
// FilterResponse filters a HTTP response coming from the proxy. The current response may be
|
||||
// ResponseHandler can filter HTTP responses coming from the proxy.
|
||||
type ResponseHandler interface {
|
||||
// HandlerResponse filters a HTTP response coming from the proxy. The current response may be
|
||||
// obtained from [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.
|
||||
type ResponseFilterFunc func(Context) *http.Response
|
||||
// ResponseHandlerFunc is a function that implements the [ResponseHandler] interface.
|
||||
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)
|
||||
}
|
||||
|
||||
// CleanRequestProxyHeaders removes all headers added by downstream proxies from the [http.Request].
|
||||
func CleanRequestProxyHeaders() RequestFilter {
|
||||
return RequestFilterFunc(func(ctx Context) (*http.Request, *http.Response) {
|
||||
func CleanRequestProxyHeaders() RequestHandler {
|
||||
return RequestHandlerFunc(func(ctx Context) (*http.Request, *http.Response) {
|
||||
if req := ctx.Request(); req != nil {
|
||||
cleanProxyHeaders(req.Header)
|
||||
}
|
||||
@@ -197,8 +216,8 @@ func CleanRequestProxyHeaders() RequestFilter {
|
||||
}
|
||||
|
||||
// CleanRequestProxyHeaders removes all headers for upstream proxies from the [http.Response].
|
||||
func CleanResponseProxyHeaders() ResponseFilter {
|
||||
return ResponseFilterFunc(func(ctx Context) *http.Response {
|
||||
func CleanResponseProxyHeaders() ResponseHandler {
|
||||
return ResponseHandlerFunc(func(ctx Context) *http.Response {
|
||||
if res := ctx.Response(); res != nil {
|
||||
cleanProxyHeaders(res.Header)
|
||||
}
|
||||
@@ -208,8 +227,8 @@ func CleanResponseProxyHeaders() ResponseFilter {
|
||||
|
||||
// AddRequestHeaders adds headers to the [http.Request]. Any existing headers with the same
|
||||
// key will remain intact.
|
||||
func AddRequestHeaders(h http.Header) RequestFilter {
|
||||
return RequestFilterFunc(func(ctx Context) (*http.Request, *http.Response) {
|
||||
func AddRequestHeaders(h http.Header) RequestHandler {
|
||||
return RequestHandlerFunc(func(ctx Context) (*http.Request, *http.Response) {
|
||||
if req := ctx.Request(); req != nil {
|
||||
if req.Header == nil {
|
||||
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
|
||||
// key will be removed.
|
||||
func SetRequestHeaders(h http.Header) RequestFilter {
|
||||
return RequestFilterFunc(func(ctx Context) (*http.Request, *http.Response) {
|
||||
func SetRequestHeaders(h http.Header) RequestHandler {
|
||||
return RequestHandlerFunc(func(ctx Context) (*http.Request, *http.Response) {
|
||||
if req := ctx.Request(); req != nil {
|
||||
if req.Header == nil {
|
||||
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
|
||||
// key will remain intact.
|
||||
func AddResponseHeaders(h http.Header) ResponseFilter {
|
||||
return ResponseFilterFunc(func(ctx Context) *http.Response {
|
||||
func AddResponseHeaders(h http.Header) ResponseHandler {
|
||||
return ResponseHandlerFunc(func(ctx Context) *http.Response {
|
||||
if res := ctx.Response(); res != nil {
|
||||
if res.Header == nil {
|
||||
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
|
||||
// key will be removed.
|
||||
func SetResponseHeaders(h http.Header) ResponseFilter {
|
||||
return ResponseFilterFunc(func(ctx Context) *http.Response {
|
||||
func SetResponseHeaders(h http.Header) ResponseHandler {
|
||||
return ResponseHandlerFunc(func(ctx Context) *http.Response {
|
||||
if res := ctx.Response(); res != nil {
|
||||
if res.Header == nil {
|
||||
res.Header = make(http.Header)
|
||||
|
266
proxy/proxy.go
266
proxy/proxy.go
@@ -13,13 +13,14 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.maze.io/maze/styx/internal/netutil"
|
||||
"git.maze.io/maze/styx/logger"
|
||||
"git.maze.io/maze/styx/stats"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Common HTTP headers.
|
||||
@@ -32,6 +33,7 @@ const (
|
||||
HeaderForwardedHost = "X-Forwarded-Host"
|
||||
HeaderForwardedPort = "X-Forwarded-Port"
|
||||
HeaderForwardedProto = "X-Forwarded-Proto"
|
||||
HeaderLocation = "Location"
|
||||
HeaderRealIP = "X-Real-Ip"
|
||||
HeaderUpgrade = "Upgrade"
|
||||
HeaderVia = "Via"
|
||||
@@ -46,43 +48,111 @@ const (
|
||||
|
||||
var (
|
||||
// AccessLog is used for logging requests to the proxy.
|
||||
AccessLog = logrus.StandardLogger()
|
||||
AccessLog = logger.Get()
|
||||
|
||||
// ServerLog is used for logging server log messages.
|
||||
ServerLog = logrus.StandardLogger()
|
||||
ServerLog = logger.Get()
|
||||
)
|
||||
|
||||
// Proxy implements a HTTP(S) proxy.
|
||||
type Proxy struct {
|
||||
rt http.RoundTripper
|
||||
dialer map[string]Dialer
|
||||
connFilter []ConnFilter
|
||||
requestFilter []RequestFilter
|
||||
responseFilter []ResponseFilter
|
||||
dialTimeout time.Duration
|
||||
idleTimeout time.Duration
|
||||
webSocketIdleTimeout time.Duration
|
||||
// RoundTripper is used to make outbound HTTP requests. It defaults to a [http.Transport]
|
||||
// with a custom DialContext that uses the configured [Dialer]s.
|
||||
//
|
||||
// Only override this if you know what you are doing.
|
||||
RoundTripper http.RoundTripper
|
||||
|
||||
// Dialer is a map of protocol names to [Dialer] implementations. The default [Dialer]
|
||||
// corresponds to an empty string key.
|
||||
//
|
||||
// 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.
|
||||
func New() *Proxy {
|
||||
p := &Proxy{
|
||||
dialer: map[string]Dialer{"": defaultDialer{}},
|
||||
dialTimeout: DefaultDialTimeout,
|
||||
idleTimeout: DefaultIdleTimeout,
|
||||
webSocketIdleTimeout: DefaultWebSocketIdleTimeout,
|
||||
Dialer: map[string]Dialer{"": defaultDialer{}},
|
||||
DialTimeout: DefaultDialTimeout,
|
||||
IdleTimeout: DefaultIdleTimeout,
|
||||
WebSocketIdleTimeout: DefaultWebSocketIdleTimeout,
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
|
||||
// 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),
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: time.Second,
|
||||
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{
|
||||
Scheme: network,
|
||||
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) {
|
||||
if dialer == nil {
|
||||
if proto != "" {
|
||||
delete(p.dialer, proto)
|
||||
delete(p.Dialer, proto)
|
||||
}
|
||||
} 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) {
|
||||
d, ok := p.dialer[req.URL.Scheme]
|
||||
d, ok := p.Dialer[req.URL.Scheme]
|
||||
if !ok {
|
||||
d = p.dialer[""]
|
||||
d = p.Dialer[""]
|
||||
}
|
||||
|
||||
return d.DialContext(ctx, req)
|
||||
@@ -170,23 +216,32 @@ func (p *Proxy) handle(nc net.Conn) {
|
||||
err error
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
log := ctx.AccessLogEntry().WithField("duration", time.Since(start))
|
||||
log := ctx.AccessLogEntry().Value("duration", time.Since(start))
|
||||
if err != nil && !netutil.IsClosing(err) {
|
||||
log = log.WithError(err)
|
||||
log = log.Err(err)
|
||||
}
|
||||
if req := ctx.Request(); req != nil {
|
||||
log = log.WithFields(logrus.Fields{
|
||||
log = log.Values(logger.Values{
|
||||
"method": req.Method,
|
||||
"request": req.URL.String(),
|
||||
})
|
||||
}
|
||||
if res := ctx.Response(); res != nil {
|
||||
//countStatus(res.StatusCode)
|
||||
log.WithFields(logrus.Fields{
|
||||
log.Values(logger.Values{
|
||||
"response": res.StatusCode,
|
||||
}).Info(res.Status)
|
||||
} else {
|
||||
@@ -196,38 +251,42 @@ func (p *Proxy) handle(nc net.Conn) {
|
||||
}()
|
||||
|
||||
// Propagate timeouts
|
||||
ctx.SetIdleTimeout(p.idleTimeout)
|
||||
ctx.SetIdleTimeout(p.IdleTimeout)
|
||||
|
||||
for _, f := range p.connFilter {
|
||||
fc, err := f.FilterConn(ctx)
|
||||
for _, f := range p.OnConnect {
|
||||
fc, err := f.HandleConn(ctx)
|
||||
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()
|
||||
return
|
||||
} 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.br = bufio.NewReader(fc)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
if ctx.isTransparentTLS {
|
||||
if ctx.transparentTLS {
|
||||
ctx.req = &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{
|
||||
Scheme: "tcp",
|
||||
Host: net.JoinHostPort(ctx.serverName, "443"),
|
||||
},
|
||||
URL: &url.URL{Scheme: "tcp", Host: net.JoinHostPort(ctx.serverName, strconv.Itoa(ctx.transparent))},
|
||||
Host: net.JoinHostPort(ctx.serverName, strconv.Itoa(ctx.transparent)),
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Close: true,
|
||||
}
|
||||
} else if ctx.req, err = http.ReadRequest(ctx.Reader()); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
if ctx.isTransparent {
|
||||
if ctx.transparent > 0 {
|
||||
// Canonicallize to absolute URL
|
||||
if ctx.req.URL.Host == "" {
|
||||
ctx.req.URL.Host = ctx.req.Host
|
||||
@@ -235,47 +294,68 @@ func (p *Proxy) handle(nc net.Conn) {
|
||||
if ctx.req.URL.Scheme == "" {
|
||||
ctx.req.URL.Scheme = "http"
|
||||
}
|
||||
ctx.isTransparent = false
|
||||
ctx.transparent = 0
|
||||
}
|
||||
|
||||
for _, f := range p.requestFilter {
|
||||
newReq, newRes := f.FilterRequest(ctx)
|
||||
for _, f := range p.OnRequest {
|
||||
newReq, newRes := f.HandleRequest(ctx)
|
||||
if newReq != nil {
|
||||
ServerLog.WithFields(logrus.Fields{
|
||||
ServerLog.Values(logger.Values{
|
||||
"filter": fmt.Sprintf("%T", f),
|
||||
"old_method": ctx.req.Method,
|
||||
"old_url": ctx.req.URL,
|
||||
"new_method": newReq.Method,
|
||||
"new_url": newReq.URL,
|
||||
}).Debug("replacing request from filter")
|
||||
}).Debug("Replacing request from filter")
|
||||
ctx.req = newReq
|
||||
}
|
||||
if newRes != nil {
|
||||
log := ServerLog.WithFields(logrus.Fields{
|
||||
log := ServerLog.Values(logger.Values{
|
||||
"filter": fmt.Sprintf("%T", f),
|
||||
"response": newRes.StatusCode,
|
||||
"status": newRes.Status,
|
||||
})
|
||||
log.Debug("replacing response from filter")
|
||||
log.Debug("Replacing response from filter")
|
||||
ctx.res = newRes
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if err = p.handleRequest(ctx); err != nil {
|
||||
p.handleError(ctx, err, true)
|
||||
return
|
||||
}
|
||||
|
||||
// Only once
|
||||
if ctx.isTransparent || ctx.isTransparentTLS {
|
||||
if ctx.transparent > 0 || ctx.transparentTLS || ctx.req.Method == http.MethodConnect {
|
||||
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) {
|
||||
switch {
|
||||
case ctx.req == nil:
|
||||
@@ -300,9 +380,9 @@ func (p *Proxy) handleRequest(ctx *proxyContext) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy) applyResponseFilter(ctx *proxyContext) {
|
||||
for _, f := range p.responseFilter {
|
||||
if newRes := f.FilterResponse(ctx); newRes != nil {
|
||||
func (p *Proxy) applyResponseHandler(ctx *proxyContext) {
|
||||
for _, f := range p.OnResponse {
|
||||
if newRes := f.HandleResponse(ctx); newRes != nil {
|
||||
if ctx.res.Body != nil {
|
||||
_ = 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
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
@@ -356,10 +436,10 @@ func (p *Proxy) serveConnect(ctx *proxyContext) (err error) {
|
||||
case "":
|
||||
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 (
|
||||
timeout, cancel = context.WithTimeout(context.Background(), p.dialTimeout)
|
||||
timeout, cancel = context.WithTimeout(context.Background(), p.DialTimeout)
|
||||
c net.Conn
|
||||
)
|
||||
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)
|
||||
srv := NewContext(c).(*proxyContext)
|
||||
srv.SetIdleTimeout(p.idleTimeout)
|
||||
srv.SetIdleTimeout(p.IdleTimeout)
|
||||
return p.multiplex(ctx, srv)
|
||||
}
|
||||
|
||||
func (p *Proxy) serveForward(ctx *proxyContext) (err error) {
|
||||
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)
|
||||
ctx.res = NewErrorResponse(err, ctx.req)
|
||||
_ = p.writeResponse(ctx)
|
||||
_ = ctx.Close()
|
||||
return fmt.Errorf("proxy: forward %s error: %w", ctx.req.URL, err)
|
||||
}
|
||||
p.applyResponseFilter(ctx)
|
||||
p.applyResponseHandler(ctx)
|
||||
return p.writeResponse(ctx)
|
||||
}
|
||||
|
||||
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 {
|
||||
case "http":
|
||||
@@ -402,9 +482,9 @@ func (p *Proxy) serveWebSocket(ctx *proxyContext) (err error) {
|
||||
ctx.req.URL.Scheme = "wss"
|
||||
}
|
||||
|
||||
log.Debug("http websocket request")
|
||||
log.Debugf("%s websocket request", ctx.req.Proto)
|
||||
var (
|
||||
timeout, cancel = context.WithTimeout(context.Background(), p.dialTimeout)
|
||||
timeout, cancel = context.WithTimeout(context.Background(), p.DialTimeout)
|
||||
c net.Conn
|
||||
)
|
||||
if c, err = p.dial(timeout, ctx.req); err != nil {
|
||||
@@ -417,7 +497,7 @@ func (p *Proxy) serveWebSocket(ctx *proxyContext) (err error) {
|
||||
cancel()
|
||||
|
||||
srv := NewContext(c).(*proxyContext)
|
||||
srv.SetIdleTimeout(p.idleTimeout)
|
||||
srv.SetIdleTimeout(p.IdleTimeout)
|
||||
if err = ctx.req.Write(srv); err != nil {
|
||||
ctx.res = NewErrorResponse(err, ctx.req)
|
||||
_ = 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)
|
||||
}
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
log.Values(logger.Values{
|
||||
"response": ctx.res.StatusCode,
|
||||
"status": ctx.res.Status,
|
||||
}).Debug("websocket response from upstream")
|
||||
}).Debug("WebSocket response from upstream")
|
||||
if err = p.writeResponse(ctx); err != nil {
|
||||
_ = ctx.Close()
|
||||
return
|
||||
}
|
||||
ctx.SetIdleTimeout(p.webSocketIdleTimeout)
|
||||
ctx.SetIdleTimeout(p.WebSocketIdleTimeout)
|
||||
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) {
|
||||
res := ctx.Response()
|
||||
for _, f := range p.responseFilter {
|
||||
if newRes := f.FilterResponse(ctx); newRes != nil {
|
||||
log.Printf("filter returned response HTTP %s", newRes.Status)
|
||||
for _, f := range p.OnResponse {
|
||||
if newRes := f.HandleResponse(ctx); newRes != nil {
|
||||
log.Printf("Filter returned response HTTP %s", newRes.Status)
|
||||
if res.Body != nil {
|
||||
_ = res.Body.Close()
|
||||
}
|
||||
res = newRes
|
||||
}
|
||||
}
|
||||
ServerLog.WithFields(logrus.Fields{
|
||||
ServerLog.Values(logger.Values{
|
||||
"close": res.Close,
|
||||
"header": res.Header,
|
||||
}).Debug("writing response")
|
||||
}).Debug("Writing response")
|
||||
if err = res.Write(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
142
styx.hcl
142
styx.hcl
@@ -1,7 +1,26 @@
|
||||
|
||||
proxy {
|
||||
# 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
|
||||
#bind = "10.42.42.215"
|
||||
@@ -12,86 +31,29 @@ proxy {
|
||||
upstream = []
|
||||
|
||||
|
||||
policy {
|
||||
on intercept {
|
||||
domain = ["sensitive"]
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
on {
|
||||
intercept = ["intercept"]
|
||||
request = ["bogons", "childsafe"]
|
||||
}
|
||||
}
|
||||
|
||||
dns {
|
||||
# Set the cache size
|
||||
#size = 1024
|
||||
|
||||
# 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
|
||||
policy "intercept" {
|
||||
path = "testdata/policy/intercept.rego"
|
||||
package = "styx.intercept"
|
||||
}
|
||||
|
||||
mitm {
|
||||
ca {
|
||||
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
|
||||
}
|
||||
policy "bogons" {
|
||||
path = "testdata/policy/bogons.rego"
|
||||
}
|
||||
|
||||
cache {
|
||||
type = "memory"
|
||||
size = 10485760
|
||||
policy "childsafe" {
|
||||
path = "testdata/policy/childsafe.rego"
|
||||
}
|
||||
|
||||
match {
|
||||
data {
|
||||
path = "testdata/match"
|
||||
|
||||
network "internal" {
|
||||
network "reserved" {
|
||||
type = "list"
|
||||
list = [
|
||||
"0.0.0.0/32",
|
||||
@@ -129,8 +91,25 @@ match {
|
||||
domain "social" {
|
||||
type = "list"
|
||||
list = [
|
||||
"facebook.com",
|
||||
"facebook.net",
|
||||
"fbsbx.com",
|
||||
"pinterest.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",
|
||||
# YouTube
|
||||
"googlevideo.com",
|
||||
@@ -140,15 +119,20 @@ match {
|
||||
]
|
||||
}
|
||||
|
||||
domain "nsfw" {
|
||||
type = "domains"
|
||||
from = "https://energized.pro/nsfw/domains.txt"
|
||||
refresh = 43200 # 12h
|
||||
domain "toxic" {
|
||||
type = "list"
|
||||
list = []
|
||||
}
|
||||
|
||||
domain "ads" {
|
||||
type = "detect"
|
||||
from = "https://small.oisd.nl/dnsmasq"
|
||||
refresh = 12
|
||||
}
|
||||
#domain "nsfw" {
|
||||
# type = "domains"
|
||||
# from = "https://energized.pro/nsfw/domains.txt"
|
||||
# refresh = 43200 # 12h
|
||||
#}
|
||||
#
|
||||
#domain "ads" {
|
||||
# type = "detect"
|
||||
# from = "https://small.oisd.nl/dnsmasq"
|
||||
# refresh = 12
|
||||
#}
|
||||
}
|
||||
|
12
testdata/policy/bogons.rego
vendored
Normal file
12
testdata/policy/bogons.rego
vendored
Normal 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
56
testdata/policy/childsafe.rego
vendored
Normal 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
21
testdata/policy/intercept.rego
vendored
Normal 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)
|
||||
}
|
Reference in New Issue
Block a user