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
1 change: 1 addition & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ func testIntegration(t *testing.T, funcs ...func(t *testing.T, sb integration.Sa
integration.Run(t, integration.TestFuncs(
// policy_test.go
testProxyNetworkNoRootless,
testProxyNetworkGatewayExecEnvNoRootless,
testProxyNetworkModesNoRootless,
testProxyNetworkDefaultEgressNoRootless,
),
Expand Down
83 changes: 83 additions & 0 deletions client/policy_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package client

import (
"bytes"
"context"
"crypto"
"crypto/sha256"
Expand Down Expand Up @@ -34,6 +35,7 @@ import (
sourcepolicypb "github.com/moby/buildkit/sourcepolicy/pb"
"github.com/moby/buildkit/sourcepolicy/policysession"
"github.com/moby/buildkit/util/entitlements"
"github.com/moby/buildkit/util/iohelper"
"github.com/moby/buildkit/util/pgpsign"
"github.com/moby/buildkit/util/testutil/integration"
"github.com/moby/buildkit/util/testutil/workers"
Expand Down Expand Up @@ -253,6 +255,87 @@ func testProxyNetworkNoRootless(t *testing.T, sb integration.Sandbox) {
require.Equal(t, "unsuccessful_response", materialsErr.Incomplete[0].Reason)
}

func testProxyNetworkGatewayExecEnvNoRootless(t *testing.T, sb integration.Sandbox) {
integration.SkipOnPlatform(t, "windows")

ctx := sb.Context()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
childEnv := bytes.NewBuffer(nil)

_, err = c.Build(ctx, SolveOpt{ProxyNetwork: true}, "proxy-network-gateway-exec-env", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
def, err := llb.Image("busybox:latest").Marshal(ctx)
if err != nil {
return nil, err
}
res, err := c.Solve(ctx, gateway.SolveRequest{Definition: def.ToPB()})
if err != nil {
return nil, err
}
ctr, err := c.NewContainer(ctx, gateway.NewContainerRequest{
Mounts: []gateway.Mount{{
Dest: "/",
MountType: opspb.MountType_BIND,
Ref: res.Ref,
}},
})
if err != nil {
return nil, err
}
pid1, err := ctr.Start(ctx, gateway.StartRequest{
Args: []string{"sleep", "30"},
Env: []string{
"INIT_ONLY=must-not-leak",
"ALL_PROXY=http://initial-process-proxy.invalid",
},
})
if err != nil {
_ = ctr.Release(context.WithoutCancel(ctx))
return nil, err
}
defer func() {
_ = ctr.Release(context.WithoutCancel(ctx))
_ = pid1.Wait()
}()

pid2, err := ctr.Start(ctx, gateway.StartRequest{
Args: []string{"env"},
Env: []string{
"CHILD_ENV=preserved",
"ALL_PROXY=http://child-process-proxy.invalid",
},
Stdout: &iohelper.NopWriteCloser{Writer: childEnv},
})
if err != nil {
return nil, err
}
if err := pid2.Wait(); err != nil {
return nil, err
}
return &gateway.Result{}, nil
}, nil)
require.NoError(t, err)

env := strings.Split(strings.TrimSpace(childEnv.String()), "\n")
require.Contains(t, env, "CHILD_ENV=preserved")
require.NotContains(t, env, "ALL_PROXY=http://child-process-proxy.invalid")
require.NotContains(t, env, "ALL_PROXY=http://initial-process-proxy.invalid")
require.NotContains(t, env, "INIT_ONLY=must-not-leak")
values := make(map[string]string, len(env))
for _, entry := range env {
name, value, ok := strings.Cut(entry, "=")
if ok {
values[name] = value
}
}
for _, name := range []string{"HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy", "NO_PROXY", "no_proxy"} {
require.NotEmptyf(t, values[name], "%s is not set in the gateway exec environment:\n%s", name, childEnv.String())
}
require.Equal(t, values["HTTP_PROXY"], values["ALL_PROXY"])
require.Equal(t, values["http_proxy"], values["all_proxy"])
}

func testProxyNetworkModesNoRootless(t *testing.T, sb integration.Sandbox) {
integration.SkipOnPlatform(t, "windows")
workers.CheckFeatureCompat(t, sb, workers.FeatureCNINetwork)
Expand Down
51 changes: 48 additions & 3 deletions docs/proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ process:
```text
HTTP_PROXY
HTTPS_PROXY
ALL_PROXY
http_proxy
https_proxy
all_proxy
NO_PROXY
no_proxy
```
Expand All @@ -91,6 +93,49 @@ BuildKit also injects a generated CA certificate into common Linux trust bundle
locations for the duration of the exec. This lets HTTPS requests using the
system trust store pass through the BuildKit proxy.

## Upstream proxies

To use an upstream proxy, set `HTTP_PROXY` and/or `HTTPS_PROXY` in the
`buildkitd` environment and enable proxy networking:

```bash
HTTP_PROXY=http://proxy.example:3128 \
HTTPS_PROXY=http://proxy.example:3128 \
NO_PROXY=localhost,127.0.0.1,.example.internal \
buildkitd --proxy-network
```

BuildKit uses Go's standard proxy environment handling:

| Variable pair | Purpose |
| --- | --- |
| `HTTP_PROXY`, `http_proxy` | Selects the upstream proxy for HTTP destinations. |
| `HTTPS_PROXY`, `https_proxy` | Selects the upstream proxy for HTTPS destinations. |
| `NO_PROXY`, `no_proxy` | Lists destinations that bypass the upstream proxy. |

For each pair, BuildKit uses the uppercase variable when it is non-empty and
falls back to the lowercase variable. The HTTP and HTTPS settings are
independent: `HTTP_PROXY` and `http_proxy` do not apply to HTTPS destinations.

BuildKit injects `ALL_PROXY` and `all_proxy` into proxy-network execs. It does
not read these variables from the `buildkitd` environment when it configures
upstream routing.

Proxy values can be complete `http://`, `https://`, `socks5://`, or
`socks5h://` URLs. A bare `host[:port]` uses HTTP.

`NO_PROXY` is a comma-separated list of domain names, IP addresses, and CIDR
prefixes. Domain names and IP addresses can include a port. When a destination
matches the list, BuildKit's proxy connects to it directly. A value of `*`
makes direct connections to all destinations. `NO_PROXY` controls how the
BuildKit proxy reaches the destination. It does not change the proxy variables
in the exec.

These settings apply to all proxy-network execs. Proxy settings passed to an
individual exec do not change upstream routing because its proxy variables
point to BuildKit's internal proxy. If an upstream proxy URL is invalid, the
proxy-network exec fails to start instead of connecting directly.

## Request capture and logs

The proxy records network requests made by exec steps. Build output includes a
Expand Down Expand Up @@ -126,6 +171,6 @@ The current implementation is Linux-focused. Rootless workers also have the
usual rootless networking limitations, where worker networking may behave like
host networking.

Applications that ignore `HTTP_PROXY` and `HTTPS_PROXY`, use custom trust
stores, or open raw TCP connections cannot bypass the proxy. That traffic is
blocked instead of being captured.
Applications that ignore the injected proxy environment variables, use custom
trust stores, or open raw TCP connections cannot bypass the proxy. That traffic
is blocked instead of being captured.
10 changes: 7 additions & 3 deletions executor/containerdexecutor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.M
}
defer namespace.Close()
if proxyNS, ok := namespace.(network.ProxyNamespace); ok {
meta.Env = append(meta.Env, proxyNS.ProxyEnv()...)
meta.Env = executor.ReplaceEnv(meta.Env, proxyNS.ProxyEnv())
cleanProxyCA, err := executor.InjectProxyCA(details.rootfsPath, proxyNS.ProxyCACert())
if err != nil {
return nil, err
Expand Down Expand Up @@ -315,6 +315,10 @@ func (w *containerdExecutor) Exec(ctx context.Context, id string, process execut
}

proc := spec.Process
if meta.Proxy != nil && len(meta.Env) > 0 {
meta.Env = executor.ReplaceEnv(meta.Env, network.FilterProxyEnv(proc.Env))
process.Meta = meta
}
if meta.User != "" {
userSpec, err := getUserSpec(meta.User, details.rootfsPath)
if err != nil {
Expand All @@ -332,8 +336,8 @@ func (w *containerdExecutor) Exec(ctx context.Context, id string, process execut
if meta.Cwd != "" {
spec.Process.Cwd = meta.Cwd
}
if len(process.Meta.Env) > 0 {
spec.Process.Env = process.Meta.Env
if len(meta.Env) > 0 {
proc.Env = meta.Env
}

fixProcessOutput(&process)
Expand Down
21 changes: 21 additions & 0 deletions executor/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package executor

import "strings"

// ReplaceEnv removes entries whose names are present in replacement, then
// appends replacement in order.
func ReplaceEnv(env, replacement []string) []string {
names := make(map[string]struct{}, len(replacement))
for _, entry := range replacement {
name, _, _ := strings.Cut(entry, "=")
names[name] = struct{}{}
}
out := make([]string, 0, len(env)+len(replacement))
for _, entry := range env {
name, _, _ := strings.Cut(entry, "=")
if _, ok := names[name]; !ok {
out = append(out, entry)
}
}
return append(out, replacement...)
}
30 changes: 30 additions & 0 deletions executor/env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package executor

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestReplaceEnv(t *testing.T) {
env := []string{
"FOO=one",
"HTTP_PROXY=http://upstream.example",
"http_proxy=http://upstream.example",
"NO_PROXY=example.com",
"BAR=two",
}
replacement := []string{
"HTTP_PROXY=http://buildkit-proxy",
"http_proxy=http://buildkit-proxy",
"NO_PROXY=localhost",
}

require.Equal(t, []string{
"FOO=one",
"BAR=two",
"HTTP_PROXY=http://buildkit-proxy",
"http_proxy=http://buildkit-proxy",
"NO_PROXY=localhost",
}, ReplaceEnv(env, replacement))
}
23 changes: 14 additions & 9 deletions executor/runcexecutor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ func (w *runcExecutor) Run(ctx context.Context, id string, root executor.Mount,
return nil, err
}
if proxyNS, ok := namespace.(network.ProxyNamespace); ok {
meta.Env = append(meta.Env, proxyNS.ProxyEnv()...)
meta.Env = executor.ReplaceEnv(meta.Env, proxyNS.ProxyEnv())
}
doReleaseNetwork := true
defer func() {
Expand Down Expand Up @@ -436,6 +436,7 @@ func exitError(ctx context.Context, cgroupPath string, err error, validExitCodes
}

func (w *runcExecutor) Exec(ctx context.Context, id string, process executor.ProcessInfo) (err error) {
meta := process.Meta
// first verify the container is running, if we get an error assume the container
// is in the process of being created and check again every 100ms or until
// context is canceled.
Expand Down Expand Up @@ -479,9 +480,13 @@ func (w *runcExecutor) Exec(ctx context.Context, id string, process executor.Pro
if _, err := dec.Token(); !errors.Is(err, io.EOF) {
return errors.Errorf("unexpected data after JSON spec object")
}
if meta.Proxy != nil && len(meta.Env) > 0 {
meta.Env = executor.ReplaceEnv(meta.Env, network.FilterProxyEnv(spec.Process.Env))
process.Meta = meta
}

if process.Meta.User != "" {
uid, gid, sgids, err := oci.GetUser(state.Rootfs, process.Meta.User)
if meta.User != "" {
uid, gid, sgids, err := oci.GetUser(state.Rootfs, meta.User)
if err != nil {
return err
}
Expand All @@ -492,14 +497,14 @@ func (w *runcExecutor) Exec(ctx context.Context, id string, process executor.Pro
}
}

spec.Process.Terminal = process.Meta.Tty
spec.Process.Args = process.Meta.Args
if process.Meta.Cwd != "" {
spec.Process.Cwd = process.Meta.Cwd
spec.Process.Terminal = meta.Tty
spec.Process.Args = meta.Args
if meta.Cwd != "" {
spec.Process.Cwd = meta.Cwd
}

if len(process.Meta.Env) > 0 {
spec.Process.Env = process.Meta.Env
if len(meta.Env) > 0 {
spec.Process.Env = meta.Env
}

err = w.exec(ctx, id, spec.Process, process, nil)
Expand Down
48 changes: 48 additions & 0 deletions util/network/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"io"
"slices"
"strings"
"sync"

"github.com/moby/buildkit/solver/pb"
Expand Down Expand Up @@ -34,6 +35,53 @@ type ProxyNamespace interface {
ProxyCACert() []byte
}

var proxyEnvNames = [...]struct {
name string
noProxy bool
}{
{name: "HTTP_PROXY"},
{name: "HTTPS_PROXY"},
{name: "ALL_PROXY"},
{name: "http_proxy"},
{name: "https_proxy"},
{name: "all_proxy"},
{name: "NO_PROXY", noProxy: true},
{name: "no_proxy", noProxy: true},
}

// ProxyEnv returns the environment entries used to configure a process to use
// a BuildKit-owned HTTP(S) proxy.
func ProxyEnv(proxy, noProxy string) []string {
out := make([]string, 0, len(proxyEnvNames))
for _, env := range proxyEnvNames {
value := proxy
if env.noProxy {
value = noProxy
}
out = append(out, env.name+"="+value)
}
return out
}

// FilterProxyEnv returns entries whose names are emitted by ProxyEnv, preserving
// their original order.
func FilterProxyEnv(env []string) []string {
out := make([]string, 0, len(proxyEnvNames))
for _, entry := range env {
name, _, ok := strings.Cut(entry, "=")
if !ok {
continue
}
for _, proxyEnv := range proxyEnvNames {
if name == proxyEnv.name {
out = append(out, entry)
break
}
}
}
return out
}

type ProxyMaterial struct {
URL string
Digest digest.Digest
Expand Down
Loading
Loading