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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,9 @@ keep-alive); HTTP/2-only clients are not supported. WebSocket and other protocol
upgrades return 501 in inspection mode; frame inspection is not implemented.
Plain HTTP carried inside CONNECT (as used by Node fetch) is also captured.
Only traffic configured to pass through Netkit can be inspected.

Inspection has no upstream response-header deadline by default, so long-polling
requests can wait for a response. Client cancellation still cancels the upstream
request. To impose a deadline, use `--inspection-response-header-timeout=90s`
with a duration longer than the upstream polling interval. TLS handshake and
idle-connection timeouts remain in effect.
23 changes: 14 additions & 9 deletions cmd/netkit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ func runServe() {
inspectHTTPS := flag.Bool("inspect-https", false, "Decrypt and capture HTTPS for all destinations (clients must trust the CA)")
caCert := flag.String("ca-cert", "", "Inspection CA certificate PEM file")
caKey := flag.String("ca-key", "", "Inspection CA private key PEM file")
inspectionTimeout := flag.Duration("inspection-response-header-timeout", 0, "Upstream response-header timeout in inspection mode (0 disables; allow for long polling)")
flag.Parse()
if *inspectionTimeout < 0 {
log.Fatal("--inspection-response-header-timeout must not be negative")
}
var ca *proxy.CertificateAuthority
if *inspectHTTPS {
var err error
Expand All @@ -62,15 +66,16 @@ func runServe() {

// Create proxy configuration
config := &proxy.Config{
InspectionCA: ca,
Port: *port,
AdminPort: *adminPort,
HistorySize: *historySize,
Dashboard: *dashboard,
DashboardPort: *dashboardPort,
DashboardDir: *dashboardDir,
DashboardBasePath: *dashboardBasePath,
LogLevel: *logLevel,
InspectionResponseHeaderTimeout: *inspectionTimeout,
InspectionCA: ca,
Port: *port,
AdminPort: *adminPort,
HistorySize: *historySize,
Dashboard: *dashboard,
DashboardPort: *dashboardPort,
DashboardDir: *dashboardDir,
DashboardBasePath: *dashboardBasePath,
LogLevel: *logLevel,
}

// Create and start proxy server
Expand Down
53 changes: 53 additions & 0 deletions internal/proxy/inspection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,56 @@ func TestInspectionStopClosesIdleTunnels(t *testing.T) {
require.NoError(t, p.Stop())
require.Eventually(t, func() bool { p.inspectionMu.Lock(); defer p.inspectionMu.Unlock(); return len(p.inspectionConns) == 0 }, time.Second, time.Millisecond)
}

func TestInspectionLongPolling(t *testing.T) {
pollDelay := 100 * time.Millisecond
if os.Getenv("NETKIT_LONG_POLL_TEST") == "1" {
pollDelay = 31 * time.Second
}
for _, tc := range []struct {
name string
timeout time.Duration
status int
}{
{"default permits delayed headers", 0, 200},
{"explicit deadline is enforced", 20 * time.Millisecond, 502},
} {
t.Run(tc.name, func(t *testing.T) {
upstream := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(pollDelay):
_, err := io.WriteString(w, "poll-result")
require.NoError(t, err)
case <-r.Context().Done():
}
}))
defer upstream.Close()
p, c, _ := inspectionClient(t, upstream)
c.Timeout = 40 * time.Second
// Check the default separately so a fixed deadline regression cannot pass
// merely because this test uses a short simulated polling interval.
require.Zero(t, p.inspectionTransport.ResponseHeaderTimeout)
p.inspectionTransport.ResponseHeaderTimeout = tc.timeout
resp, err := c.Get(upstream.URL + "/poll")
require.NoError(t, err)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, tc.status, resp.StatusCode)
record := waitRecords(t, p, 1)[0]
if tc.status == 200 {
require.Equal(t, "poll-result", string(body))
require.Equal(t, "poll-result", record.ResponseBody)
} else {
require.False(t, record.Success)
require.Contains(t, record.Error, "timeout")
}
})
}
}

func TestInspectionTimeoutConfiguration(t *testing.T) {
p := New(&Config{InspectionResponseHeaderTimeout: 90 * time.Second})
require.Equal(t, 90*time.Second, p.inspectionTransport.ResponseHeaderTimeout)
require.NoError(t, p.Stop())
}
21 changes: 11 additions & 10 deletions internal/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ import (

// Config holds the proxy configuration
type Config struct {
InspectionCA *CertificateAuthority // Non-nil enables inspection for every CONNECT destination
Port int
AdminPort int
LogLevel string
HistorySize int // Maximum number of requests to keep in history
Dashboard bool // Enable dashboard serving
DashboardPort int // Port for dashboard (separate from admin port)
DashboardDir string // Directory containing dashboard build files
DashboardBasePath string // Optional public URL prefix for path-based reverse proxies
InspectionResponseHeaderTimeout time.Duration // Zero allows long polling without a proxy header deadline
InspectionCA *CertificateAuthority // Non-nil enables inspection for every CONNECT destination
Port int
AdminPort int
LogLevel string
HistorySize int // Maximum number of requests to keep in history
Dashboard bool // Enable dashboard serving
DashboardPort int // Port for dashboard (separate from admin port)
DashboardDir string // Directory containing dashboard build files
DashboardBasePath string // Optional public URL prefix for path-based reverse proxies
}

// Proxy represents the HTTP proxy server
Expand Down Expand Up @@ -64,7 +65,7 @@ func New(config *Config) *Proxy {

proxy.inspectionConns = make(map[net.Conn]struct{})
proxy.inspectionTransport = http.DefaultTransport.(*http.Transport).Clone()
proxy.inspectionTransport.ResponseHeaderTimeout = 30 * time.Second
proxy.inspectionTransport.ResponseHeaderTimeout = config.InspectionResponseHeaderTimeout
proxy.inspectionTransport.Proxy = nil // Never route upstream connections back into this proxy.

// Initialize the main HTTP proxy server
Expand Down
Loading