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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 26 additions & 17 deletions .claude/skills/connectors/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,23 +206,32 @@ over HTTPS to a host in `sandbox.hosts`. Daytona's egress swaps it for the token
router with this broker serves the proxy too, so it also needs `ROUTER_CODE_SANDBOX_PROXY_ADDR`
and `ROUTER_CODE_SANDBOX_PROXY_CA_KEY`; without them it does not start. It needs no secret
prefix.
- Behind the proxy, the sandbox gets a placeholder (`va_ph_…`) in the env var, `HTTPS_PROXY`
with its own proxy credential, and the proxy's CA certificate at `/tmp/va-egress-ca.pem`
(`NODE_EXTRA_CA_CERTS`). Before any code runs, the router writes `/tmp/va-egress-ca-bundle.pem`:
the image's own roots and that CA (`SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `GIT_SSL_CAINFO`), so a
host reached directly is still verified. An image with no system roots file runs no code. The proxy puts
the token in place of the placeholder in `Authorization` (Bearer, `token`, or Basic), so git
works too.
- Network behind the proxy: the allow list is the proxy and the config's `allowed_domains` (or its
`network_allow_list` and the proxy's IP address). `NO_PROXY` holds the config's own domains, so
they go direct; a domain that matches a grant's host is left out, so its requests still go
through the proxy, and it is left out of the allow list too. The proxy refuses (403) any name
no grant covers. So a config domain that a grant's host also matches (for example `github.com`
in the config beside a grant for `api.github.com`) is reachable neither directly nor through the
proxy. A config with `network: true` and no
allow list is refused when a session with a sandbox binding starts: every request goes to the
proxy, which refuses hosts no grant names. So is a `network_allow_list` when the proxy URL names
a host, not an IP address.
- Behind the proxy, the sandbox gets a placeholder (`va_ph_…`) in the env var. Daytona gets the
proxy URL with the sandbox's own proxy credential (`OutboundProxyUrl`). Daytona's netleash holds
that credential: the sandbox's `HTTPS_PROXY` names netleash, not the proxy. Netleash replaces the
proxy and CA env vars a sandbox is created with, but not those of one command or run of code
(live probes, 2026-10-21). So the router sets the CA env vars on every command and run:
`SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE` and `GIT_SSL_CAINFO` name
`/tmp/va-egress-ca-bundle.pem`, and `NODE_EXTRA_CA_CERTS` names the proxy's CA certificate,
`/tmp/va-egress-ca.pem`. Before any code runs, the router writes the bundle: netleash's
`ca.crt` (or the image's own roots) and that CA. An image with no roots file runs no code. The
proxy puts the token in place of the placeholder in `Authorization` (Bearer, `token`, or Basic),
so git works too.
- Network behind the proxy: the sandbox's domain allow list is the grants' hosts and the config's
`allowed_domains`. Only that list makes Daytona force every connection through netleash; the
proxy URL alone does not. The proxy then does one of three things with a `CONNECT`:
- a host a grant covers: it ends TLS with its own CA and puts the token in;
- a host of `allowed_domains` only: it sends the bytes on as they are, with no token, and reads
none of them;
- any other host, a non-public address or a port other than 443: it refuses.
A host both cover is a grant's. A config with `network: true` and no `allowed_domains`, or with
a `network_allow_list`, is refused when a session with a sandbox grant starts: the proxy reaches hosts by name
only. Unverified until the staging E2E (AI-1663): that netleash sends the allowed hosts on to
the proxy.
- Sandbox code has root (`sudo` with no password, live probe 2026-10-21). No control inside the
sandbox protects anything: code can change its env, its CA files and its routes. The controls
are the token, which stays in the router; Daytona's domain allow list, which forces every
connection through netleash (a route change as root: unverified); and the proxy's grant check.
- A connection the proxy cannot get a token for just now (a provider outage) keeps its grant; a
connection that needs a reconnect gives the sandbox nothing, and refuses a `required` binding.
- Revocation through the proxy is immediate: it resolves the token on every request and keeps no
Expand Down
6 changes: 6 additions & 0 deletions acceleration/internal/egress/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,12 @@ func (p policy) publicAddresses(ctx context.Context, host string) error {
return nil
}

// DialPublic opens a connection to address, a host and a port, only when every address the host
// resolves to is public: the dial NewClient makes, for callers that carry bytes they do not read.
func DialPublic(ctx context.Context, network, address string) (net.Conn, error) {
return defaultPolicy.dialPublic(ctx, network, address)
}

// IsPublic says whether traffic to a tenant-supplied host may go to ip. It is the check
// NewClient makes at dial, for callers that open their own connections.
func IsPublic(ip netip.Addr) bool { return isPublic(ip) }
Expand Down
8 changes: 6 additions & 2 deletions acceleration/internal/sandbox/broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,12 @@ type Mount struct {
// Env are env vars the sandbox is created with as they are, such as a placeholder the
// broker's egress knows.
Env map[string]string
// Proxy, when set, is the egress every request from the sandbox goes through, and the
// only host the sandbox may reach.
// RunEnv are env vars every command and run of code in the sandbox gets, such as the CA
// files it trusts. Set per run, they win over what the provider sets in the sandbox, which
// Env does not: Daytona's netleash replaces its proxy and CA env vars (live probe, 2026-10-21).
RunEnv map[string]string
// Proxy, when set, is the egress the provider sends every request from the sandbox through,
// with its credential in the userinfo.
Proxy *url.URL
// Files are written into the sandbox, by absolute path, before code runs in it.
Files map[string][]byte
Expand Down
100 changes: 12 additions & 88 deletions acceleration/internal/sandbox/daytona/daytona.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ import (
"errors"
"fmt"
"log/slog"
"maps"
"mime"
"net"
"net/http"
"net/netip"
"os"
"path"
"slices"
Expand Down Expand Up @@ -118,7 +115,8 @@ func (s *Sandbox) Run(ctx context.Context, code string, outputs []string) (sandb
}
run, cancel := context.WithTimeout(ctx, s.runTimeout+5*time.Second)
defer cancel()
ran, err := s.box.Process.CodeRun(run, code, options.WithCodeRunTimeout(s.runTimeout))
ran, err := s.box.Process.CodeRun(run, code, options.WithCodeRunTimeout(s.runTimeout),
options.WithCodeRunParams(types.CodeRunParams{Env: s.mount.RunEnv}))
if err != nil {
return sandbox.Result{}, err
}
Expand Down Expand Up @@ -150,7 +148,7 @@ func (s *Sandbox) Write(ctx context.Context, name string, data []byte) error {
return err
}
if dir := path.Dir(name); dir != "." {
made, err := s.box.Process.ExecuteCommand(ctx, "mkdir -p "+quoted(dir))
made, err := s.box.Process.ExecuteCommand(ctx, "mkdir -p "+quoted(dir), options.WithCommandEnv(s.mount.RunEnv))
if err != nil {
return stack.Wrap(err)
}
Expand Down Expand Up @@ -199,7 +197,7 @@ func (s *Sandbox) started(ctx context.Context) error {
delete(s.files, name)
}
for len(s.setup) > 0 {
ran, err := s.box.Process.ExecuteCommand(ready, "sh -c "+quoted(s.setup[0]))
ran, err := s.box.Process.ExecuteCommand(ready, "sh -c "+quoted(s.setup[0]), options.WithCommandEnv(s.mount.RunEnv))
if err != nil {
return stack.Wrap(err)
}
Expand All @@ -225,16 +223,12 @@ func (s *Sandbox) params() any {
if s.mount.Proxy != nil {
proxy := s.mount.Proxy.String()
base.OutboundProxyUrl = &proxy
if direct := noProxy(s.config, s.grants); direct != "" {
base.EnvVars = maps.Clone(s.mount.Env)
base.EnvVars["NO_PROXY"], base.EnvVars["no_proxy"] = direct, direct
}
}
if len(domains) > 0 {
joined := strings.Join(domains, ",")
base.DomainAllowList = &joined
}
if cidrs := s.ranges(); len(cidrs) > 0 {
if cidrs := s.config.NetworkAllowList; len(cidrs) > 0 {
joined := strings.Join(cidrs, ",")
base.NetworkAllowList = &joined
}
Expand Down Expand Up @@ -298,21 +292,19 @@ func (s *Sandbox) Grants() []sandbox.Grant {

// domains are the config's allowed domains and, unless the config opens the whole network or
// lists CIDR ranges, which Daytona takes instead of domains, the hosts of each grant the
// sandbox holds a secret for: a sandbox that may reach nothing else still reaches its grants.
// A sandbox behind an egress proxy reaches the proxy and the config's own domains less those
// that name a grant's host: code that leaves the proxy out reaches a grant's host only through
// it.
// sandbox holds a secret or, behind an egress proxy, a placeholder for: a sandbox that may reach
// nothing else still reaches its grants. Behind the proxy, Daytona's netleash refuses a CONNECT to
// a host its allow list leaves out (live probe, 2026-10-21: 403). That it reaches the proxy itself, so that the
// proxy's host is not in the list, is unverified until the staging E2E (AI-1663); Daytona refuses an IP address there.
func (s *Sandbox) domains() []string {
domains := slices.Clone(s.config.AllowedDomains)
if s.mount.Proxy != nil && len(s.config.NetworkAllowList) == 0 {
direct := slices.DeleteFunc(domains, func(domain string) bool { return granted(domain, s.grants) })
return slices.Insert(direct, 0, s.mount.Proxy.Hostname())
}
if s.config.Network && len(domains) == 0 || len(s.config.NetworkAllowList) > 0 {
return domains
}
for _, grant := range s.grants {
if _, held := s.mount.Secrets[grant.Env]; held {
_, secret := s.mount.Secrets[grant.Env]
_, placeholder := s.mount.Env[grant.Env]
if secret || placeholder {
for _, host := range grant.Hosts {
if !slices.Contains(domains, host) {
domains = append(domains, host)
Expand All @@ -322,71 +314,3 @@ func (s *Sandbox) domains() []string {
}
return domains
}

// ranges are the config's CIDR ranges and, behind an egress proxy, the proxy's address: Daytona
// takes one allow list, so the proxy joins the one the config uses. A proxy named by a host
// name cannot, and the session refuses that config.
func (s *Sandbox) ranges() []string {
ranges := s.config.NetworkAllowList
if len(ranges) == 0 || s.mount.Proxy == nil {
return ranges
}
address, err := netip.ParseAddr(s.mount.Proxy.Hostname())
if err != nil {
return ranges
}
return append(slices.Clone(ranges), netip.PrefixFrom(address, address.BitLen()).String())
}

// noProxy is NO_PROXY for a sandbox behind an egress proxy: the config's own domains and
// ranges, which code reaches directly, less each domain whose entry would match a grant's host,
// since a request the proxy never sees keeps its placeholder. curl, Python requests and Go take
// an entry as that name and every name under it, and none takes "*.", so *.pypi.org is
// .pypi.org (https://curl.se/docs/manpage.html#ENVIRONMENT,
// https://pkg.go.dev/golang.org/x/net/http/httpproxy#Config).
func noProxy(config sandbox.Config, grants []sandbox.Grant) string {
var direct []string
for _, domain := range config.AllowedDomains {
if !granted(domain, grants) {
name, wildcard := domainName(domain)
if wildcard {
name = "." + name
}
direct = append(direct, name)
}
}
return strings.Join(append(direct, config.NetworkAllowList...), ",")
}

// granted says whether a NO_PROXY entry for a config's domain would match a host one of grants
// covers.
func granted(domain string, grants []sandbox.Grant) bool {
name, _ := domainName(domain)
return slices.ContainsFunc(grants, func(grant sandbox.Grant) bool { return matches(name, grant.Hosts) })
}

// domainName is a config's domain as the name a NO_PROXY entry for it takes, and whether it
// names the names under it too. curl takes a trailing dot and Go's httpproxy a port and "**."
// as the same name (https://curl.se/docs/manpage.html#ENVIRONMENT,
// https://pkg.go.dev/golang.org/x/net/http/httpproxy#Config), so they are taken off.
func domainName(domain string) (name string, wildcard bool) {
domain = strings.ToLower(domain)
if host, _, err := net.SplitHostPort(domain); err == nil {
domain = host
}
domain = strings.TrimSuffix(domain, ".")
name = strings.TrimLeft(domain, "*.")
return name, name != domain
}

// matches says whether a NO_PROXY entry for name matches a host one of hosts covers: hosts are
// exact names, or *. and a domain, which takes the domain and every name under it.
func matches(name string, hosts []string) bool {
for _, host := range hosts {
granted, wildcard := strings.CutPrefix(host, "*.")
if granted == name || strings.HasSuffix(granted, "."+name) || wildcard && strings.HasSuffix(name, "."+granted) {
return true
}
}
return false
}
26 changes: 5 additions & 21 deletions acceleration/internal/sandbox/daytona/daytona_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ type platform struct {
// and commandExit the exit code each gives.
commands []string
commandExit int
// rawCommands are the request bodies of those commands.
rawCommands []string
// steps are the file uploads ("upload") and shell commands ("command") in the order they came.
steps []string
// auth is the last Authorization header seen.
Expand Down Expand Up @@ -133,7 +135,9 @@ func (p *platform) serve(w http.ResponseWriter, r *http.Request) {

case strings.HasSuffix(r.URL.Path, "/process/execute"):
var body struct{ Command string }
_ = json.NewDecoder(r.Body).Decode(&body)
raw, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(raw, &body)
p.rawCommands = append(p.rawCommands, string(raw))
p.steps = append(p.steps, "command")
p.commands = append(p.commands, fmt.Sprintf("%s after %d runs", body.Command, len(p.ran)))
w.Header().Set("Content-Type", "application/json")
Expand Down Expand Up @@ -544,23 +548,3 @@ func (s *DaytonaSuite) TestOnlySoManyFilesComeBackFromOneRun() {
s.Len(result.Files, sandbox.MaxFiles)
s.Equal(wanted[sandbox.MaxFiles:], result.Missing)
}

// TestNoProxyLeavesOutEveryDomainWhoseEntryMatchesAGrantsHost: a request the egress proxy
// never sees keeps its placeholder, so no NO_PROXY entry may match a granted host.
func (s *DaytonaSuite) TestNoProxyLeavesOutEveryDomainWhoseEntryMatchesAGrantsHost() {
crm := sandbox.Grant{Hosts: []string{"api.crm.example", "*.files.example"}}
config := sandbox.Config{AllowedDomains: []string{"api.crm.example", "crm.example", "CRM.example", "v2.api.crm.example",
"files.example", "eu.files.example", "*.files.example", "example", "pypi.org", "*.pythonhosted.org", ".npmjs.org",
"notfiles.example"}, NetworkAllowList: []string{"10.0.0.0/8"}}

s.Equal("v2.api.crm.example,pypi.org,.pythonhosted.org,.npmjs.org,notfiles.example,10.0.0.0/8", noProxy(config, []sandbox.Grant{crm}))
s.Equal("pypi.org", noProxy(sandbox.Config{AllowedDomains: []string{"pypi.org"}}, nil))
s.Empty(noProxy(sandbox.Config{}, []sandbox.Grant{crm}))
s.Equal("pypi.org", noProxy(sandbox.Config{AllowedDomains: []string{"crm.example", "pypi.org"}}, []sandbox.Grant{github, crm}),
"every grant counts, not the first alone")
s.Equal("ithub.com", noProxy(sandbox.Config{AllowedDomains: []string{"ithub.com", ".crm.example"}}, []sandbox.Grant{github, crm}),
"a domain matches at a dot, and a leading dot names the domain")
s.Equal("pypi.org,.pythonhosted.org,pypi.org", noProxy(sandbox.Config{AllowedDomains: []string{"GitHub.com.", "api.github.com:443",
"**.github.com", "pypi.org.", "**.pythonhosted.org", "pypi.org:443"}}, []sandbox.Grant{github}),
"curl and Go take a trailing dot, a port and **. as the name")
}
Loading
Loading