Merge pull request #6858 from tonistiigi/proxy-unset-fallback-fix

network: fix proxy default egress
This commit is contained in:
CrazyMax
2026-06-10 19:03:58 +02:00
committed by GitHub
3 changed files with 180 additions and 29 deletions

View File

@@ -160,7 +160,6 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
testCgroupParent,
testLinuxResources,
testNetworkMode,
testProxyNetworkNoRootless,
testFrontendMetadataReturn,
testFrontendUseSolveResults,
testSSHMount,
@@ -325,11 +324,16 @@ func testIntegration(t *testing.T, funcs ...func(t *testing.T, sb integration.Sa
)
integration.Run(t, integration.TestFuncs(
testProxyNetworkNoRootless,
testProxyNetworkModesNoRootless,
testProxyNetworkDefaultEgressNoRootless,
),
mirrors,
integration.WithMatrix("netmode", map[string]any{
"bridge": proxyBridgeNetwork,
"default": proxyDefaultNetwork,
"host": proxyHostNetwork,
"bridge": proxyBridgeNetwork,
"default-no-cni": proxyDefaultNetworkNoCNI,
}),
)
@@ -12996,6 +13000,45 @@ networkMode = "bridge"
[worker.containerd]
networkMode = "bridge"
`, nil
}
type netModeProxyDefault struct{}
func (*netModeProxyDefault) UpdateConfigFile(in string) (string, func() error) {
return in + `
insecure-entitlements = ["network.host"]
`, nil
}
type netModeProxyDefaultNoCNI struct{}
func (*netModeProxyDefaultNoCNI) UpdateConfigFile(in string) (string, func() error) {
return in + `
insecure-entitlements = ["network.host"]
[worker.oci]
cniConfigPath = "/tmp/buildkit-missing-cni.json"
[worker.containerd]
cniConfigPath = "/tmp/buildkit-missing-cni.json"
`, nil
}
type netModeProxyHost struct{}
func (*netModeProxyHost) UpdateConfigFile(in string) (string, func() error) {
return in + `
insecure-entitlements = ["network.host"]
[worker.oci]
networkMode = "host"
[worker.containerd]
networkMode = "host"
`, nil
}
@@ -13024,10 +13067,13 @@ nameservers = ["10.11.0.1"]
}
var (
hostNetwork integration.ConfigUpdater = &netModeHost{}
defaultNetwork integration.ConfigUpdater = &netModeDefault{}
proxyBridgeNetwork integration.ConfigUpdater = &netModeProxyBridge{}
bridgeDNSNetwork integration.ConfigUpdater = &netModeBridgeDNS{}
hostNetwork integration.ConfigUpdater = &netModeHost{}
defaultNetwork integration.ConfigUpdater = &netModeDefault{}
proxyDefaultNetwork integration.ConfigUpdater = &netModeProxyDefault{}
proxyDefaultNetworkNoCNI integration.ConfigUpdater = &netModeProxyDefaultNoCNI{}
proxyBridgeNetwork integration.ConfigUpdater = &netModeProxyBridge{}
proxyHostNetwork integration.ConfigUpdater = &netModeProxyHost{}
bridgeDNSNetwork integration.ConfigUpdater = &netModeBridgeDNS{}
)
func fixedWriteCloser(wc io.WriteCloser) filesync.FileOutputFunc {

View File

@@ -281,6 +281,7 @@ func testProxyNetworkModesNoRootless(t *testing.T, sb integration.Sandbox) {
internetURL := "http://example.com/"
allowedHost := []string{entitlements.EntitlementNetworkHost.String()}
defaultHasHostLoopback := proxyNetModeDefaultHasHostLoopback(sb)
// Host mode without the network.host entitlement should be rejected before exec starts.
hostWithoutEntitlement := llb.Image("alpine:latest").
@@ -319,22 +320,34 @@ func testProxyNetworkModesNoRootless(t *testing.T, sb integration.Sandbox) {
return <-logsCh, err
}
// Bridge proxy egress should allow normal external network access.
logOutput, err := runProxySolve("bridge internet", llb.Image("alpine:latest").
// Default proxy egress should allow normal external network access.
logOutput, err := runProxySolve("default internet", llb.Image("alpine:latest").
Run(llb.Shlexf(`wget -q -O /tmp/internet %s`, internetURL), llb.IgnoreCache).
Root(), nil)
require.NoError(t, err)
require.Contains(t, logOutput, "proxy network requests:\n- GET "+internetURL+" -> 200")
require.Equal(t, int32(0), hostHit.Load())
// Bridge proxy egress should not reach services bound to buildkitd host loopback.
logOutput, err = runProxySolve("bridge host", llb.Image("alpine:latest").
// Clear NO_PROXY so the localhost URL is forced through the BuildKit proxy.
Run(llb.Shlexf(`sh -c '! env NO_PROXY= no_proxy= wget -T 2 -q -O- %s'`, hostPath), llb.IgnoreCache).
Root(), nil)
require.NoError(t, err)
require.Contains(t, logOutput, "proxy network requests:\n- GET "+hostPath+" -> 502")
require.Equal(t, int32(0), hostHit.Load())
expectedHostHits := int32(0)
if defaultHasHostLoopback {
// Proxy UNSET should follow the worker default provider, including host fallback/default.
logOutput, err = runProxySolve("default host fallback", llb.Image("alpine:latest").
Run(llb.Shlexf(`sh -c 'env NO_PROXY= no_proxy= wget -q -O- %s | grep "proxy host ok"'`, hostPath), llb.IgnoreCache).
Root(), nil)
require.NoError(t, err)
require.Contains(t, logOutput, "proxy network requests:\n- GET "+hostPath+" -> 200")
expectedHostHits = 1
require.Equal(t, expectedHostHits, hostHit.Load())
} else {
// Bridge proxy egress should not reach services bound to buildkitd host loopback.
logOutput, err = runProxySolve("bridge host", llb.Image("alpine:latest").
// Clear NO_PROXY so the localhost URL is forced through the BuildKit proxy.
Run(llb.Shlexf(`sh -c '! env NO_PROXY= no_proxy= wget -T 2 -q -O- %s'`, hostPath), llb.IgnoreCache).
Root(), nil)
require.NoError(t, err)
require.Contains(t, logOutput, "proxy network requests:\n- GET "+hostPath+" -> 502")
require.Equal(t, expectedHostHits, hostHit.Load())
}
// Host proxy egress is allowed only with the network.host entitlement.
logOutput, err = runProxySolve("host allowed", llb.Image("alpine:latest").
@@ -342,7 +355,8 @@ func testProxyNetworkModesNoRootless(t *testing.T, sb integration.Sandbox) {
Root(), allowedHost)
require.NoError(t, err)
require.Contains(t, logOutput, "proxy network requests:\n- GET "+hostPath+" -> 200")
require.Equal(t, int32(1), hostHit.Load())
expectedHostHits++
require.Equal(t, expectedHostHits, hostHit.Load())
// None mode should not inject proxy env or allow external network access.
logOutput, err = runProxySolve("none internet", llb.Image("alpine:latest").
@@ -357,7 +371,104 @@ func testProxyNetworkModesNoRootless(t *testing.T, sb integration.Sandbox) {
Root(), nil)
require.NoError(t, err)
require.NotContains(t, logOutput, hostPath)
require.Equal(t, int32(1), hostHit.Load())
require.Equal(t, expectedHostHits, hostHit.Load())
}
func testProxyNetworkDefaultEgressNoRootless(t *testing.T, sb integration.Sandbox) {
integration.SkipOnPlatform(t, "windows")
if sb.Rootless() {
t.SkipNow()
}
ctx := sb.Context()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
hostSrv, hostURL := newProxyHTTPServer(t, "127.0.0.1", "127.0.0.1", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/host-default", "/denied":
default:
http.NotFound(w, r)
return
}
_, _ = w.Write([]byte("proxy host default ok\n"))
}))
defer hostSrv.Close()
runProxySolve := func(st llb.State, opt SolveOpt) (string, error) {
t.Helper()
def, err := st.Marshal(ctx)
require.NoError(t, err)
statusCh := make(chan *SolveStatus)
logsCh := make(chan string, 1)
go func() {
var b strings.Builder
for st := range statusCh {
for _, l := range st.Logs {
b.Write(l.Data)
}
}
logsCh <- b.String()
}()
_, err = c.Solve(ctx, def, opt, statusCh)
return <-logsCh, err
}
defaultHasHostLoopback := proxyNetModeDefaultHasHostLoopback(sb)
hostDefaultCmd := llb.Shlexf(`sh -c 'env NO_PROXY= no_proxy= wget -q -O- %s/host-default | grep "proxy host default ok"'`, hostURL)
if !defaultHasHostLoopback {
hostDefaultCmd = llb.Shlexf(`sh -c '! env NO_PROXY= no_proxy= wget -T 2 -q -O- %s/host-default'`, hostURL)
}
st := llb.Image("alpine:latest").
Run(hostDefaultCmd, llb.IgnoreCache).
Root()
logOutput, err := runProxySolve(st, SolveOpt{
ProxyNetwork: true,
})
require.NoError(t, err)
if defaultHasHostLoopback {
require.Contains(t, logOutput, "proxy network requests:\n- GET "+hostURL+"/host-default -> 200")
} else {
require.Contains(t, logOutput, "proxy network requests:\n- GET "+hostURL+"/host-default -> 502")
}
var checked atomic.Int32
denyProvider := policysession.NewPolicyProvider(func(ctx context.Context, req *policysession.CheckPolicyRequest) (*policysession.DecisionResponse, *pb.ResolveSourceMetaRequest, error) {
if req.Source.Source.Identifier == hostURL+"/denied" {
checked.Add(1)
return &policysession.DecisionResponse{
Action: sourcepolicypb.PolicyAction_DENY,
}, nil, nil
}
return &policysession.DecisionResponse{
Action: sourcepolicypb.PolicyAction_ALLOW,
}, nil, nil
})
deny := llb.Image("alpine:latest").
Run(llb.Shlexf(`env NO_PROXY= no_proxy= wget -S -O- %s/denied`, hostURL), llb.IgnoreCache).
Root()
logOutput, err = runProxySolve(deny, SolveOpt{
ProxyNetwork: true,
SourcePolicyProvider: denyProvider,
})
require.Error(t, err)
require.ErrorContains(t, err, "exit code: 1")
require.Contains(t, logOutput, "HTTP/1.1 403 Forbidden")
require.Equal(t, int32(1), checked.Load())
require.NotContains(t, err.Error(), "unknown proxy egress network mode UNSET")
}
func proxyNetModeDefaultHasHostLoopback(sb integration.Sandbox) bool {
switch sb.Value("netmode").(type) {
case *netModeProxyDefaultNoCNI, *netModeProxyHost:
return true
default:
return false
}
}
func newProxyHTTPServer(t *testing.T, listenHost, urlHost string, handler http.Handler) (*httptest.Server, string) {

View File

@@ -69,22 +69,16 @@ func Providers(opt Opt) (providers map[pb.NetMode]network.Provider, proxyProvide
pb.NetMode_NONE: network.NewNoneProvider(),
}
if proxyprovider.Supported() {
proxyEgressProviders := map[pb.NetMode]network.Provider{}
var ownedProxyEgressProviders []network.Provider
if resolvedMode == "cni" {
proxyEgressProviders[pb.NetMode_UNSET] = defaultProvider
} else if bridgeProvider, err := getBridgeProvider(opt.CNI); err == nil {
proxyEgressProviders[pb.NetMode_UNSET] = bridgeProvider
ownedProxyEgressProviders = append(ownedProxyEgressProviders, bridgeProvider)
proxyEgressProviders := map[pb.NetMode]network.Provider{
pb.NetMode_UNSET: defaultProvider,
}
if hostProvider, ok := getHostProvider(); ok {
proxyEgressProviders[pb.NetMode_HOST] = hostProvider
}
proxyProvider, err = proxyprovider.New(proxyprovider.Opt{
Root: opt.CNI.Root,
PoolSize: opt.CNI.PoolSize,
EgressProviders: proxyEgressProviders,
OwnedEgressProviders: ownedProxyEgressProviders,
Root: opt.CNI.Root,
PoolSize: opt.CNI.PoolSize,
EgressProviders: proxyEgressProviders,
})
if err != nil {
return nil, nil, resolvedMode, err