Files
wehub-resource-sync e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

258 lines
8.7 KiB
Go

// Copyright 2026 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"fmt"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/alibaba/opensandbox/egress/pkg/constants"
"github.com/alibaba/opensandbox/egress/pkg/iptables"
"github.com/alibaba/opensandbox/egress/pkg/log"
"github.com/alibaba/opensandbox/egress/pkg/mitmproxy"
"github.com/alibaba/opensandbox/internal/safego"
)
// exitEvent carries an OnExit notification tagged with the generation of the
// mitmdump process that produced it. Generation tagging lets the watcher tell
// "the currently-running mitmdump just died" apart from "a half-launched
// attempt we already killed during a retry storm just finished reaping".
type exitEvent struct {
gen uint64
err error
}
type mitmTransparent struct {
mu sync.Mutex
running *mitmproxy.Running
currentGen uint64 // generation of the mitmdump currently considered live
port int
uid uint32
cfg mitmproxy.Config // OnExit must NOT be set here; built per-Launch
nextGen uint64 // atomic; monotonic gen counter handed to each Launch
restartCh chan exitEvent
shutdownCh chan struct{} // closed by watchMitmproxy on ctx cancel; lets OnExit unblock during shutdown
}
func (m *mitmTransparent) getRunning() *mitmproxy.Running {
m.mu.Lock()
defer m.mu.Unlock()
return m.running
}
func (m *mitmTransparent) setRunning(r *mitmproxy.Running, gen uint64) {
m.mu.Lock()
defer m.mu.Unlock()
m.running = r
m.currentGen = gen
}
func (m *mitmTransparent) getCurrentGen() uint64 {
m.mu.Lock()
defer m.mu.Unlock()
return m.currentGen
}
// launchTagged starts mitmdump with an OnExit closure that publishes the death
// of this specific process (identified by gen) into restartCh.
//
// The send is blocking with shutdownCh as the only escape: dropping an exit
// event while the watcher is still running can leave egress in a silent dead
// state (the watcher would never see the death and never trigger a restart).
// Stale events from killed half-launched attempts are still cheap to discard
// downstream via the gen check in watchMitmproxy; we just must not lose them
// in transit. Shutdown is the only legitimate reason to give up on a send,
// and we log a warning when that happens so the drop is observable.
func launchTagged(cfg mitmproxy.Config, restartCh chan<- exitEvent, shutdownCh <-chan struct{}, gen uint64) (*mitmproxy.Running, error) {
cfg.OnExit = func(err error) {
select {
case restartCh <- exitEvent{gen: gen, err: err}:
case <-shutdownCh:
log.Warnf("[mitmproxy] dropping exit event during shutdown (gen=%d): %v", gen, err)
}
}
return mitmproxy.Launch(cfg)
}
// startMitmproxyTransparentIfEnabled starts mitmdump in transparent mode, waits for the listener, and installs OUTPUT REDIRECT, then syncs the CA.
func startMitmproxyTransparentIfEnabled() (*mitmTransparent, error) {
if !constants.IsTruthy(os.Getenv(constants.EnvMitmproxyTransparent)) {
return nil, nil
}
mpPort := constants.EnvIntOrDefault(constants.EnvMitmproxyPort, constants.DefaultMitmproxyPort)
mpUID, _, mpHome, err := mitmproxy.LookupUser(mitmproxy.RunAsUser)
if err != nil {
return nil, fmt.Errorf("lookup user %q: %w (ensure this user exists in the image)", mitmproxy.RunAsUser, err)
}
cfg := mitmproxy.Config{
ListenPort: mpPort,
UserName: mitmproxy.RunAsUser,
ScriptPaths: parseScriptPaths(os.Getenv(constants.EnvMitmproxyScript)),
}
// Buffer absorbs OnExit events from a retry storm so OnExit goroutines
// don't all park waiting for the watcher to drain. Correctness does not
// depend on the size: launchTagged uses a blocking send with shutdownCh
// as the only escape, so events cannot be silently dropped while the
// watcher is alive.
restartCh := make(chan exitEvent, 64)
shutdownCh := make(chan struct{})
const initialGen uint64 = 1
running, err := launchTagged(cfg, restartCh, shutdownCh, initialGen)
if err != nil {
return nil, fmt.Errorf("start mitmdump: %w", err)
}
waitAddr := fmt.Sprintf("127.0.0.1:%d", mpPort)
if err := mitmproxy.WaitListenPort(waitAddr, 15*time.Second); err != nil {
return nil, fmt.Errorf("wait listen %s: %w", waitAddr, err)
}
if err := iptables.SetupTransparentHTTP(mpPort, mpUID); err != nil {
return nil, fmt.Errorf("iptables transparent: %w", err)
}
log.Infof("mitmproxy: transparent intercept active (OUTPUT tcp 80,443 -> %d; trust mitm CA in clients)", mpPort)
if err := mitmproxy.SyncRootCA("", mpHome); err != nil {
return nil, fmt.Errorf("mitm CA export: %w", err)
}
return &mitmTransparent{
running: running,
currentGen: initialGen,
port: mpPort,
uid: mpUID,
cfg: cfg,
nextGen: initialGen,
restartCh: restartCh,
shutdownCh: shutdownCh,
}, nil
}
// watchMitmproxy monitors mitmdump for unexpected exits, logs the error, and restarts it.
// Must be called after startMitmproxyTransparentIfEnabled.
func (m *mitmTransparent) watchMitmproxy(ctx context.Context, gate *mitmproxy.HealthGate) {
// Closing shutdownCh on ctx cancel unblocks any OnExit closures that are
// parked on the (now-unread) restartCh send so they don't leak past
// shutdown.
safego.Go(func() {
<-ctx.Done()
close(m.shutdownCh)
})
safego.Go(func() {
for {
select {
case ev := <-m.restartCh:
select {
case <-ctx.Done():
return
default:
}
cur := m.getCurrentGen()
if ev.gen != cur {
// Stale event: a previous half-launched attempt that we
// killed is just now being reaped. The currently-live
// mitmdump is unaffected; ignore and keep watching.
log.Infof("[mitmproxy] ignoring stale exit event (gen=%d, current=%d): %v", ev.gen, cur, ev.err)
continue
}
log.Errorf("[mitmproxy] mitmdump exited (gen=%d): %v; restarting...", ev.gen, ev.err)
gate.SetReady(false)
m.restartWithBackoff(ctx, gate)
case <-ctx.Done():
return
}
}
})
}
// restartWithBackoff retries mitmdump launch indefinitely with exponential backoff
// (1s, 2s, 4s, ..., capped at 30s) until it succeeds or ctx is cancelled.
// Transient OOM / resource pressure must not leave egress in a permanent dead state.
//
// Each attempt is tagged with a fresh generation; setRunning publishes that
// generation as the "live" one. Exit events for older (killed) generations are
// filtered out by watchMitmproxy, so we do NOT drain restartCh here -- doing
// so could swallow a real death of the freshly-restarted mitmdump.
func (m *mitmTransparent) restartWithBackoff(ctx context.Context, gate *mitmproxy.HealthGate) {
const (
initialBackoff = time.Second
maxBackoff = 30 * time.Second
)
backoff := initialBackoff
waitAddr := fmt.Sprintf("127.0.0.1:%d", m.cfg.ListenPort)
for attempt := 1; ; attempt++ {
select {
case <-ctx.Done():
return
default:
}
gen := atomic.AddUint64(&m.nextGen, 1)
newRunning, launchErr := launchTagged(m.cfg, m.restartCh, m.shutdownCh, gen)
if launchErr == nil {
if waitErr := mitmproxy.WaitListenPort(waitAddr, 15*time.Second); waitErr == nil {
m.setRunning(newRunning, gen)
gate.SetReady(true)
log.Infof("[mitmproxy] mitmdump restarted (pid %d, gen %d, attempt %d)", newRunning.Cmd.Process.Pid, gen, attempt)
return
} else {
log.Errorf("[mitmproxy] restart attempt %d (gen %d): wait listen %s: %v", attempt, gen, waitAddr, waitErr)
// GracefulShutdown SIGTERMs then SIGKILLs and waits for reap, so
// the listen port is released before the next attempt's Launch
// races to bind it. Direct Process.Kill returns immediately and
// can cause spurious WaitListenPort failures on port contention.
mitmproxy.GracefulShutdown(newRunning, time.Second)
}
} else {
log.Errorf("[mitmproxy] restart attempt %d (gen %d): launch failed: %v", attempt, gen, launchErr)
}
log.Warnf("[mitmproxy] restart attempt %d failed; retrying in %s", attempt, backoff)
select {
case <-ctx.Done():
return
case <-time.After(backoff):
}
if backoff < maxBackoff {
backoff *= 2
if backoff > maxBackoff {
backoff = maxBackoff
}
}
}
}
func parseScriptPaths(raw string) []string {
if strings.TrimSpace(raw) == "" {
return nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
if s := strings.TrimSpace(p); s != "" {
out = append(out, s)
}
}
return out
}