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
16 changes: 10 additions & 6 deletions sandboxd/egress/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import (
"slices"
"strings"
"sync"
"time"

"github.com/cocoonstack/sandbox/sandboxd/utils"
)

const idleConnTimeout = 90 * time.Second

// hopHeaders are hop-by-hop and proxy-scoped headers this hop owns: stripped
// from forwarded requests and from relayed responses rather than passed on.
var hopHeaders = []string{
Expand Down Expand Up @@ -85,15 +88,12 @@ func New(sandbox, tenant string, policy Evaluator, secrets Secrets, ca *CA, dial
dial: dial,
// The stdlib default of 2 idle conns per host re-dials bursty
// same-host plaintext traffic.
tr: &http.Transport{DialContext: dial, MaxIdleConnsPerHost: 8},
tr: &http.Transport{DialContext: dial, MaxIdleConnsPerHost: 8, IdleConnTimeout: idleConnTimeout},
conns: map[net.Conn]struct{}{},
}
if ca != nil {
p.mitmTr = &http.Transport{
DialContext: dial,
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
MaxIdleConnsPerHost: 8,
}
p.mitmTr = p.tr.Clone()
p.mitmTr.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12}
p.leaves = map[string]*tls.Certificate{}
}
return p
Expand All @@ -118,6 +118,10 @@ func (p *Proxy) Close() {
for _, conn := range conns {
_ = conn.Close()
}
p.tr.CloseIdleConnections()
if p.mitmTr != nil {
p.mitmTr.CloseIdleConnections()
}
}

// track registers conn for Close; false means already closed, conn closed instead.
Expand Down
37 changes: 37 additions & 0 deletions sandboxd/egress/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,43 @@ func TestForwardAllowInjectsSecretAndOverwritesGuestHeader(t *testing.T) {
}
}

func TestCloseReleasesIdleUpstreamConns(t *testing.T) {
closed := make(chan struct{}, 4)
upstream := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = io.WriteString(w, "hello")
}))
upstream.Config.ConnState = func(_ net.Conn, st http.ConnState) {
if st == http.StateClosed {
closed <- struct{}{}
}
}
upstream.Start()
defer upstream.Close()

policy := Policy{Allow: []Rule{{Host: "api.internal"}}}
p := New("sb_1", "acme", policy, nil, nil, fixedDial(upstream.Listener.Addr().String()), nil)
front := httptest.NewServer(p)
defer front.Close()

req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://api.internal/x", nil)
if err != nil {
t.Fatalf("build request: %v", err)
}
resp, err := proxyClient(t, front.URL).Do(req)
if err != nil {
t.Fatalf("proxied GET: %v", err)
}
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()

p.Close()
select {
case <-closed:
case <-time.After(5 * time.Second):
t.Fatal("upstream connection still open after Close")
}
}

func TestForwardNeverInjectsInterceptSecret(t *testing.T) {
reached := false
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
13 changes: 10 additions & 3 deletions sdk/go/silkd/silkdtest/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,16 @@ func (f *Fake) fsFind(conn net.Conn, req *wire.FsFind) {
if req.Glob != "" {
nameRe = globRegexp(req.Glob)
}
walkErr := filepath.WalkDir(f.abs(req.Path), func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
root := f.abs(req.Path)
walkErr := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
if path == root {
return err
}
return nil
}
if d.IsDir() {
return nil
}
if nameRe != nil && !nameRe.MatchString(d.Name()) {
return nil
Expand Down
14 changes: 9 additions & 5 deletions silkd/src/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! rewrites matches in named files. Regex handling lives here so agents pass a
//! pattern as data rather than shell-quoting it through exec.

use std::path::Path;
use std::path::{Path, PathBuf};

use regex::Regex;
use tokio::fs;
Expand Down Expand Up @@ -32,28 +32,32 @@ pub async fn find<W: AsyncWrite + Unpin>(
Some(Ok(re)) => Some(re),
Some(Err(e)) => return proto::error_frame(w, ErrorKind::BadRequest, e.to_string()).await,
};
let mut stack = vec![path];
let mut stack = vec![PathBuf::from(path)];
let mut root = true;
while let Some(dir) = stack.pop() {
let mut rd = match fs::read_dir(&dir).await {
Ok(rd) => rd,
Err(e) => return err_frame(w, &e, "read_dir").await,
Err(e) if root => return err_frame(w, &e, "read_dir").await,
Err(_) => continue,
};
loop {
let ent = match rd.next_entry().await {
Ok(Some(ent)) => ent,
Ok(None) => break,
Err(e) => return err_frame(w, &e, "read_dir").await,
Err(e) if root => return err_frame(w, &e, "read_dir").await,
Err(_) => break,
};
let Ok(ft) = ent.file_type().await else {
continue;
};
let p = ent.path();
if ft.is_dir() {
stack.push(p.to_string_lossy().into_owned());
stack.push(p);
} else if ft.is_file() && name_matches(&p, name_re.as_ref()) {
scan_file(w, &re, &p).await?;
}
}
root = false;
}
proto::write_frame(w, &Response::Done).await
}
Expand Down