github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/worker/runtime/cni_network.go (about)

     1  package runtime
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/pf-qiu/concourse/v6/worker/runtime/iptables"
    10  	"github.com/containerd/containerd"
    11  	"github.com/containerd/go-cni"
    12  	"github.com/opencontainers/runtime-spec/specs-go"
    13  )
    14  
    15  //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 github.com/containerd/go-cni.CNI
    16  
    17  // CNINetworkConfig provides configuration for CNINetwork to override the
    18  // defaults.
    19  //
    20  type CNINetworkConfig struct {
    21  	// BridgeName is the name that the bridge set up in the current network
    22  	// namespace to connect the veth's to.
    23  	//
    24  	BridgeName string
    25  
    26  	// NetworkName is the virtual name used to identify the managed network.
    27  	//
    28  	NetworkName string
    29  
    30  	// Subnet is the subnet (in CIDR notation) which the veths should be
    31  	// added to.
    32  	//
    33  	Subnet string
    34  }
    35  
    36  const (
    37  	// fileStoreWorkDir is a default directory used for storing
    38  	// container-related files
    39  	//
    40  	fileStoreWorkDir = "/tmp"
    41  
    42  	// binariesDir corresponds to the directory where CNI plugins have their
    43  	// binaries in.
    44  	//
    45  	binariesDir = "/usr/local/concourse/bin"
    46  
    47  	ipTablesAdminChainName = "CONCOURSE-OPERATOR"
    48  )
    49  
    50  var (
    51  	// defaultCNINetworkConfig is the default configuration for the CNI network
    52  	// created to put concourse containers into.
    53  	//
    54  	defaultCNINetworkConfig = CNINetworkConfig{
    55  		BridgeName:  "concourse0",
    56  		NetworkName: "concourse",
    57  		Subnet:      "10.80.0.0/16",
    58  	}
    59  )
    60  
    61  func (c CNINetworkConfig) ToJSON() string {
    62  	const networksConfListFormat = `{
    63    "cniVersion": "0.4.0",
    64    "name": "%s",
    65    "plugins": [
    66      {
    67        "type": "bridge",
    68        "bridge": "%s",
    69        "isGateway": true,
    70        "ipMasq": true,
    71        "ipam": {
    72          "type": "host-local",
    73          "subnet": "%s",
    74          "routes": [
    75            {
    76              "dst": "0.0.0.0/0"
    77            }
    78          ]
    79        }
    80      },
    81      {
    82        "type": "firewall",
    83        "iptablesAdminChainName": "%s"
    84      }
    85    ]
    86  }`
    87  
    88  	return fmt.Sprintf(networksConfListFormat,
    89  		c.NetworkName, c.BridgeName, c.Subnet, ipTablesAdminChainName,
    90  	)
    91  }
    92  
    93  // CNINetworkOpt defines a functional option that when applied, modifies the
    94  // configuration of a CNINetwork.
    95  //
    96  type CNINetworkOpt func(n *cniNetwork)
    97  
    98  // WithCNIBinariesDir is the directory where the binaries necessary for setting
    99  // up the network live.
   100  //
   101  func WithCNIBinariesDir(dir string) CNINetworkOpt {
   102  	return func(n *cniNetwork) {
   103  		n.binariesDir = dir
   104  	}
   105  }
   106  
   107  // WithNameServers sets the set of nameservers to be configured for the
   108  // /etc/resolv.conf inside the containers.
   109  //
   110  func WithNameServers(nameservers []string) CNINetworkOpt {
   111  	return func(n *cniNetwork) {
   112  		for _, ns := range nameservers {
   113  			n.nameServers = append(n.nameServers, "nameserver "+ns)
   114  		}
   115  	}
   116  }
   117  
   118  // WithCNIClient is an implementor of the CNI interface for reaching out to CNI
   119  // plugins.
   120  //
   121  func WithCNIClient(c cni.CNI) CNINetworkOpt {
   122  	return func(n *cniNetwork) {
   123  		n.client = c
   124  	}
   125  }
   126  
   127  // WithCNINetworkConfig provides a custom CNINetworkConfig to be used by the CNI
   128  // client at startup time.
   129  //
   130  func WithCNINetworkConfig(c CNINetworkConfig) CNINetworkOpt {
   131  	return func(n *cniNetwork) {
   132  		n.config = c
   133  	}
   134  }
   135  
   136  // WithCNIFileStore changes the default FileStore used to store files that
   137  // belong to network configurations for containers.
   138  //
   139  func WithCNIFileStore(f FileStore) CNINetworkOpt {
   140  	return func(n *cniNetwork) {
   141  		n.store = f
   142  	}
   143  }
   144  
   145  // WithRestrictedNetworks defines the network ranges that containers will be restricted
   146  // from accessing.
   147  func WithRestrictedNetworks(restrictedNetworks []string) CNINetworkOpt {
   148  	return func(n *cniNetwork) {
   149  		n.restrictedNetworks = restrictedNetworks
   150  	}
   151  }
   152  
   153  // WithIptables allows for a custom implementation of the iptables.Iptables interface
   154  // to be provided.
   155  func WithIptables(ipt iptables.Iptables) CNINetworkOpt {
   156  	return func(n *cniNetwork) {
   157  		n.ipt = ipt
   158  	}
   159  }
   160  
   161  type cniNetwork struct {
   162  	client             cni.CNI
   163  	store              FileStore
   164  	config             CNINetworkConfig
   165  	nameServers        []string
   166  	binariesDir        string
   167  	restrictedNetworks []string
   168  	ipt                iptables.Iptables
   169  }
   170  
   171  var _ Network = (*cniNetwork)(nil)
   172  
   173  func NewCNINetwork(opts ...CNINetworkOpt) (*cniNetwork, error) {
   174  	var err error
   175  
   176  	n := &cniNetwork{
   177  		config:      defaultCNINetworkConfig,
   178  	}
   179  
   180  	for _, opt := range opts {
   181  		opt(n)
   182  	}
   183  
   184  	if n.binariesDir == "" {
   185  		n.binariesDir = binariesDir
   186  	}
   187  
   188  	if n.store == nil {
   189  		n.store = NewFileStore(fileStoreWorkDir)
   190  	}
   191  
   192  	if n.client == nil {
   193  		n.client, err = cni.New(cni.WithPluginDir([]string{n.binariesDir}))
   194  		if err != nil {
   195  			return nil, fmt.Errorf("cni init: %w", err)
   196  		}
   197  
   198  		err = n.client.Load(
   199  			cni.WithConfListBytes([]byte(n.config.ToJSON())),
   200  			cni.WithLoNetwork,
   201  		)
   202  		if err != nil {
   203  			return nil, fmt.Errorf("cni configuration loading: %w", err)
   204  		}
   205  	}
   206  
   207  	if n.ipt == nil {
   208  		n.ipt, err = iptables.New()
   209  
   210  		if err != nil {
   211  			return nil, fmt.Errorf("failed to initialize iptables")
   212  		}
   213  	}
   214  
   215  	return n, nil
   216  }
   217  
   218  func (n cniNetwork) SetupMounts(handle string) ([]specs.Mount, error) {
   219  	if handle == "" {
   220  		return nil, ErrInvalidInput("empty handle")
   221  	}
   222  
   223  	etcHosts, err := n.store.Create(
   224  		filepath.Join(handle, "/hosts"),
   225  		[]byte("127.0.0.1 localhost"),
   226  	)
   227  	if err != nil {
   228  		return nil, fmt.Errorf("creating /etc/hosts: %w", err)
   229  	}
   230  
   231  	resolvContents, err := n.generateResolvConfContents()
   232  	if err != nil {
   233  		return nil, fmt.Errorf("generating resolv.conf: %w", err)
   234  	}
   235  
   236  	resolvConf, err := n.store.Create(
   237  		filepath.Join(handle, "/resolv.conf"),
   238  		resolvContents,
   239  	)
   240  	if err != nil {
   241  		return nil, fmt.Errorf("creating /etc/resolv.conf: %w", err)
   242  	}
   243  
   244  	return []specs.Mount{
   245  		{
   246  			Destination: "/etc/hosts",
   247  			Type:        "bind",
   248  			Source:      etcHosts,
   249  			Options:     []string{"bind", "rw"},
   250  		}, {
   251  			Destination: "/etc/resolv.conf",
   252  			Type:        "bind",
   253  			Source:      resolvConf,
   254  			Options:     []string{"bind", "rw"},
   255  		},
   256  	}, nil
   257  }
   258  
   259  func (n cniNetwork) SetupRestrictedNetworks() error {
   260  	const tableName = "filter"
   261  	err := n.ipt.CreateChainOrFlushIfExists(tableName, ipTablesAdminChainName)
   262  	if err != nil {
   263  		return fmt.Errorf("create chain or flush if exists failed: %w", err)
   264  	}
   265  
   266  	// Optimization that allows packets of ESTABLISHED and RELATED connections to go through without further rule matching
   267  	err = n.ipt.AppendRule(tableName, ipTablesAdminChainName, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT")
   268  	if err != nil {
   269  		return fmt.Errorf("appending accept rule for RELATED & ESTABLISHED connections failed: %w", err)
   270  	}
   271  
   272  	for _, restrictedNetwork := range n.restrictedNetworks {
   273  		// Create REJECT rule in admin chain
   274  		err = n.ipt.AppendRule(tableName, ipTablesAdminChainName, "-d", restrictedNetwork, "-j", "REJECT")
   275  		if err != nil {
   276  			return fmt.Errorf("appending reject rule for restricted network %s failed: %w", restrictedNetwork, err)
   277  		}
   278  	}
   279  	return nil
   280  }
   281  
   282  func (n cniNetwork) generateResolvConfContents() ([]byte, error) {
   283  	contents := ""
   284  	resolvConfEntries := n.nameServers
   285  	var err error
   286  
   287  	if len(n.nameServers) == 0 {
   288  		resolvConfEntries, err = ParseHostResolveConf("/etc/resolv.conf")
   289  	}
   290  
   291  	contents = strings.Join(resolvConfEntries, "\n")
   292  
   293  	return []byte(contents), err
   294  }
   295  
   296  func (n cniNetwork) Add(ctx context.Context, task containerd.Task) error {
   297  	if task == nil {
   298  		return ErrInvalidInput("nil task")
   299  	}
   300  
   301  	id, netns := netId(task), netNsPath(task)
   302  
   303  	_, err := n.client.Setup(ctx, id, netns)
   304  	if err != nil {
   305  		return fmt.Errorf("cni net setup: %w", err)
   306  	}
   307  
   308  	return nil
   309  }
   310  
   311  func (n cniNetwork) Remove(ctx context.Context, task containerd.Task) error {
   312  	if task == nil {
   313  		return ErrInvalidInput("nil task")
   314  	}
   315  
   316  	id, netns := netId(task), netNsPath(task)
   317  
   318  	err := n.client.Remove(ctx, id, netns)
   319  	if err != nil {
   320  		return fmt.Errorf("cni net teardown: %w", err)
   321  	}
   322  
   323  	return nil
   324  }
   325  
   326  func netId(task containerd.Task) string {
   327  	return task.ID()
   328  }
   329  
   330  func netNsPath(task containerd.Task) string {
   331  	return fmt.Sprintf("/proc/%d/ns/net", task.Pid())
   332  }