diff --git a/.gitignore b/.gitignore index cb7a921..4532edb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ vendor/ vendor/ go.work /cmd/sandworm/sandworm +/sandworm.db +/sandworm.db-shm +/sandworm.db-wal diff --git a/README.md b/README.md index e395606..6c3b44b 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,49 @@ Allow mode (default) — only the listed domains/CIDRs are reachable: sandworm --port 2137 --domains "github.com,golang.org" --cidrs "10.0.0.0/8" ``` +## Simple Token Translation Workflow + +Start `sandworm` with SQLite-backed token translation enabled: + +```bash +sandworm \ + --port 2137 \ + --domains "api.openai.com,api.github.com,github.com" \ + --translate-tokens +``` + +Add bearer-token mappings for APIs that should receive translated secrets: + +```bash +sandworm token add bearer --host api.openai.com \ + --path /v1/ \ + --secret "$OPENAI_API_KEY" \ + --placeholder OPENAI_PLACEHOLDER + +sandworm token add bearer --host api.github.com \ + --secret "$GITHUB_TOKEN" \ + --placeholder GITHUB_PLACEHOLDER +``` + +Generate a sourceable shell config with proxy env vars and the local CA: + +```bash +sandworm token config --host 127.0.0.1 --port 2137 > sandworm-env.sh +source ./sandworm-env.sh +``` + +Clients can then use placeholders instead of real tokens: + +```bash +curl https://api.openai.com/v1/models \ + -H 'Authorization: Bearer OPENAI_PLACEHOLDER' + +curl https://api.github.com/user \ + -H 'Authorization: Bearer GITHUB_PLACEHOLDER' +``` + +`sandworm` rewrites the outbound requests using the real values stored in SQLite. + Block mode — everything is reachable except the listed domains/CIDRs: ```bash sandworm --port 2137 --mode block --blocked-domains "facebook.com,*.ads.example.com" @@ -75,6 +118,7 @@ Note: block mode cannot be used together with `--ip-proxy-range`. - `--ip-proxy-range`: CIDR range for IP mode domain mapping - `--ip-ports`: Comma-separated list of ports for IP mode - `--dns-port`: DNS server port (requires IP mode) +- `--translate-tokens`: Enable SQLite-backed token translation using `./sandworm.db` ## Building diff --git a/cmd/sandworm/ipmode_cli.go b/cmd/sandworm/ipmode_cli.go index 1735c94..a6e14aa 100644 --- a/cmd/sandworm/ipmode_cli.go +++ b/cmd/sandworm/ipmode_cli.go @@ -69,17 +69,15 @@ func runEtcHosts(args proxyArgs) error { } } - proxyServer := proxy.NewProxy( - 0, /* port */ - "", /* adminBind */ - "allow", /* mode */ - allowedDomains, - nil, /* allowedCIDRs */ - nil, /* blockedDomains */ - nil, /* blockedCIDRs */ - args.IPProxyRange, - ipPorts, - 0 /* dnsPort */) + proxyServer, err := proxy.NewProxy(proxy.Options{ + Mode: "allow", + AllowedDomains: allowedDomains, + IPProxyRange: args.IPProxyRange, + IPPorts: ipPorts, + }) + if err != nil { + return err + } mappings := proxyServer.GetIPMappings() if len(mappings) == 0 { diff --git a/cmd/sandworm/main.go b/cmd/sandworm/main.go index e732ada..a688681 100644 --- a/cmd/sandworm/main.go +++ b/cmd/sandworm/main.go @@ -30,6 +30,7 @@ to containers on an internal network, with configurable domain and CIDR filterin } addSharedFlags(rootCmd, &args) + rootCmd.AddCommand(tokenCommand()) addIPModeCommands(rootCmd) if err := rootCmd.Execute(); err != nil { @@ -38,18 +39,19 @@ to containers on an internal network, with configurable domain and CIDR filterin } type proxyArgs struct { - Port int - AdminEnabled bool - AdminBind string - Mode string - Domains string - CIDRs string - BlockedDomains string - BlockedCIDRs string - LogLevel string - IPProxyRange string - IPPorts string - DNSPort int + Port int + AdminEnabled bool + AdminBind string + Mode string + Domains string + CIDRs string + BlockedDomains string + BlockedCIDRs string + LogLevel string + IPProxyRange string + IPPorts string + DNSPort int + TranslateTokens bool } func addSharedFlags(cmd *cobra.Command, args *proxyArgs) { @@ -63,7 +65,9 @@ func addSharedFlags(cmd *cobra.Command, args *proxyArgs) { cmd.Flags().StringVar(&args.BlockedDomains, "blocked-domains", "", "Comma-separated list of blocked domains (block mode)") cmd.Flags().StringVar(&args.BlockedCIDRs, "blocked-cidrs", "", "Comma-separated list of blocked CIDRs (block mode)") cmd.Flags().StringVarP(&args.LogLevel, "log-level", "l", "info", "Log level (debug, info, warn, error)") + cmd.Flags().BoolVar(&args.TranslateTokens, "translate-tokens", false, "Enable SQLite-backed token translation using the default sandworm DB") addIPModeFlags(cmd, args) + } func validateArgs(args proxyArgs) error { @@ -121,6 +125,9 @@ func runProxy(ctx context.Context, args proxyArgs) error { slog.Warn("Deprecated flag --admin used; set --admin-bind explicitly (empty disables)", "admin_bind", args.AdminBind) } + if args.TranslateTokens && args.AdminBind == "" { + args.AdminBind = "127.0.0.1" + } if err := validateArgs(args); err != nil { return err @@ -170,7 +177,27 @@ func runProxy(ctx context.Context, args proxyArgs) error { } } - proxyServer := proxy.NewProxy(args.Port, args.AdminBind, args.Mode, allowedDomains, allowedCIDRs, blockedDomains, blockedCIDRs, args.IPProxyRange, ipPorts, args.DNSPort) + tokenDBPath := "" + if args.TranslateTokens { + tokenDBPath = proxy.DefaultTokenDBPath + } + + proxyServer, err := proxy.NewProxy(proxy.Options{ + Port: args.Port, + AdminBind: args.AdminBind, + Mode: args.Mode, + AllowedDomains: allowedDomains, + AllowedCIDRs: allowedCIDRs, + BlockedDomains: blockedDomains, + BlockedCIDRs: blockedCIDRs, + IPProxyRange: args.IPProxyRange, + IPPorts: ipPorts, + DNSPort: args.DNSPort, + TokenDB: tokenDBPath, + }) + if err != nil { + return err + } ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -187,7 +214,9 @@ func runProxy(ctx context.Context, args proxyArgs) error { "domains", allowedDomains, "cidrs", allowedCIDRs, "blocked_domains", blockedDomains, - "blocked_cidrs", blockedCIDRs) + "blocked_cidrs", blockedCIDRs, + "translate_tokens", args.TranslateTokens, + "translate_tokens_db", tokenDBPath) sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) diff --git a/cmd/sandworm/token_commands.go b/cmd/sandworm/token_commands.go new file mode 100644 index 0000000..1501dff --- /dev/null +++ b/cmd/sandworm/token_commands.go @@ -0,0 +1,276 @@ +package main + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/poolsideai/sandworm/pkg/proxy" +) + +type tokenArgs struct { + DB string + Token string + Name string + Host string + Path string + Secret string + Placeholder string + Header string + BindHost string + Port int + CAPath string +} + +func tokenCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "token", + Short: "Manage translated secrets in SQLite", + } + + cmd.AddCommand(tokenAddCommand()) + cmd.AddCommand(tokenListCommand()) + cmd.AddCommand(tokenRevokeCommand()) + cmd.AddCommand(tokenConfigCommand()) + return cmd +} + +func tokenAddCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "add", + Short: "Add a translated secret mapping", + } + cmd.AddCommand(tokenAddBearerCommand()) + cmd.AddCommand(tokenAddHeaderCommand()) + return cmd +} + +func tokenAddBearerCommand() *cobra.Command { + args := tokenArgs{} + cmd := &cobra.Command{ + Use: "bearer", + Short: "Add a bearer-token mapping", + RunE: func(cmd *cobra.Command, cmdArgs []string) error { + store, err := proxy.OpenTranslationStore(args.DB) + if err != nil { + return err + } + defer store.Close() + + rule, token, err := proxy.AddBearerMapping(store, args.Token, args.Name, args.Host, args.Path, args.Secret, args.Placeholder) + if err != nil { + return err + } + fmt.Fprintf(os.Stdout, "created bearer mapping %q for %s %s using proxy token %q\n", rule.Name, rule.HostPattern, rule.PathPattern, token.Name) + return nil + }, + } + addTokenStoreFlags(cmd, &args) + cmd.Flags().StringVar(&args.Host, "host", "", "Target host to rewrite (required)") + cmd.Flags().StringVar(&args.Path, "path", "", "Optional path prefix or exact path") + cmd.Flags().StringVar(&args.Secret, "secret", "", "Real bearer token to store (required)") + cmd.Flags().StringVar(&args.Placeholder, "placeholder", "", "Optional inbound placeholder token to match") + _ = cmd.MarkFlagRequired("host") + _ = cmd.MarkFlagRequired("secret") + return cmd +} + +func tokenAddHeaderCommand() *cobra.Command { + args := tokenArgs{} + cmd := &cobra.Command{ + Use: "header", + Short: "Add a raw header-value mapping", + RunE: func(cmd *cobra.Command, cmdArgs []string) error { + store, err := proxy.OpenTranslationStore(args.DB) + if err != nil { + return err + } + defer store.Close() + + rule, token, err := proxy.AddHeaderMapping(store, args.Token, args.Name, args.Host, args.Path, args.Secret, args.Header, args.Placeholder) + if err != nil { + return err + } + fmt.Fprintf(os.Stdout, "created header mapping %q for %s %s using proxy token %q\n", rule.Name, rule.HostPattern, rule.PathPattern, token.Name) + return nil + }, + } + addTokenStoreFlags(cmd, &args) + cmd.Flags().StringVar(&args.Host, "host", "", "Target host to rewrite (required)") + cmd.Flags().StringVar(&args.Path, "path", "", "Optional path prefix or exact path") + cmd.Flags().StringVar(&args.Secret, "secret", "", "Real header value to store (required)") + cmd.Flags().StringVar(&args.Header, "header", "", "Header name to inject (required)") + cmd.Flags().StringVar(&args.Placeholder, "placeholder", "", "Optional inbound placeholder value to match") + _ = cmd.MarkFlagRequired("host") + _ = cmd.MarkFlagRequired("secret") + _ = cmd.MarkFlagRequired("header") + return cmd +} + +func tokenConfigCommand() *cobra.Command { + args := tokenArgs{ + BindHost: "127.0.0.1", + Port: 2137, + CAPath: proxy.DefaultCACertOutPath, + } + cmd := &cobra.Command{ + Use: "config", + Short: "Print a sourceable shell config for the proxy token", + RunE: func(cmd *cobra.Command, cmdArgs []string) error { + store, err := proxy.OpenTranslationStore(args.DB) + if err != nil { + return err + } + defer store.Close() + + token, err := store.EnsureToken(args.Token) + if err != nil { + return err + } + ca, err := proxy.EnsureTranslationCA(store) + if err != nil { + return err + } + + return writeTokenConfig(os.Stdout, token, ca.CACertPEM(), args) + }, + } + addTokenStoreFlags(cmd, &args) + cmd.Flags().StringVar(&args.BindHost, "host", args.BindHost, "Proxy host to embed in the generated env") + cmd.Flags().IntVar(&args.Port, "port", args.Port, "Proxy port to embed in the generated env") + cmd.Flags().StringVar(&args.CAPath, "ca-path", args.CAPath, "CA certificate path to embed in the generated env") + return cmd +} + +func tokenListCommand() *cobra.Command { + args := tokenArgs{} + cmd := &cobra.Command{ + Use: "list", + Short: "List proxy tokens", + RunE: func(cmd *cobra.Command, cmdArgs []string) error { + store, err := proxy.OpenTranslationStore(args.DB) + if err != nil { + return err + } + defer store.Close() + + tokens := store.ListTokens() + if len(tokens) == 0 { + fmt.Fprintln(os.Stderr, "no proxy tokens") + return nil + } + + rules := store.ListRules() + for _, token := range tokens { + fmt.Println(token.Name) + count := 0 + for _, rule := range rules { + if rule.TokenID != nil && *rule.TokenID == token.ID { + fmt.Fprintf(os.Stdout, " - %s\n", mappingSummary(rule)) + count++ + } + } + if count == 0 { + fmt.Fprintln(os.Stdout, " - no mappings") + } + } + + sharedCount := 0 + for _, rule := range rules { + if rule.TokenID == nil { + if sharedCount == 0 { + fmt.Fprintln(os.Stdout, "shared") + } + fmt.Fprintf(os.Stdout, " - %s\n", mappingSummary(rule)) + sharedCount++ + } + } + return nil + }, + } + addTokenStoreFlags(cmd, &args) + return cmd +} + +func tokenRevokeCommand() *cobra.Command { + args := tokenArgs{} + cmd := &cobra.Command{ + Use: "revoke", + Short: "Revoke a proxy token by name", + RunE: func(cmd *cobra.Command, cmdArgs []string) error { + store, err := proxy.OpenTranslationStore(args.DB) + if err != nil { + return err + } + defer store.Close() + + if err := store.RevokeTokenByName(args.Token); err != nil { + return err + } + fmt.Fprintf(os.Stdout, "revoked proxy token %q\n", args.Token) + return nil + }, + } + addTokenStoreFlags(cmd, &args) + return cmd +} + +func addTokenStoreFlags(cmd *cobra.Command, args *tokenArgs) { + cmd.Flags().StringVar(&args.DB, "db", proxy.DefaultTokenDBPath, "SQLite database path") + cmd.Flags().StringVar(&args.Token, "token", proxy.DefaultProxyToken, "Proxy token name to scope this mapping or config") + cmd.Flags().StringVar(&args.Name, "name", "", "Optional display name for the mapping") +} + +func writeTokenConfig(w io.Writer, token *proxy.TranslationToken, caPEM string, args tokenArgs) error { + proxyURL := fmt.Sprintf("http://x:%s@%s:%d", token.Token, args.BindHost, args.Port) + fmt.Fprintln(w, "# sandworm token config") + fmt.Fprintln(w, "#") + fmt.Fprintln(w, "# The proxy URL below uses HTTP Basic auth for the proxy itself.") + fmt.Fprintln(w, "# Username 'x' is ignored; the password field carries the sandworm proxy token.") + fmt.Fprintln(w, "# sandworm reads that proxy token from Proxy-Authorization and uses it to select translated secrets.") + fmt.Fprintln(w, "#") + fmt.Fprintln(w, "# The CA file below is required for HTTPS translation.") + fmt.Fprintln(w, "# sandworm terminates TLS locally so it can rewrite outbound headers, then reconnects upstream.") + fmt.Fprintln(w, "# Clients must trust this CA or HTTPS requests through the proxy will fail certificate validation.") + fmt.Fprintf(w, "export SANDWORM_PROXY_URL=%q\n", proxyURL) + fmt.Fprintf(w, "export SANDWORM_CA_PATH=%q\n", args.CAPath) + fmt.Fprintf(w, "cat >\"$SANDWORM_CA_PATH\" <<'__SANDWORM_CA__'\n%s__SANDWORM_CA__\n", caPEM) + fmt.Fprintf(w, "export HTTP_PROXY=%q\n", proxyURL) + fmt.Fprintf(w, "export HTTPS_PROXY=%q\n", proxyURL) + fmt.Fprintf(w, "export http_proxy=%q\n", proxyURL) + fmt.Fprintf(w, "export https_proxy=%q\n", proxyURL) + fmt.Fprintf(w, "export SSL_CERT_FILE=%q\n", args.CAPath) + fmt.Fprintf(w, "export NODE_EXTRA_CA_CERTS=%q\n", args.CAPath) + return nil +} + +func mappingSummary(rule proxy.TranslationRule) string { + mode := "header" + switch { + case strings.EqualFold(rule.InjectHeaderName, "Authorization") && rule.InjectHeaderValueFormat == "Bearer {value}": + mode = "bearer" + case rule.InjectHeaderValueFormat == "{value}": + mode = fmt.Sprintf("header %s", rule.InjectHeaderName) + default: + mode = fmt.Sprintf("header %s", rule.InjectHeaderName) + } + + path := rule.PathPattern + if path == "" { + path = "*" + } + + var extras []string + if rule.MatchHeaderName != "" { + extras = append(extras, fmt.Sprintf("placeholder via %s", rule.MatchHeaderName)) + } + + summary := fmt.Sprintf("%s %s %s", mode, rule.HostPattern, path) + if len(extras) > 0 { + summary += " (" + strings.Join(extras, ", ") + ")" + } + return summary +} diff --git a/cmd/sandworm/token_commands_test.go b/cmd/sandworm/token_commands_test.go new file mode 100644 index 0000000..f723304 --- /dev/null +++ b/cmd/sandworm/token_commands_test.go @@ -0,0 +1,35 @@ +package main + +import ( + "bytes" + "strings" + "testing" + + "github.com/poolsideai/sandworm/pkg/proxy" + "github.com/stretchr/testify/require" +) + +func TestWriteTokenConfig(t *testing.T) { + token := &proxy.TranslationToken{ + Name: "default", + Token: "sand_test", + } + args := tokenArgs{ + BindHost: "127.0.0.1", + Port: 2137, + CAPath: "/tmp/sandworm-ca.pem", + } + + var out bytes.Buffer + require.NoError(t, writeTokenConfig(&out, token, "TEST CA\n", args)) + + got := out.String() + for _, want := range []string{ + `export SANDWORM_PROXY_URL="http://x:sand_test@127.0.0.1:2137"`, + `export SANDWORM_CA_PATH="/tmp/sandworm-ca.pem"`, + `export NODE_EXTRA_CA_CERTS="/tmp/sandworm-ca.pem"`, + "TEST CA", + } { + require.Truef(t, strings.Contains(got, want), "generated config missing %q:\n%s", want, got) + } +} diff --git a/go.mod b/go.mod index 98427ae..de8d96f 100644 --- a/go.mod +++ b/go.mod @@ -7,14 +7,28 @@ toolchain go1.24.11 require github.com/spf13/cobra v1.8.1 require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/miekg/dns v1.1.69 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netns v0.0.5 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/tools v0.39.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.66.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.39.1 // indirect ) diff --git a/go.sum b/go.sum index 8e6f70b..4a3ebc1 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,35 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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/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/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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= @@ -19,10 +37,20 @@ golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4= +modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= diff --git a/integration_test.go b/integration_test.go index b7ff2bd..a9115fd 100644 --- a/integration_test.go +++ b/integration_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/poolsideai/sandworm/pkg/proxy" + "github.com/stretchr/testify/require" ) const ( @@ -173,7 +174,7 @@ func runTestSuite(t *testing.T, suite testSuite) { func startSandworm(t *testing.T, allowedDomains, allowedCIDRs string) (context.Context, context.CancelFunc, int) { port := findAvailablePort(t) - + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelWarn, })) @@ -196,13 +197,17 @@ func startSandworm(t *testing.T, allowedDomains, allowedCIDRs string) (context.C } } - proxyServer := proxy.NewProxy(port, "", "allow", allowedDomainsList, allowedCIDRsList, nil, nil, "", nil, 0) + proxyServer, err := proxy.NewProxy(proxy.Options{ + Port: port, + Mode: "allow", + AllowedDomains: allowedDomainsList, + AllowedCIDRs: allowedCIDRsList, + }) + require.NoError(t, err, "failed to create proxy") ctx, cancel := context.WithCancel(context.Background()) - if err := proxyServer.Start(ctx); err != nil { - t.Fatalf("Failed to start proxy: %v", err) - } + require.NoError(t, proxyServer.Start(ctx), "failed to start proxy") return ctx, cancel, port } @@ -232,13 +237,17 @@ func startSandwormBlockMode(t *testing.T, blockedDomains, blockedCIDRs string) ( } } - proxyServer := proxy.NewProxy(port, "", "block", nil, nil, blockedDomainsList, blockedCIDRsList, "", nil, 0) + proxyServer, err := proxy.NewProxy(proxy.Options{ + Port: port, + Mode: "block", + BlockedDomains: blockedDomainsList, + BlockedCIDRs: blockedCIDRsList, + }) + require.NoError(t, err, "failed to create proxy") ctx, cancel := context.WithCancel(context.Background()) - if err := proxyServer.Start(ctx); err != nil { - t.Fatalf("Failed to start proxy: %v", err) - } + require.NoError(t, proxyServer.Start(ctx), "failed to start proxy") return ctx, cancel, port } @@ -251,15 +260,14 @@ func findAvailablePort(t *testing.T) int { return port } } - t.Fatal("No available ports found") + require.FailNow(t, "No available ports found") return 0 } - func createProxyClient(port int) *http.Client { proxyURL := fmt.Sprintf("http://localhost:%d", port) proxyURLParsed, _ := url.Parse(proxyURL) - + return &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyURL(proxyURLParsed), @@ -277,6 +285,6 @@ func testProxy(client *http.Client, targetURL string) string { if resp.StatusCode >= 200 && resp.StatusCode < 400 { return "ALLOWED" } - + return "BLOCKED" } diff --git a/pkg/proxy/admin_panel.html b/pkg/proxy/admin_panel.html index 2f5cf41..6cf00b1 100644 --- a/pkg/proxy/admin_panel.html +++ b/pkg/proxy/admin_panel.html @@ -142,6 +142,12 @@ margin: 0; width: auto; } + pre { + background: var(--logs-bg); + padding: 12px; + border-radius: 5px; + overflow-x: auto; + }