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 }