Merge pull request #7002 from concourse/issue/6720

containerd: add flag to allow host access
This commit is contained in:
Taylor Silva 2021-05-20 13:44:24 -04:00 committed by GitHub
commit 5c2e643d81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 398 additions and 103 deletions

View File

@ -159,9 +159,9 @@ func (b *GardenBackend) Start() (err error) {
return fmt.Errorf("client init: %w", err)
}
err = b.network.SetupRestrictedNetworks()
err = b.network.SetupHostNetwork()
if err != nil {
return fmt.Errorf("setup restricted networks failed: %w", err)
return fmt.Errorf("setup host network failed: %w", err)
}
return

View File

@ -488,7 +488,7 @@ func (s *BackendSuite) TestStartInitsClientAndSetsUpRestrictedNetworks() {
err := s.backend.Start()
s.NoError(err)
s.Equal(1, s.client.InitCallCount())
s.Equal(1, s.network.SetupRestrictedNetworksCallCount())
s.Equal(1, s.network.SetupHostNetworkCallCount())
}
func (s *BackendSuite) TestStartInitError() {

View File

@ -154,12 +154,21 @@ func WithCNIFileStore(f FileStore) CNINetworkOpt {
// WithRestrictedNetworks defines the network ranges that containers will be restricted
// from accessing.
//
func WithRestrictedNetworks(restrictedNetworks []string) CNINetworkOpt {
return func(n *cniNetwork) {
n.restrictedNetworks = restrictedNetworks
}
}
// WithAllowHostAccess allows containers to talk to the host
//
func WithAllowHostAccess() CNINetworkOpt {
return func(n *cniNetwork) {
n.allowHostAccess = true
}
}
// WithIptables allows for a custom implementation of the iptables.Iptables interface
// to be provided.
func WithIptables(ipt iptables.Iptables) CNINetworkOpt {
@ -175,6 +184,7 @@ type cniNetwork struct {
nameServers []string
binariesDir string
restrictedNetworks []string
allowHostAccess bool
ipt iptables.Iptables
}
@ -225,6 +235,22 @@ func NewCNINetwork(opts ...CNINetworkOpt) (*cniNetwork, error) {
return n, nil
}
func (n cniNetwork) SetupHostNetwork() error {
err := n.setupRestrictedNetworks()
if err != nil {
return err
}
if !n.allowHostAccess {
err = n.restrictHostAccess()
if err != nil {
return err
}
}
return nil
}
func (n cniNetwork) SetupMounts(handle string) ([]specs.Mount, error) {
if handle == "" {
return nil, ErrInvalidInput("empty handle")
@ -266,22 +292,23 @@ func (n cniNetwork) SetupMounts(handle string) ([]specs.Mount, error) {
}, nil
}
func (n cniNetwork) SetupRestrictedNetworks() error {
const tableName = "filter"
err := n.ipt.CreateChainOrFlushIfExists(tableName, ipTablesAdminChainName)
const filterTable = "filter"
func (n cniNetwork) setupRestrictedNetworks() error {
err := n.ipt.CreateChainOrFlushIfExists(filterTable, ipTablesAdminChainName)
if err != nil {
return fmt.Errorf("create chain or flush if exists failed: %w", err)
}
// Optimization that allows packets of ESTABLISHED and RELATED connections to go through without further rule matching
err = n.ipt.AppendRule(tableName, ipTablesAdminChainName, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT")
err = n.ipt.AppendRule(filterTable, ipTablesAdminChainName, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT")
if err != nil {
return fmt.Errorf("appending accept rule for RELATED & ESTABLISHED connections failed: %w", err)
}
for _, restrictedNetwork := range n.restrictedNetworks {
// Create REJECT rule in admin chain
err = n.ipt.AppendRule(tableName, ipTablesAdminChainName, "-d", restrictedNetwork, "-j", "REJECT")
err = n.ipt.AppendRule(filterTable, ipTablesAdminChainName, "-d", restrictedNetwork, "-j", "REJECT")
if err != nil {
return fmt.Errorf("appending reject rule for restricted network %s failed: %w", restrictedNetwork, err)
}
@ -303,6 +330,20 @@ func (n cniNetwork) generateResolvConfContents() ([]byte, error) {
return []byte(contents), err
}
func (n cniNetwork) restrictHostAccess() error {
err := n.ipt.CreateChainOrFlushIfExists(filterTable, "INPUT")
if err != nil {
return fmt.Errorf("create chain or flush if exists failed: %w", err)
}
err = n.ipt.AppendRule(filterTable, "INPUT", "-i", n.config.BridgeName, "-j", "REJECT", "--reject-with", "icmp-host-prohibited")
if err != nil {
return fmt.Errorf("error appending iptables rule: %w", err)
}
return nil
}
func (n cniNetwork) Add(ctx context.Context, task containerd.Task) error {
if task == nil {
return ErrInvalidInput("nil task")

View File

@ -3,6 +3,7 @@ package runtime_test
import (
"context"
"errors"
"reflect"
"strings"
"github.com/concourse/concourse/worker/runtime"
@ -139,33 +140,108 @@ func (s *CNINetworkSuite) TestSetupMountsCallsStoreWithoutNameServers() {
s.Equal(resolvConfContents, []byte(contents))
}
func (s *CNINetworkSuite) TestSetupRestrictedNetworksCreatesEmptyAdminChain() {
network, err := runtime.NewCNINetwork(
runtime.WithRestrictedNetworks([]string{"1.1.1.1", "8.8.8.8"}),
runtime.WithIptables(s.iptables),
)
func (s *CNINetworkSuite) TestSetupHostNetwork() {
testCases := map[string]struct {
cniNetworkSetup func() (runtime.Network, error)
expectedTableName string
expectedChainName string
expectedRuleSpec []string
}{
"flushes the CONCOURSE-OPERATOR chain": {
cniNetworkSetup: func() (runtime.Network, error) {
return runtime.NewCNINetwork(
runtime.WithIptables(s.iptables),
)
},
expectedTableName: "filter",
expectedChainName: "CONCOURSE-OPERATOR",
},
"adds rule to CONCOURSE-OPERATOR chain for accepting established connections": {
cniNetworkSetup: func() (runtime.Network, error) {
return runtime.NewCNINetwork(
runtime.WithIptables(s.iptables),
)
},
expectedTableName: "filter",
expectedChainName: "CONCOURSE-OPERATOR",
expectedRuleSpec: []string{"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"},
},
"adds rule to CONCOURSE-OPERATOR chain to reject IP 1.1.1.1": {
cniNetworkSetup: func() (runtime.Network, error) {
return runtime.NewCNINetwork(
runtime.WithRestrictedNetworks([]string{"1.1.1.1", "8.8.8.8"}),
runtime.WithIptables(s.iptables),
)
},
expectedTableName: "filter",
expectedChainName: "CONCOURSE-OPERATOR",
expectedRuleSpec: []string{"-d", "1.1.1.1", "-j", "REJECT"},
},
"adds rule to CONCOURSE-OPERATOR chain to reject IP 8.8.8.8": {
cniNetworkSetup: func() (runtime.Network, error) {
return runtime.NewCNINetwork(
runtime.WithRestrictedNetworks([]string{"1.1.1.1", "8.8.8.8"}),
runtime.WithIptables(s.iptables),
)
},
expectedTableName: "filter",
expectedChainName: "CONCOURSE-OPERATOR",
expectedRuleSpec: []string{"-d", "8.8.8.8", "-j", "REJECT"},
},
"flushes the INPUT chain": {
cniNetworkSetup: func() (runtime.Network, error) {
return runtime.NewCNINetwork(
runtime.WithIptables(s.iptables),
)
},
expectedTableName: "filter",
expectedChainName: "INPUT",
},
"adds rule to INPUT chain to block host access by default": {
cniNetworkSetup: func() (runtime.Network, error) {
return runtime.NewCNINetwork(
runtime.WithIptables(s.iptables),
)
},
expectedTableName: "filter",
expectedChainName: "INPUT",
expectedRuleSpec: []string{"-i", "concourse0", "-j", "REJECT", "--reject-with", "icmp-host-prohibited"},
},
}
err = network.SetupRestrictedNetworks()
s.NoError(err)
for description, testCase := range testCases {
network, err := testCase.cniNetworkSetup()
s.NoError(err)
err = network.SetupHostNetwork()
s.NoError(err)
tablename, chainName := s.iptables.CreateChainOrFlushIfExistsArgsForCall(0)
s.Equal(tablename, "filter")
s.Equal(chainName, "CONCOURSE-OPERATOR")
foundExpected := false
tablename, chainName, rulespec := s.iptables.AppendRuleArgsForCall(0)
s.Equal(tablename, "filter")
s.Equal(chainName, "CONCOURSE-OPERATOR")
s.Equal(rulespec, []string{"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"})
if testCase.expectedRuleSpec == nil {
// Test cases to check if correct chain is created
numOfCalls := s.iptables.CreateChainOrFlushIfExistsCallCount()
for i := 0; i < numOfCalls; i++ {
tablename, chainName := s.iptables.CreateChainOrFlushIfExistsArgsForCall(i)
if tablename == testCase.expectedTableName && chainName == testCase.expectedChainName {
foundExpected = true
break
}
}
} else {
// Test cases to check if correct rule is appended
numOfCalls := s.iptables.AppendRuleCallCount()
for i := 0; i < numOfCalls; i++ {
tablename, chainName, rulespec := s.iptables.AppendRuleArgsForCall(i)
if tablename == testCase.expectedTableName && chainName == testCase.expectedChainName && reflect.DeepEqual(rulespec, testCase.expectedRuleSpec) {
foundExpected = true
break
}
}
tablename, chainName, rulespec = s.iptables.AppendRuleArgsForCall(1)
s.Equal(tablename, "filter")
s.Equal(chainName, "CONCOURSE-OPERATOR")
s.Equal(rulespec, []string{"-d", "1.1.1.1", "-j", "REJECT"})
}
tablename, chainName, rulespec = s.iptables.AppendRuleArgsForCall(2)
s.Equal(tablename, "filter")
s.Equal(chainName, "CONCOURSE-OPERATOR")
s.Equal(rulespec, []string{"-d", "8.8.8.8", "-j", "REJECT"})
s.Equal(foundExpected, true, description)
}
}
func (s *CNINetworkSuite) TestAddNilTask() {

View File

@ -4,6 +4,9 @@ import (
"bytes"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
@ -100,7 +103,7 @@ func (s *IntegrationSuite) TearDownSuite() {
s.NoError(os.RemoveAll(s.tmpDir))
}
func (s *IntegrationSuite) SetupTest() {
func (s *IntegrationSuite) BeforeTest(suiteName, testName string) {
var (
err error
namespace = "test"
@ -138,7 +141,7 @@ func (s *IntegrationSuite) setupRootfs() {
return
}
func (s *IntegrationSuite) TearDownTest() {
func (s *IntegrationSuite) AfterTest(suiteName, testName string) {
s.gardenBackend.Stop()
os.RemoveAll(s.rootfs)
s.cleanupIptables()
@ -233,6 +236,10 @@ func (s *IntegrationSuite) TestContainerNetworkEgress() {
// we have blocked access to.
//
func (s *IntegrationSuite) TestContainerNetworkEgressWithRestrictedNetworks() {
// Using custom backend, clean up BeforeTest() stuff
s.gardenBackend.Stop()
s.cleanupIptables()
namespace := "test-restricted-networks"
requestTimeout := 3 * time.Second
@ -291,6 +298,142 @@ func (s *IntegrationSuite) TestContainerNetworkEgressWithRestrictedNetworks() {
s.Contains(buf.String(), "connect: connection refused")
}
// TestContainerBlocksHostAccess verifies that a process that we run in a
// container is not able to reach the host but is able to reach the internet.
//
func (s *IntegrationSuite) TestContainerBlocksHostAccess() {
handle := uuid()
container, err := s.gardenBackend.Create(garden.ContainerSpec{
Handle: handle,
RootFSPath: "raw://" + s.rootfs,
Privileged: true,
})
s.NoError(err)
defer func() {
s.NoError(s.gardenBackend.Destroy(handle))
s.gardenBackend.Stop()
}()
hostIp, err := getHostIp()
s.NoError(err)
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, client")
}))
l, err := net.Listen("tcp", hostIp+":0")
ts.Listener = l
ts.Start()
defer ts.Close()
buf := new(buffer)
proc, err := container.Run(
garden.ProcessSpec{
Path: "/executable",
Args: []string{
"-http-get=" + ts.URL,
},
},
garden.ProcessIO{
Stdout: buf,
Stderr: buf,
},
)
s.NoError(err)
exitCode, err := proc.Wait()
s.NoError(err)
s.Equal(exitCode, 1, "Process in container should not be able to connect to host network")
proc, err = container.Run(
garden.ProcessSpec{
Path: "/executable",
Args: []string{
"-http-get=http://1.1.1.1",
},
},
garden.ProcessIO{
Stdout: buf,
Stderr: buf,
},
)
s.NoError(err)
exitCode, err = proc.Wait()
s.NoError(err)
s.Equal(exitCode, 0, "Process in container should also be able to reach the internet")
}
func (s *IntegrationSuite) TestContainerAllowsHostAccess() {
// Using custom backend, clean up BeforeTest() stuff
s.gardenBackend.Stop()
s.cleanupIptables()
namespace := "test-block-host-access"
requestTimeout := 3 * time.Second
network, err := runtime.NewCNINetwork(runtime.WithAllowHostAccess())
s.NoError(err)
networkOpt := runtime.WithNetwork(network)
customBackend, err := runtime.NewGardenBackend(
libcontainerd.New(
s.containerdSocket(),
namespace,
requestTimeout,
),
networkOpt,
)
s.NoError(err)
s.NoError(customBackend.Start())
handle := uuid()
container, err := customBackend.Create(garden.ContainerSpec{
Handle: handle,
RootFSPath: "raw://" + s.rootfs,
Privileged: true,
})
s.NoError(err)
defer func() {
s.NoError(customBackend.Destroy(handle))
customBackend.Stop()
}()
hostIp, err := getHostIp()
s.NoError(err)
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, client")
}))
l, err := net.Listen("tcp", hostIp+":0")
ts.Listener = l
ts.Start()
defer ts.Close()
buf := new(buffer)
proc, err := container.Run(
garden.ProcessSpec{
Path: "/executable",
Args: []string{
"-http-get=" + ts.URL,
},
},
garden.ProcessIO{
Stdout: buf,
Stderr: buf,
},
)
s.NoError(err)
exitCode, err := proc.Wait()
s.NoError(err)
s.Equal(exitCode, 0, "Process in container should be able to reach the host network")
}
// TestRunPrivileged tests whether we're able to run a process in a privileged
// container.
//

View File

@ -53,6 +53,7 @@ func httpGet(url string) {
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Timeout: 5 * time.Second,
}
resp, err := client.Get(url)

View File

@ -1,8 +1,11 @@
package integration_test
import (
"errors"
"io/ioutil"
"net"
"os/user"
"regexp"
"sync"
"testing"
@ -56,3 +59,28 @@ func TestSuite(t *testing.T) {
tmpDir: tmpDir,
})
}
func getHostIp() (string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return "", err
}
ethInterface := regexp.MustCompile("eth0")
for _, i := range ifaces {
if ethInterface.MatchString(i.Name) {
addrs, err := i.Addrs()
if err != nil {
return "", err
}
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String(), nil
}
}
}
}
}
return "", errors.New("unable to find host's IP")
}

View File

@ -9,16 +9,16 @@ import (
//counterfeiter:generate . Network
type Network interface {
// SetupHostNetwork sets up networking rules that
// affect all containers
//
SetupHostNetwork() (err error)
// SetupMounts prepares mounts that might be necessary for proper
// networking functionality.
//
SetupMounts(handle string) (mounts []specs.Mount, err error)
// SetupRestrictedNetworks sets up networking rules to prevent
// container access to specified network ranges
//
SetupRestrictedNetworks() (err error)
// Add adds a task to the network.
//
Add(ctx context.Context, task containerd.Task) (err error)

View File

@ -35,6 +35,16 @@ type FakeNetwork struct {
removeReturnsOnCall map[int]struct {
result1 error
}
SetupHostNetworkStub func() error
setupHostNetworkMutex sync.RWMutex
setupHostNetworkArgsForCall []struct {
}
setupHostNetworkReturns struct {
result1 error
}
setupHostNetworkReturnsOnCall map[int]struct {
result1 error
}
SetupMountsStub func(string) ([]specs.Mount, error)
setupMountsMutex sync.RWMutex
setupMountsArgsForCall []struct {
@ -48,16 +58,6 @@ type FakeNetwork struct {
result1 []specs.Mount
result2 error
}
SetupRestrictedNetworksStub func() error
setupRestrictedNetworksMutex sync.RWMutex
setupRestrictedNetworksArgsForCall []struct {
}
setupRestrictedNetworksReturns struct {
result1 error
}
setupRestrictedNetworksReturnsOnCall map[int]struct {
result1 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
@ -186,6 +186,59 @@ func (fake *FakeNetwork) RemoveReturnsOnCall(i int, result1 error) {
}{result1}
}
func (fake *FakeNetwork) SetupHostNetwork() error {
fake.setupHostNetworkMutex.Lock()
ret, specificReturn := fake.setupHostNetworkReturnsOnCall[len(fake.setupHostNetworkArgsForCall)]
fake.setupHostNetworkArgsForCall = append(fake.setupHostNetworkArgsForCall, struct {
}{})
stub := fake.SetupHostNetworkStub
fakeReturns := fake.setupHostNetworkReturns
fake.recordInvocation("SetupHostNetwork", []interface{}{})
fake.setupHostNetworkMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeNetwork) SetupHostNetworkCallCount() int {
fake.setupHostNetworkMutex.RLock()
defer fake.setupHostNetworkMutex.RUnlock()
return len(fake.setupHostNetworkArgsForCall)
}
func (fake *FakeNetwork) SetupHostNetworkCalls(stub func() error) {
fake.setupHostNetworkMutex.Lock()
defer fake.setupHostNetworkMutex.Unlock()
fake.SetupHostNetworkStub = stub
}
func (fake *FakeNetwork) SetupHostNetworkReturns(result1 error) {
fake.setupHostNetworkMutex.Lock()
defer fake.setupHostNetworkMutex.Unlock()
fake.SetupHostNetworkStub = nil
fake.setupHostNetworkReturns = struct {
result1 error
}{result1}
}
func (fake *FakeNetwork) SetupHostNetworkReturnsOnCall(i int, result1 error) {
fake.setupHostNetworkMutex.Lock()
defer fake.setupHostNetworkMutex.Unlock()
fake.SetupHostNetworkStub = nil
if fake.setupHostNetworkReturnsOnCall == nil {
fake.setupHostNetworkReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.setupHostNetworkReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeNetwork) SetupMounts(arg1 string) ([]specs.Mount, error) {
fake.setupMountsMutex.Lock()
ret, specificReturn := fake.setupMountsReturnsOnCall[len(fake.setupMountsArgsForCall)]
@ -250,59 +303,6 @@ func (fake *FakeNetwork) SetupMountsReturnsOnCall(i int, result1 []specs.Mount,
}{result1, result2}
}
func (fake *FakeNetwork) SetupRestrictedNetworks() error {
fake.setupRestrictedNetworksMutex.Lock()
ret, specificReturn := fake.setupRestrictedNetworksReturnsOnCall[len(fake.setupRestrictedNetworksArgsForCall)]
fake.setupRestrictedNetworksArgsForCall = append(fake.setupRestrictedNetworksArgsForCall, struct {
}{})
stub := fake.SetupRestrictedNetworksStub
fakeReturns := fake.setupRestrictedNetworksReturns
fake.recordInvocation("SetupRestrictedNetworks", []interface{}{})
fake.setupRestrictedNetworksMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeNetwork) SetupRestrictedNetworksCallCount() int {
fake.setupRestrictedNetworksMutex.RLock()
defer fake.setupRestrictedNetworksMutex.RUnlock()
return len(fake.setupRestrictedNetworksArgsForCall)
}
func (fake *FakeNetwork) SetupRestrictedNetworksCalls(stub func() error) {
fake.setupRestrictedNetworksMutex.Lock()
defer fake.setupRestrictedNetworksMutex.Unlock()
fake.SetupRestrictedNetworksStub = stub
}
func (fake *FakeNetwork) SetupRestrictedNetworksReturns(result1 error) {
fake.setupRestrictedNetworksMutex.Lock()
defer fake.setupRestrictedNetworksMutex.Unlock()
fake.SetupRestrictedNetworksStub = nil
fake.setupRestrictedNetworksReturns = struct {
result1 error
}{result1}
}
func (fake *FakeNetwork) SetupRestrictedNetworksReturnsOnCall(i int, result1 error) {
fake.setupRestrictedNetworksMutex.Lock()
defer fake.setupRestrictedNetworksMutex.Unlock()
fake.SetupRestrictedNetworksStub = nil
if fake.setupRestrictedNetworksReturnsOnCall == nil {
fake.setupRestrictedNetworksReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.setupRestrictedNetworksReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeNetwork) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
@ -310,10 +310,10 @@ func (fake *FakeNetwork) Invocations() map[string][][]interface{} {
defer fake.addMutex.RUnlock()
fake.removeMutex.RLock()
defer fake.removeMutex.RUnlock()
fake.setupHostNetworkMutex.RLock()
defer fake.setupHostNetworkMutex.RUnlock()
fake.setupMountsMutex.RLock()
defer fake.setupMountsMutex.RUnlock()
fake.setupRestrictedNetworksMutex.RLock()
defer fake.setupRestrictedNetworksMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value

View File

@ -70,6 +70,11 @@ func (cmd *WorkerCommand) containerdGardenServerRunner(
networkOpts = append(networkOpts, runtime.WithRestrictedNetworks(cmd.Containerd.Network.RestrictedNetworks))
}
// DNS proxy won't work without allowing access to host network
if cmd.Containerd.Network.AllowHostAccess || cmd.Containerd.Network.DNS.Enable {
networkOpts = append(networkOpts, runtime.WithAllowHostAccess())
}
networkConfig := runtime.DefaultCNINetworkConfig
if cmd.Containerd.Network.Pool != "" {
networkConfig.Subnet = cmd.Containerd.Network.Pool

View File

@ -52,13 +52,14 @@ type ContainerdRuntime struct {
RestrictedNetworks []string `long:"restricted-network" description:"Network ranges to which traffic from containers will be restricted. Can be specified multiple times."`
Pool string `long:"network-pool" default:"10.80.0.0/16" description:"Network range to use for dynamically allocated container subnets."`
MTU int `long:"mtu" description:"MTU size for container network interfaces. Defaults to the MTU of the interface used for outbound access by the host."`
AllowHostAccess bool `long:"allow-host-access" description:"Allow containers to reach the host's network. This is turned off by default."`
} `group:"Container Networking"`
MaxContainers int `long:"max-containers" default:"250" description:"Max container capacity. 0 means no limit."`
}
type DNSConfig struct {
Enable bool `long:"enable" description:"Enable proxy DNS server."`
Enable bool `long:"enable" description:"Enable proxy DNS server. Note: this will enable containers to access the host network."`
}
const containerdRuntime = "containerd"