diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 90fb3ea7..dfe930db 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -6,6 +6,11 @@ "./..." ], "Deps": [ + { + "ImportPath": "github.com/containernetworking/cni/libcni", + "Comment": "v0.5.2", + "Rev": "137b4975ecab6e1f0c24c1e3c228a50a3cfba75e" + }, { "ImportPath": "github.com/containernetworking/cni/pkg/invoke", "Comment": "v0.5.2", @@ -38,8 +43,8 @@ }, { "ImportPath": "github.com/coreos/go-iptables/iptables", - "Comment": "v0.1.0", - "Rev": "fbb73372b87f6e89951c2b6b31470c2c9d5cfae3" + "Comment": "v0.1.0-9-g197187d", + "Rev": "197187d414d7704f99ea52a692b9672e76f063bf" }, { "ImportPath": "github.com/coreos/go-systemd/activation", @@ -54,6 +59,11 @@ "ImportPath": "github.com/d2g/dhcp4client", "Rev": "bed07e1bc5b85f69c6f0fd73393aa35ec68ed892" }, + { + "ImportPath": "github.com/mattn/go-shellwords", + "Comment": "v1.0.3", + "Rev": "02e3cf038dcea8290e44424da473dd12be796a8a" + }, { "ImportPath": "github.com/onsi/ginkgo", "Comment": "v1.2.0-29-g7f8ab55", diff --git a/README.md b/README.md index 5aa1dd55..09e25603 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Some CNI network plugins, maintained by the containernetworking team. For more i ### Meta: other plugins * `flannel`: generates an interface corresponding to a flannel config file * `tuning`: Tweaks sysctl parameters of an existing interface - +* `portmap`: An iptables-based portmapping plugin. Maps ports from the host's address space to the container. ### Sample The sample plugin provides an example for building your own plugin. diff --git a/plugins/meta/portmap/README.md b/plugins/meta/portmap/README.md new file mode 100644 index 00000000..fc6b86c9 --- /dev/null +++ b/plugins/meta/portmap/README.md @@ -0,0 +1,117 @@ +## Port-mapping plugin + +This plugin will forward traffic from one or more ports on the host to the +container. It expects to be run as a chained plugin. + +## Usage +You should use this plugin as part of a network configuration list. It accepts +the following configuration options: + +* `snat` - boolean, default true. If true or omitted, set up the SNAT chains +* `conditionsV4`, `conditionsV6` - array of strings. A list of arbitrary `iptables` +matches to add to the per-container rule. This may be useful if you wish to +exclude specific IPs from port-mapping + +The plugin expects to receive the actual list of port mappings via the +`portMappings` [capability argument](https://github.com/containernetworking/cni/blob/master/CONVENTIONS.md) + +So a sample standalone config list (with the file extension .conflist) might +look like: + +```json +{ + "cniVersion": "0.3.1", + "name": "mynet", + "plugins": [ + { + "type": "ptp", + "ipMasq": true, + "ipam": { + "type": "host-local", + "subnet": "172.16.30.0/24", + "routes": [ + { + "dst": "0.0.0.0/0" + } + ] + } + }, + { + "type": "portmap", + "capabilities": {"portMappings": true}, + "snat": false, + "conditionsV4": ["!", "-d", "192.0.2.0/24"], + "conditionsV6": ["!", "-d", "fc00::/7"] + } + ] +} +``` + + + +## Rule structure +The plugin sets up two sequences of chains and rules - one "primary" DNAT +sequence to rewrite the destination, and one additional SNAT sequence that +rewrites the source address for packets from localhost. The sequence is somewhat +complex to minimize the number of rules non-forwarded packets must traverse. + + +### DNAT +The DNAT rule rewrites the destination port and address of new connections. +There is a top-level chain, `CNI-HOSTPORT-DNAT` which is always created and +never deleted. Each plugin execution creates an additional chain for ease +of cleanup. So, if a single container exists on IP 172.16.30.2 with ports +8080 and 8043 on the host forwarded to ports 80 and 443 in the container, the +rules look like this: + +`PREROUTING`, `OUTPUT` chains: +- `--dst-type LOCAL -j CNI-HOSTPORT-DNAT` + +`CNI-HOSTPORT-DNAT` chain: +- `${ConditionsV4/6} -j CNI-DN-xxxxxx` (where xxxxxx is a function of the ContainerID and network name) + +`CNI-DN-xxxxxx` chain: +- `-p tcp --dport 8080 -j DNAT --to-destination 172.16.30.2:80` +- `-p tcp --dport 8043 -j DNAT --to-destination 172.16.30.2:443` + +New connections to the host will have to traverse every rule, so large numbers +of port forwards may have a performance impact. This won't affect established +connections, just the first packet. + +### SNAT +The SNAT rule enables port-forwarding from the localhost IP on the host. +This rule rewrites (masquerades) the source address for connections from +localhost. If this rule did not exist, a connection to `localhost:80` would +still have a source IP of 127.0.0.1 when received by the container, so no +packets would respond. Again, it is a sequence of 3 chains. Because SNAT has to +occur in the `POSTROUTING` chain, the packet has already been through the DNAT +chain. + +`POSTROUTING`: +- `-s 127.0.0.1 ! -d 127.0.0.1 -j CNI-HOSTPORT-SNAT` + +`CNI-HOSTPORT-SNAT`: +- `-j CNI-SN-xxxxx` + +`CNI-SN-xxxxx`: +- `-p tcp -s 127.0.0.1 -d 172.16.30.2 --dport 80 -j MASQUERADE` +- `-p tcp -s 127.0.0.1 -d 172.16.30.2 --dport 443 -j MASQUERADE` + +Only new connections from the host, where the source address is 127.0.0.1 but +not the destination will traverse this chain. It is unlikely that any packets +will reach these rules without being SNATted, so the cost should be minimal. + +Because MASQUERADE happens in POSTROUTING, it means that packets with source ip +127.0.0.1 need to pass a routing boundary. By default, that is not allowed +in Linux. So, need to enable the sysctl `net.ipv4.conf.IFNAME.route_localnet`, +where IFNAME is the name of the host-side interface that routes traffic to the +container. + +There is no equivalent to `route_localnet` for ipv6, so SNAT does not work +for ipv6. If you need port forwarding from localhost, your container must have +an ipv4 address. + + +## Known issues +- ipsets could improve efficiency +- SNAT does not work with ipv6. diff --git a/plugins/meta/portmap/chain.go b/plugins/meta/portmap/chain.go new file mode 100644 index 00000000..f8a53a45 --- /dev/null +++ b/plugins/meta/portmap/chain.go @@ -0,0 +1,127 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "strings" + + "github.com/coreos/go-iptables/iptables" + shellwords "github.com/mattn/go-shellwords" +) + +type chain struct { + table string + name string + entryRule []string // the rule that enters this chain + entryChains []string // the chains to add the entry rule +} + +// setup idempotently creates the chain. It will not error if the chain exists. +func (c *chain) setup(ipt *iptables.IPTables, rules [][]string) error { + // create the chain + exists, err := chainExists(ipt, c.table, c.name) + if err != nil { + return err + } + if !exists { + if err := ipt.NewChain(c.table, c.name); err != nil { + return err + } + } + + // Add the rules to the chain + for i := len(rules) - 1; i >= 0; i-- { + if err := prependUnique(ipt, c.table, c.name, rules[i]); err != nil { + return err + } + } + + // Add the entry rules + entryRule := append(c.entryRule, "-j", c.name) + for _, entryChain := range c.entryChains { + if err := prependUnique(ipt, c.table, entryChain, entryRule); err != nil { + return err + } + } + + return nil +} + +// teardown idempotently deletes a chain. It will not error if the chain doesn't exist. +// It will first delete all references to this chain in the entryChains. +func (c *chain) teardown(ipt *iptables.IPTables) error { + // flush the chain + // This will succeed *and create the chain* if it does not exist. + // If the chain doesn't exist, the next checks will fail. + if err := ipt.ClearChain(c.table, c.name); err != nil { + return err + } + + for _, entryChain := range c.entryChains { + entryChainRules, err := ipt.List(c.table, entryChain) + if err != nil { + // Swallow error here - probably the chain doesn't exist. + // If we miss something the deletion will fail + continue + } + + for _, entryChainRule := range entryChainRules[1:] { + if strings.HasSuffix(entryChainRule, "-j "+c.name) { + chainParts, err := shellwords.Parse(entryChainRule) + if err != nil { + return fmt.Errorf("error parsing iptables rule: %s: %v", entryChainRule, err) + } + chainParts = chainParts[2:] // List results always include an -A CHAINNAME + + if err := ipt.Delete(c.table, entryChain, chainParts...); err != nil { + return fmt.Errorf("Failed to delete referring rule %s %s: %v", c.table, entryChainRule, err) + } + } + } + } + + if err := ipt.DeleteChain(c.table, c.name); err != nil { + return err + } + return nil +} + +// prependUnique will prepend a rule to a chain, if it does not already exist +func prependUnique(ipt *iptables.IPTables, table, chain string, rule []string) error { + exists, err := ipt.Exists(table, chain, rule...) + if err != nil { + return err + } + if exists { + return nil + } + + return ipt.Insert(table, chain, 1, rule...) +} + +func chainExists(ipt *iptables.IPTables, tableName, chainName string) (bool, error) { + chains, err := ipt.ListChains(tableName) + if err != nil { + return false, err + } + + for _, ch := range chains { + if ch == chainName { + return true, nil + } + } + return false, nil +} diff --git a/plugins/meta/portmap/chain_test.go b/plugins/meta/portmap/chain_test.go new file mode 100644 index 00000000..5cc4cf64 --- /dev/null +++ b/plugins/meta/portmap/chain_test.go @@ -0,0 +1,203 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "math/rand" + "runtime" + + "github.com/containernetworking/plugins/pkg/ns" + "github.com/coreos/go-iptables/iptables" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +const TABLE = "filter" // We'll monkey around here + +// TODO: run these tests in a new namespace +var _ = Describe("chain tests", func() { + var testChain chain + var ipt *iptables.IPTables + var cleanup func() + + BeforeEach(func() { + + // Save a reference to the original namespace, + // Add a new NS + currNs, err := ns.GetCurrentNS() + Expect(err).NotTo(HaveOccurred()) + + testNs, err := ns.NewNS() + Expect(err).NotTo(HaveOccurred()) + + tlChainName := fmt.Sprintf("cni-test-%d", rand.Intn(10000000)) + chainName := fmt.Sprintf("cni-test-%d", rand.Intn(10000000)) + + testChain = chain{ + table: TABLE, + name: chainName, + entryRule: []string{"-d", "203.0.113.1"}, + entryChains: []string{tlChainName}, + } + + ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv4) + Expect(err).NotTo(HaveOccurred()) + + runtime.LockOSThread() + err = testNs.Set() + Expect(err).NotTo(HaveOccurred()) + + err = ipt.ClearChain(TABLE, tlChainName) // This will create the chain + if err != nil { + currNs.Set() + Expect(err).NotTo(HaveOccurred()) + } + + cleanup = func() { + if ipt == nil { + return + } + ipt.ClearChain(TABLE, testChain.name) + ipt.ClearChain(TABLE, tlChainName) + ipt.DeleteChain(TABLE, testChain.name) + ipt.DeleteChain(TABLE, tlChainName) + currNs.Set() + } + + }) + + It("creates and destroys a chain", func() { + defer cleanup() + + tlChainName := testChain.entryChains[0] + + // add an extra rule to the test chain to make sure it's not touched + err := ipt.Append(TABLE, tlChainName, "-m", "comment", "--comment", + "canary value", "-j", "ACCEPT") + Expect(err).NotTo(HaveOccurred()) + + // Create the chain + chainRules := [][]string{ + {"-m", "comment", "--comment", "test 1", "-j", "RETURN"}, + {"-m", "comment", "--comment", "test 2", "-j", "RETURN"}, + } + err = testChain.setup(ipt, chainRules) + Expect(err).NotTo(HaveOccurred()) + + // Verify the chain exists + ok := false + chains, err := ipt.ListChains(TABLE) + Expect(err).NotTo(HaveOccurred()) + for _, chain := range chains { + if chain == testChain.name { + ok = true + break + } + } + if !ok { + Fail("Could not find created chain") + } + + // Check that the entry rule was created + haveRules, err := ipt.List(TABLE, tlChainName) + Expect(err).NotTo(HaveOccurred()) + Expect(haveRules).To(Equal([]string{ + "-N " + tlChainName, + "-A " + tlChainName + " -d 203.0.113.1/32 -j " + testChain.name, + "-A " + tlChainName + ` -m comment --comment "canary value" -j ACCEPT`, + })) + + // Check that the chain and rule was created + haveRules, err = ipt.List(TABLE, testChain.name) + Expect(err).NotTo(HaveOccurred()) + Expect(haveRules).To(Equal([]string{ + "-N " + testChain.name, + "-A " + testChain.name + ` -m comment --comment "test 1" -j RETURN`, + "-A " + testChain.name + ` -m comment --comment "test 2" -j RETURN`, + })) + + err = testChain.teardown(ipt) + Expect(err).NotTo(HaveOccurred()) + + tlRules, err := ipt.List(TABLE, tlChainName) + Expect(err).NotTo(HaveOccurred()) + Expect(tlRules).To(Equal([]string{ + "-N " + tlChainName, + "-A " + tlChainName + ` -m comment --comment "canary value" -j ACCEPT`, + })) + + chains, err = ipt.ListChains(TABLE) + Expect(err).NotTo(HaveOccurred()) + for _, chain := range chains { + if chain == testChain.name { + Fail("chain was not deleted") + } + } + }) + + It("creates chains idempotently", func() { + defer cleanup() + + // Create the chain + chainRules := [][]string{ + {"-m", "comment", "--comment", "test", "-j", "RETURN"}, + } + err := testChain.setup(ipt, chainRules) + Expect(err).NotTo(HaveOccurred()) + + // Create it again! + err = testChain.setup(ipt, chainRules) + Expect(err).NotTo(HaveOccurred()) + + // Make sure there are only two rules + // (the first rule is an -N because go-iptables + rules, err := ipt.List(TABLE, testChain.name) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(rules)).To(Equal(2)) + + }) + + It("deletes chains idempotently", func() { + defer cleanup() + + // Create the chain + chainRules := [][]string{ + {"-m", "comment", "--comment", "test", "-j", "RETURN"}, + } + err := testChain.setup(ipt, chainRules) + Expect(err).NotTo(HaveOccurred()) + + err = testChain.teardown(ipt) + Expect(err).NotTo(HaveOccurred()) + + chains, err := ipt.ListChains(TABLE) + for _, chain := range chains { + if chain == testChain.name { + Fail("Chain was not deleted") + } + } + + err = testChain.teardown(ipt) + Expect(err).NotTo(HaveOccurred()) + chains, err = ipt.ListChains(TABLE) + for _, chain := range chains { + if chain == testChain.name { + Fail("Chain was not deleted") + } + } + }) +}) diff --git a/plugins/meta/portmap/main.go b/plugins/meta/portmap/main.go new file mode 100644 index 00000000..c0c34ae5 --- /dev/null +++ b/plugins/meta/portmap/main.go @@ -0,0 +1,159 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This is a post-setup plugin that establishes port forwarding - using iptables, +// from the host's network interface(s) to a pod's network interface. +// +// It is intended to be used as a chained CNI plugin, and determines the container +// IP from the previous result. If the result includes an IPv6 address, it will +// also be configured. (IPTables will not forward cross-family). +// +// This has one notable limitation: it does not perform any kind of reservation +// of the actual host port. If there is a service on the host, it will have all +// its traffic captured by the container. If another container also claims a given +// port, it will caputure the traffic - it is last-write-wins. +package main + +import ( + "encoding/json" + "fmt" + + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/cni/pkg/version" +) + +// PortMapEntry corresponds to a single entry in the port_mappings argument, +// see CONVENTIONS.md +type PortMapEntry struct { + HostPort int `json:"hostPort"` + ContainerPort int `json:"containerPort"` + Protocol string `json:"protocol"` + HostIP string `json:"hostIP,omitempty"` +} + +type PortMapConf struct { + types.NetConf + SNAT *bool `json:"snat,omitempty"` + ConditionsV4 *[]string `json:"conditionsV4"` + ConditionsV6 *[]string `json:"conditionsV6"` + RuntimeConfig struct { + PortMaps []PortMapEntry `json:"portMappings,omitempty"` + } `json:"runtimeConfig,omitempty"` + RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` + PrevResult *current.Result `json:"-"` + ContainerID string +} + +func cmdAdd(args *skel.CmdArgs) error { + netConf, err := parseConfig(args.StdinData) + if err != nil { + return fmt.Errorf("failed to parse config: %v", err) + } + + if netConf.PrevResult == nil { + return fmt.Errorf("must be called as chained plugin") + } + + if len(netConf.RuntimeConfig.PortMaps) == 0 { + return types.PrintResult(netConf.PrevResult, netConf.CNIVersion) + } + + netConf.ContainerID = args.ContainerID + + // Loop through IPs, setting up forwarding to the first container IP + // per family + hasV4 := false + hasV6 := false + for _, ip := range netConf.PrevResult.IPs { + if ip.Version == "6" && hasV6 { + continue + } else if ip.Version == "4" && hasV4 { + continue + } + + // Skip known non-sandbox interfaces + intIdx := ip.Interface + if intIdx >= 0 && intIdx < len(netConf.PrevResult.Interfaces) && netConf.PrevResult.Interfaces[intIdx].Name != args.IfName { + continue + } + + if err := forwardPorts(netConf, ip.Address.IP); err != nil { + return err + } + + if ip.Version == "6" { + hasV6 = true + } else { + hasV4 = true + } + } + + // Pass through the previous result + return types.PrintResult(netConf.PrevResult, netConf.CNIVersion) +} + +func cmdDel(args *skel.CmdArgs) error { + netConf, err := parseConfig(args.StdinData) + if err != nil { + return fmt.Errorf("failed to parse config: %v", err) + } + + netConf.ContainerID = args.ContainerID + + // We don't need to parse out whether or not we're using v6 or snat, + // deletion is idempotent + if err := unforwardPorts(netConf); err != nil { + return err + } + return nil +} + +func main() { + skel.PluginMain(cmdAdd, cmdDel, version.PluginSupports("", "0.1.0", "0.2.0", "0.3.0", version.Current())) +} + +// parseConfig parses the supplied configuration (and prevResult) from stdin. +func parseConfig(stdin []byte) (*PortMapConf, error) { + conf := PortMapConf{} + + if err := json.Unmarshal(stdin, &conf); err != nil { + return nil, fmt.Errorf("failed to parse network configuration: %v", err) + } + + // Parse previous result. + if conf.RawPrevResult != nil { + resultBytes, err := json.Marshal(conf.RawPrevResult) + if err != nil { + return nil, fmt.Errorf("could not serialize prevResult: %v", err) + } + res, err := version.NewResult(conf.CNIVersion, resultBytes) + if err != nil { + return nil, fmt.Errorf("could not parse prevResult: %v", err) + } + conf.RawPrevResult = nil + conf.PrevResult, err = current.NewResultFromResult(res) + if err != nil { + return nil, fmt.Errorf("could not convert result to current version: %v", err) + } + } + + if conf.SNAT == nil { + tvar := true + conf.SNAT = &tvar + } + + return &conf, nil +} diff --git a/plugins/meta/portmap/portmap.go b/plugins/meta/portmap/portmap.go new file mode 100644 index 00000000..133dfef2 --- /dev/null +++ b/plugins/meta/portmap/portmap.go @@ -0,0 +1,294 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "net" + "strconv" + + "github.com/containernetworking/plugins/pkg/utils/sysctl" + "github.com/coreos/go-iptables/iptables" +) + +// This creates the chains to be added to iptables. The basic structure is +// a bit complex for efficiencies sake. We create 2 chains: a summary chain +// that is shared between invocations, and an invocation (container)-specific +// chain. This minimizes the number of operations on the top level, but allows +// for easy cleanup. +// +// We also create DNAT chains to rewrite destinations, and SNAT chains so that +// connections to localhost work. +// +// The basic setup (all operations are on the nat table) is: +// +// DNAT case (rewrite destination IP and port): +// PREROUTING, OUTPUT: --dst-type local -j CNI-HOSTPORT_DNAT +// CNI-HOSTPORT-DNAT: -j CNI-DN-abcd123 +// CNI-DN-abcd123: -p tcp --dport 8080 -j DNAT --to-destination 192.0.2.33:80 +// CNI-DN-abcd123: -p tcp --dport 8081 -j DNAT ... +// +// SNAT case (rewrite source IP from localhost after dnat): +// POSTROUTING: -s 127.0.0.1 ! -d 127.0.0.1 -j CNI-HOSTPORT-SNAT +// CNI-HOSTPORT-SNAT: -j CNI-SN-abcd123 +// CNI-SN-abcd123: -p tcp -s 127.0.0.1 -d 192.0.2.33 --dport 80 -j MASQUERADE +// CNI-SN-abcd123: -p tcp -s 127.0.0.1 -d 192.0.2.33 --dport 90 -j MASQUERADE + +// The names of the top-level summary chains. +// These should never be changed, or else upgrading will require manual +// intervention. +const TopLevelDNATChainName = "CNI-HOSTPORT-DNAT" +const TopLevelSNATChainName = "CNI-HOSTPORT-SNAT" + +// forwardPorts establishes port forwarding to a given container IP. +// containerIP can be either v4 or v6. +func forwardPorts(config *PortMapConf, containerIP net.IP) error { + isV6 := (containerIP.To4() == nil) + + var ipt *iptables.IPTables + var err error + var conditions *[]string + + if isV6 { + ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv6) + conditions = config.ConditionsV6 + } else { + ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv4) + conditions = config.ConditionsV4 + } + if err != nil { + return fmt.Errorf("failed to open iptables: %v", err) + } + + toplevelDnatChain := genToplevelDnatChain() + if err := toplevelDnatChain.setup(ipt, nil); err != nil { + return fmt.Errorf("failed to create top-level DNAT chain: %v", err) + } + + dnatChain := genDnatChain(config.Name, config.ContainerID, conditions) + _ = dnatChain.teardown(ipt) // If we somehow collide on this container ID + network, cleanup + + dnatRules := dnatRules(config.RuntimeConfig.PortMaps, containerIP) + if err := dnatChain.setup(ipt, dnatRules); err != nil { + return fmt.Errorf("unable to setup DNAT: %v", err) + } + + // Enable SNAT for connections to localhost. + // This won't work for ipv6, since the kernel doesn't have the equvalent + // route_localnet sysctl. + if *config.SNAT && !isV6 { + toplevelSnatChain := genToplevelSnatChain(isV6) + if err := toplevelSnatChain.setup(ipt, nil); err != nil { + return fmt.Errorf("failed to create top-level SNAT chain: %v", err) + } + + snatChain := genSnatChain(config.Name, config.ContainerID) + _ = snatChain.teardown(ipt) + + snatRules := snatRules(config.RuntimeConfig.PortMaps, containerIP) + if err := snatChain.setup(ipt, snatRules); err != nil { + return fmt.Errorf("unable to setup SNAT: %v", err) + } + if !isV6 { + // Set the route_localnet bit on the host interface, so that + // 127/8 can cross a routing boundary. + hostIfName := getRoutableHostIF(containerIP) + if hostIfName != "" { + if err := enableLocalnetRouting(hostIfName); err != nil { + return fmt.Errorf("unable to enable route_localnet: %v", err) + } + } + } + } + + return nil +} + +// genToplevelDnatChain creates the top-level summary chain that we'll +// add our chain to. This is easy, because creating chains is idempotent. +// IMPORTANT: do not change this, or else upgrading plugins will require +// manual intervention. +func genToplevelDnatChain() chain { + return chain{ + table: "nat", + name: TopLevelDNATChainName, + entryRule: []string{ + "-m", "addrtype", + "--dst-type", "LOCAL", + }, + entryChains: []string{"PREROUTING", "OUTPUT"}, + } +} + +// genDnatChain creates the per-container chain. +// Conditions are any static entry conditions for the chain. +func genDnatChain(netName, containerID string, conditions *[]string) chain { + name := formatChainName("DN-", netName, containerID) + comment := fmt.Sprintf(`dnat name: "%s" id: "%s"`, netName, containerID) + + ch := chain{ + table: "nat", + name: name, + entryRule: []string{ + "-m", "comment", + "--comment", comment, + }, + entryChains: []string{TopLevelDNATChainName}, + } + if conditions != nil && len(*conditions) != 0 { + ch.entryRule = append(ch.entryRule, *conditions...) + } + + return ch +} + +// dnatRules generates the destination NAT rules, one per port, to direct +// traffic from hostip:hostport to podip:podport +func dnatRules(entries []PortMapEntry, containerIP net.IP) [][]string { + out := make([][]string, 0, len(entries)) + for _, entry := range entries { + rule := []string{ + "-p", entry.Protocol, + "--dport", strconv.Itoa(entry.HostPort)} + + if entry.HostIP != "" { + rule = append(rule, + "-d", entry.HostIP) + } + + rule = append(rule, + "-j", "DNAT", + "--to-destination", fmtIpPort(containerIP, entry.ContainerPort)) + + out = append(out, rule) + } + return out +} + +// genToplevelSnatChain creates the top-level summary snat chain. +// IMPORTANT: do not change this, or else upgrading plugins will require +// manual intervention +func genToplevelSnatChain(isV6 bool) chain { + return chain{ + table: "nat", + name: TopLevelSNATChainName, + entryRule: []string{ + "-s", localhostIP(isV6), + "!", "-d", localhostIP(isV6), + }, + entryChains: []string{"POSTROUTING"}, + } +} + +// genSnatChain creates the snat (localhost) chain for this container. +func genSnatChain(netName, containerID string) chain { + name := formatChainName("SN-", netName, containerID) + comment := fmt.Sprintf(`snat name: "%s" id: "%s"`, netName, containerID) + + return chain{ + table: "nat", + name: name, + entryRule: []string{ + "-m", "comment", + "--comment", comment, + }, + entryChains: []string{TopLevelSNATChainName}, + } +} + +// snatRules sets up masquerading for connections to localhost:hostport, +// rewriting the source so that returning packets are correct. +func snatRules(entries []PortMapEntry, containerIP net.IP) [][]string { + isV6 := (containerIP.To4() == nil) + + out := make([][]string, 0, len(entries)) + for _, entry := range entries { + out = append(out, []string{ + "-p", entry.Protocol, + "-s", localhostIP(isV6), + "-d", containerIP.String(), + "--dport", strconv.Itoa(entry.ContainerPort), + "-j", "MASQUERADE", + }) + } + return out +} + +// enableLocalnetRouting tells the kernel not to treat 127/8 as a martian, +// so that connections with a source ip of 127/8 can cross a routing boundary. +func enableLocalnetRouting(ifName string) error { + routeLocalnetPath := "net.ipv4.conf." + ifName + ".route_localnet" + _, err := sysctl.Sysctl(routeLocalnetPath, "1") + return err +} + +// unforwardPorts deletes any iptables rules created by this plugin. +// It should be idempotent - it will not error if the chain does not exist. +// +// We also need to be a bit clever about how we handle errors with initializing +// iptables. We may be on a system with no ip(6)tables, or no kernel support +// for that protocol. The ADD would be successful, since it only adds forwarding +// based on the addresses assigned to the container. However, at DELETE time we +// don't know which protocols were used. +// So, we first check that iptables is "generally OK" by doing a check. If +// not, we ignore the error, unless neither v4 nor v6 are OK. +func unforwardPorts(config *PortMapConf) error { + dnatChain := genDnatChain(config.Name, config.ContainerID, nil) + snatChain := genSnatChain(config.Name, config.ContainerID) + + ip4t := maybeGetIptables(false) + ip6t := maybeGetIptables(true) + if ip4t == nil && ip6t == nil { + return fmt.Errorf("neither iptables nor ip6tables usable") + } + + if ip4t != nil { + if err := dnatChain.teardown(ip4t); err != nil { + return fmt.Errorf("could not teardown ipv4 dnat: %v", err) + } + if err := snatChain.teardown(ip4t); err != nil { + return fmt.Errorf("could not teardown ipv4 snat: %v", err) + } + } + + if ip6t != nil { + if err := dnatChain.teardown(ip6t); err != nil { + return fmt.Errorf("could not teardown ipv6 dnat: %v", err) + } + // no SNAT teardown because it doesn't work for v6 + } + return nil +} + +// maybeGetIptables implements the soft error swallowing. If iptables is +// usable for the given protocol, returns a handle, otherwise nil +func maybeGetIptables(isV6 bool) *iptables.IPTables { + proto := iptables.ProtocolIPv4 + if isV6 { + proto = iptables.ProtocolIPv6 + } + + ipt, err := iptables.NewWithProtocol(proto) + if err != nil { + return nil + } + + _, err = ipt.List("nat", "OUTPUT") + if err != nil { + return nil + } + + return ipt +} diff --git a/plugins/meta/portmap/portmap_integ_test.go b/plugins/meta/portmap/portmap_integ_test.go new file mode 100644 index 00000000..69df51c5 --- /dev/null +++ b/plugins/meta/portmap/portmap_integ_test.go @@ -0,0 +1,225 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "net" + "os" + "path/filepath" + "time" + + "github.com/containernetworking/cni/libcni" + "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/coreos/go-iptables/iptables" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/vishvananda/netlink" +) + +var _ = Describe("portmap integration tests", func() { + + var configList *libcni.NetworkConfigList + var cniConf *libcni.CNIConfig + var targetNS ns.NetNS + var containerPort int + var closeChan chan interface{} + + BeforeEach(func() { + var err error + rawConfig := `{ + "cniVersion": "0.3.0", + "name": "cni-portmap-unit-test", + "plugins": [ + { + "type": "ptp", + "ipMasq": true, + "ipam": { + "type": "host-local", + "subnet": "172.16.31.0/24" + } + }, + { + "type": "portmap", + "capabilities": { + "portMappings": true + } + } + ] +}` + + configList, err = libcni.ConfListFromBytes([]byte(rawConfig)) + Expect(err).NotTo(HaveOccurred()) + + // turn PATH in to CNI_PATH + dirs := filepath.SplitList(os.Getenv("PATH")) + cniConf = &libcni.CNIConfig{Path: dirs} + + targetNS, err = ns.NewNS() + Expect(err).NotTo(HaveOccurred()) + fmt.Fprintln(GinkgoWriter, "namespace:", targetNS.Path()) + + // Start an echo server and get the port + containerPort, closeChan, err = RunEchoServerInNS(targetNS) + Expect(err).NotTo(HaveOccurred()) + + }) + + AfterEach(func() { + if targetNS != nil { + targetNS.Close() + } + }) + + // This needs to be done using Ginkgo's asynchronous testing mode. + It("forwards a TCP port on ipv4", func(done Done) { + var err error + hostPort := 9999 + runtimeConfig := libcni.RuntimeConf{ + ContainerID: "unit-test", + NetNS: targetNS.Path(), + IfName: "eth0", + CapabilityArgs: map[string]interface{}{ + "portMappings": []map[string]interface{}{ + { + "hostPort": hostPort, + "containerPort": containerPort, + "protocol": "tcp", + }, + }, + }, + } + + // Make delete idempotent, so we can clean up on failure + netDeleted := false + deleteNetwork := func() error { + if netDeleted { + return nil + } + netDeleted = true + return cniConf.DelNetworkList(configList, &runtimeConfig) + } + + // we'll also manually check the iptables chains + ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) + Expect(err).NotTo(HaveOccurred()) + dnatChainName := genDnatChain("cni-portmap-unit-test", "unit-test", nil).name + + // Create the network + resI, err := cniConf.AddNetworkList(configList, &runtimeConfig) + Expect(err).NotTo(HaveOccurred()) + defer deleteNetwork() + + // Check the chain exists + _, err = ipt.List("nat", dnatChainName) + Expect(err).NotTo(HaveOccurred()) + + result, err := current.GetResult(resI) + Expect(err).NotTo(HaveOccurred()) + var contIP net.IP + + for _, ip := range result.IPs { + if result.Interfaces[ip.Interface].Sandbox == "" { + continue + } + contIP = ip.Address.IP + } + if contIP == nil { + Fail("could not determine container IP") + } + + // Sanity check: verify that the container is reachable directly + contOK := testEchoServer(fmt.Sprintf("%s:%d", contIP.String(), containerPort)) + + // Verify that a connection to the forwarded port works + hostIP := getLocalIP() + dnatOK := testEchoServer(fmt.Sprintf("%s:%d", hostIP, hostPort)) + + // Verify that a connection to localhost works + snatOK := testEchoServer(fmt.Sprintf("%s:%d", "127.0.0.1", hostPort)) + + // Cleanup + close(closeChan) + err = deleteNetwork() + Expect(err).NotTo(HaveOccurred()) + + // Verify iptables rules are gone + _, err = ipt.List("nat", dnatChainName) + Expect(err).To(MatchError(ContainSubstring("iptables: No chain/target/match by that name."))) + + // Check that everything succeeded *after* we clean up the network + if !contOK { + Fail("connection direct to " + contIP.String() + " failed") + } + if !dnatOK { + Fail("Connection to " + hostIP + " was not forwarded") + } + if !snatOK { + Fail("connection to 127.0.0.1 was not forwarded") + } + + close(done) + + }, 5) +}) + +// testEchoServer returns true if we found an echo server on the port +func testEchoServer(address string) bool { + fmt.Fprintln(GinkgoWriter, "dialing", address) + conn, err := net.Dial("tcp", address) + if err != nil { + fmt.Fprintln(GinkgoWriter, "connection to", address, "failed:", err) + return false + } + defer conn.Close() + + conn.SetDeadline(time.Now().Add(2 * time.Second)) + fmt.Fprintln(GinkgoWriter, "connected to", address) + + message := "Aliquid melius quam pessimum optimum non est." + _, err = fmt.Fprint(conn, message) + if err != nil { + fmt.Fprintln(GinkgoWriter, "sending message to", address, " failed:", err) + return false + } + + conn.SetDeadline(time.Now().Add(2 * time.Second)) + fmt.Fprintln(GinkgoWriter, "reading...") + response := make([]byte, len(message)) + _, err = conn.Read(response) + if err != nil { + fmt.Fprintln(GinkgoWriter, "receiving message from", address, " failed:", err) + return false + } + + fmt.Fprintln(GinkgoWriter, "read...") + if string(response) == message { + return true + } + fmt.Fprintln(GinkgoWriter, "returned message didn't match?") + return false +} + +func getLocalIP() string { + addrs, err := netlink.AddrList(nil, netlink.FAMILY_V4) + Expect(err).NotTo(HaveOccurred()) + + for _, addr := range addrs { + return addr.IP.String() + } + Fail("no live addresses") + return "" +} diff --git a/plugins/meta/portmap/portmap_suite_test.go b/plugins/meta/portmap/portmap_suite_test.go new file mode 100644 index 00000000..51e24ff5 --- /dev/null +++ b/plugins/meta/portmap/portmap_suite_test.go @@ -0,0 +1,103 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "net" + "time" + + "github.com/containernetworking/plugins/pkg/ns" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestPortmap(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "portmap Suite") +} + +// OpenEchoServer opens a server that handles one connection before closing. +// It opens on a random port and sends the port number on portChan when +// the server is up and running. If an error is encountered, closes portChan. +// If closeChan is closed, closes the socket. +func OpenEchoServer(portChan chan<- int, closeChan <-chan interface{}) error { + laddr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:0") + if err != nil { + close(portChan) + return err + } + sock, err := net.ListenTCP("tcp", laddr) + if err != nil { + close(portChan) + return err + } + defer sock.Close() + + switch addr := sock.Addr().(type) { + case *net.TCPAddr: + portChan <- addr.Port + default: + close(portChan) + return fmt.Errorf("addr cast failed!") + } + for { + select { + case <-closeChan: + break + default: + } + + sock.SetDeadline(time.Now().Add(time.Second)) + con, err := sock.AcceptTCP() + if err != nil { + if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() { + continue + } + continue + } + + buf := make([]byte, 512) + con.Read(buf) + con.Write(buf) + con.Close() + } +} + +func RunEchoServerInNS(netNS ns.NetNS) (int, chan interface{}, error) { + portChan := make(chan int) + closeChan := make(chan interface{}) + + go func() { + err := netNS.Do(func(ns.NetNS) error { + OpenEchoServer(portChan, closeChan) + return nil + }) + // Somehow the ns.Do failed + if err != nil { + close(portChan) + } + }() + + portNum := <-portChan + if portNum == 0 { + return 0, nil, fmt.Errorf("failed to execute server") + } + + return portNum, closeChan, nil +} diff --git a/plugins/meta/portmap/portmap_test.go b/plugins/meta/portmap/portmap_test.go new file mode 100644 index 00000000..6e284614 --- /dev/null +++ b/plugins/meta/portmap/portmap_test.go @@ -0,0 +1,136 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "net" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("portmapping configuration", func() { + netName := "testNetName" + containerID := "icee6giejonei6sohng6ahngee7laquohquee9shiGo7fohferakah3Feiyoolu2pei7ciPhoh7shaoX6vai3vuf0ahfaeng8yohb9ceu0daez5hashee8ooYai5wa3y" + + mappings := []PortMapEntry{ + {80, 90, "tcp", ""}, + {1000, 2000, "udp", ""}, + } + ipv4addr := net.ParseIP("192.2.0.1") + ipv6addr := net.ParseIP("2001:db8::1") + + Describe("Generating chains", func() { + Context("for DNAT", func() { + It("generates a correct container chain", func() { + ch := genDnatChain(netName, containerID, &[]string{"-m", "hello"}) + + Expect(ch).To(Equal(chain{ + table: "nat", + name: "CNI-DN-bfd599665540dd91d5d28", + entryRule: []string{ + "-m", "comment", + "--comment", `dnat name: "testNetName" id: "` + containerID + `"`, + "-m", "hello", + }, + entryChains: []string{TopLevelDNATChainName}, + })) + }) + + It("generates a correct top-level chain", func() { + ch := genToplevelDnatChain() + + Expect(ch).To(Equal(chain{ + table: "nat", + name: "CNI-HOSTPORT-DNAT", + entryRule: []string{ + "-m", "addrtype", + "--dst-type", "LOCAL", + }, + entryChains: []string{"PREROUTING", "OUTPUT"}, + })) + }) + }) + + Context("for SNAT", func() { + It("generates a correct container chain", func() { + ch := genSnatChain(netName, containerID) + + Expect(ch).To(Equal(chain{ + table: "nat", + name: "CNI-SN-bfd599665540dd91d5d28", + entryRule: []string{ + "-m", "comment", + "--comment", `snat name: "testNetName" id: "` + containerID + `"`, + }, + entryChains: []string{TopLevelSNATChainName}, + })) + }) + + It("generates a correct top-level chain", func() { + Context("for ipv4", func() { + ch := genToplevelSnatChain(false) + Expect(ch).To(Equal(chain{ + table: "nat", + name: "CNI-HOSTPORT-SNAT", + entryRule: []string{ + "-s", "127.0.0.1", + "!", "-d", "127.0.0.1", + }, + entryChains: []string{"POSTROUTING"}, + })) + }) + }) + }) + }) + + Describe("Forwarding rules", func() { + Context("for DNAT", func() { + It("generates correct ipv4 rules", func() { + rules := dnatRules(mappings, ipv4addr) + Expect(rules).To(Equal([][]string{ + {"-p", "tcp", "--dport", "80", "-j", "DNAT", "--to-destination", "192.2.0.1:90"}, + {"-p", "udp", "--dport", "1000", "-j", "DNAT", "--to-destination", "192.2.0.1:2000"}, + })) + }) + It("generates correct ipv6 rules", func() { + rules := dnatRules(mappings, ipv6addr) + Expect(rules).To(Equal([][]string{ + {"-p", "tcp", "--dport", "80", "-j", "DNAT", "--to-destination", "[2001:db8::1]:90"}, + {"-p", "udp", "--dport", "1000", "-j", "DNAT", "--to-destination", "[2001:db8::1]:2000"}, + })) + }) + }) + + Context("for SNAT", func() { + + It("generates correct ipv4 rules", func() { + rules := snatRules(mappings, ipv4addr) + Expect(rules).To(Equal([][]string{ + {"-p", "tcp", "-s", "127.0.0.1", "-d", "192.2.0.1", "--dport", "90", "-j", "MASQUERADE"}, + {"-p", "udp", "-s", "127.0.0.1", "-d", "192.2.0.1", "--dport", "2000", "-j", "MASQUERADE"}, + })) + }) + + It("generates correct ipv6 rules", func() { + rules := snatRules(mappings, ipv6addr) + Expect(rules).To(Equal([][]string{ + {"-p", "tcp", "-s", "::1", "-d", "2001:db8::1", "--dport", "90", "-j", "MASQUERADE"}, + {"-p", "udp", "-s", "::1", "-d", "2001:db8::1", "--dport", "2000", "-j", "MASQUERADE"}, + })) + }) + }) + }) +}) diff --git a/plugins/meta/portmap/utils.go b/plugins/meta/portmap/utils.go new file mode 100644 index 00000000..a0c9b33b --- /dev/null +++ b/plugins/meta/portmap/utils.go @@ -0,0 +1,67 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "crypto/sha512" + "fmt" + "net" + + "github.com/vishvananda/netlink" +) + +const maxChainNameLength = 28 + +// fmtIpPort correctly formats ip:port literals for iptables and ip6tables - +// need to wrap v6 literals in a [] +func fmtIpPort(ip net.IP, port int) string { + if ip.To4() == nil { + return fmt.Sprintf("[%s]:%d", ip.String(), port) + } + return fmt.Sprintf("%s:%d", ip.String(), port) +} + +func localhostIP(isV6 bool) string { + if isV6 { + return "::1" + } + return "127.0.0.1" +} + +// getRoutableHostIF will try and determine which interface routes the container's +// traffic. This is the one on which we disable martian filtering. +func getRoutableHostIF(containerIP net.IP) string { + routes, err := netlink.RouteGet(containerIP) + if err != nil { + return "" + } + + for _, route := range routes { + link, err := netlink.LinkByIndex(route.LinkIndex) + if err != nil { + continue + } + + return link.Attrs().Name + } + + return "" +} + +func formatChainName(prefix, name, id string) string { + chainBytes := sha512.Sum512([]byte(name + id)) + chain := fmt.Sprintf("CNI-%s%x", prefix, chainBytes) + return chain[:maxChainNameLength] +} diff --git a/test.sh b/test.sh index 06888aa8..0300f34d 100755 --- a/test.sh +++ b/test.sh @@ -10,7 +10,7 @@ source ./build.sh echo "Running tests" -TESTABLE="plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/meta/flannel plugins/main/vlan plugins/sample pkg/ip pkg/ipam pkg/ns pkg/utils pkg/utils/hwaddr pkg/utils/sysctl" +TESTABLE="plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/meta/flannel plugins/main/vlan plugins/sample pkg/ip pkg/ipam pkg/ns pkg/utils pkg/utils/hwaddr pkg/utils/sysctl plugins/meta/portmap" # user has not provided PKG override if [ -z "$PKG" ]; then diff --git a/vendor/github.com/containernetworking/cni/libcni/api.go b/vendor/github.com/containernetworking/cni/libcni/api.go new file mode 100644 index 00000000..a23cbb2c --- /dev/null +++ b/vendor/github.com/containernetworking/cni/libcni/api.go @@ -0,0 +1,219 @@ +// Copyright 2015 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package libcni + +import ( + "os" + "strings" + + "github.com/containernetworking/cni/pkg/invoke" + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/version" +) + +type RuntimeConf struct { + ContainerID string + NetNS string + IfName string + Args [][2]string + // A dictionary of capability-specific data passed by the runtime + // to plugins as top-level keys in the 'runtimeConfig' dictionary + // of the plugin's stdin data. libcni will ensure that only keys + // in this map which match the capabilities of the plugin are passed + // to the plugin + CapabilityArgs map[string]interface{} +} + +type NetworkConfig struct { + Network *types.NetConf + Bytes []byte +} + +type NetworkConfigList struct { + Name string + CNIVersion string + Plugins []*NetworkConfig + Bytes []byte +} + +type CNI interface { + AddNetworkList(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error) + DelNetworkList(net *NetworkConfigList, rt *RuntimeConf) error + + AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) + DelNetwork(net *NetworkConfig, rt *RuntimeConf) error +} + +type CNIConfig struct { + Path []string +} + +// CNIConfig implements the CNI interface +var _ CNI = &CNIConfig{} + +func buildOneConfig(list *NetworkConfigList, orig *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (*NetworkConfig, error) { + var err error + + inject := map[string]interface{}{ + "name": list.Name, + "cniVersion": list.CNIVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + orig, err = InjectConf(orig, inject) + if err != nil { + return nil, err + } + + return injectRuntimeConfig(orig, rt) +} + +// This function takes a libcni RuntimeConf structure and injects values into +// a "runtimeConfig" dictionary in the CNI network configuration JSON that +// will be passed to the plugin on stdin. +// +// Only "capabilities arguments" passed by the runtime are currently injected. +// These capabilities arguments are filtered through the plugin's advertised +// capabilities from its config JSON, and any keys in the CapabilityArgs +// matching plugin capabilities are added to the "runtimeConfig" dictionary +// sent to the plugin via JSON on stdin. For exmaple, if the plugin's +// capabilities include "portMappings", and the CapabilityArgs map includes a +// "portMappings" key, that key and its value are added to the "runtimeConfig" +// dictionary to be passed to the plugin's stdin. +func injectRuntimeConfig(orig *NetworkConfig, rt *RuntimeConf) (*NetworkConfig, error) { + var err error + + rc := make(map[string]interface{}) + for capability, supported := range orig.Network.Capabilities { + if !supported { + continue + } + if data, ok := rt.CapabilityArgs[capability]; ok { + rc[capability] = data + } + } + + if len(rc) > 0 { + orig, err = InjectConf(orig, map[string]interface{}{"runtimeConfig": rc}) + if err != nil { + return nil, err + } + } + + return orig, nil +} + +// AddNetworkList executes a sequence of plugins with the ADD command +func (c *CNIConfig) AddNetworkList(list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) { + var prevResult types.Result + for _, net := range list.Plugins { + pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path) + if err != nil { + return nil, err + } + + newConf, err := buildOneConfig(list, net, prevResult, rt) + if err != nil { + return nil, err + } + + prevResult, err = invoke.ExecPluginWithResult(pluginPath, newConf.Bytes, c.args("ADD", rt)) + if err != nil { + return nil, err + } + } + + return prevResult, nil +} + +// DelNetworkList executes a sequence of plugins with the DEL command +func (c *CNIConfig) DelNetworkList(list *NetworkConfigList, rt *RuntimeConf) error { + for i := len(list.Plugins) - 1; i >= 0; i-- { + net := list.Plugins[i] + + pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path) + if err != nil { + return err + } + + newConf, err := buildOneConfig(list, net, nil, rt) + if err != nil { + return err + } + + if err := invoke.ExecPluginWithoutResult(pluginPath, newConf.Bytes, c.args("DEL", rt)); err != nil { + return err + } + } + + return nil +} + +// AddNetwork executes the plugin with the ADD command +func (c *CNIConfig) AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) { + pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path) + if err != nil { + return nil, err + } + + net, err = injectRuntimeConfig(net, rt) + if err != nil { + return nil, err + } + + return invoke.ExecPluginWithResult(pluginPath, net.Bytes, c.args("ADD", rt)) +} + +// DelNetwork executes the plugin with the DEL command +func (c *CNIConfig) DelNetwork(net *NetworkConfig, rt *RuntimeConf) error { + pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path) + if err != nil { + return err + } + + net, err = injectRuntimeConfig(net, rt) + if err != nil { + return err + } + + return invoke.ExecPluginWithoutResult(pluginPath, net.Bytes, c.args("DEL", rt)) +} + +// GetVersionInfo reports which versions of the CNI spec are supported by +// the given plugin. +func (c *CNIConfig) GetVersionInfo(pluginType string) (version.PluginInfo, error) { + pluginPath, err := invoke.FindInPath(pluginType, c.Path) + if err != nil { + return nil, err + } + + return invoke.GetVersionInfo(pluginPath) +} + +// ===== +func (c *CNIConfig) args(action string, rt *RuntimeConf) *invoke.Args { + return &invoke.Args{ + Command: action, + ContainerID: rt.ContainerID, + NetNS: rt.NetNS, + PluginArgs: rt.Args, + IfName: rt.IfName, + Path: strings.Join(c.Path, string(os.PathListSeparator)), + } +} diff --git a/vendor/github.com/containernetworking/cni/libcni/conf.go b/vendor/github.com/containernetworking/cni/libcni/conf.go new file mode 100644 index 00000000..c7738c66 --- /dev/null +++ b/vendor/github.com/containernetworking/cni/libcni/conf.go @@ -0,0 +1,256 @@ +// Copyright 2015 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package libcni + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" +) + +type NotFoundError struct { + Dir string + Name string +} + +func (e NotFoundError) Error() string { + return fmt.Sprintf(`no net configuration with name "%s" in %s`, e.Name, e.Dir) +} + +type NoConfigsFoundError struct { + Dir string +} + +func (e NoConfigsFoundError) Error() string { + return fmt.Sprintf(`no net configurations found in %s`, e.Dir) +} + +func ConfFromBytes(bytes []byte) (*NetworkConfig, error) { + conf := &NetworkConfig{Bytes: bytes} + if err := json.Unmarshal(bytes, &conf.Network); err != nil { + return nil, fmt.Errorf("error parsing configuration: %s", err) + } + return conf, nil +} + +func ConfFromFile(filename string) (*NetworkConfig, error) { + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("error reading %s: %s", filename, err) + } + return ConfFromBytes(bytes) +} + +func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) { + rawList := make(map[string]interface{}) + if err := json.Unmarshal(bytes, &rawList); err != nil { + return nil, fmt.Errorf("error parsing configuration list: %s", err) + } + + rawName, ok := rawList["name"] + if !ok { + return nil, fmt.Errorf("error parsing configuration list: no name") + } + name, ok := rawName.(string) + if !ok { + return nil, fmt.Errorf("error parsing configuration list: invalid name type %T", rawName) + } + + var cniVersion string + rawVersion, ok := rawList["cniVersion"] + if ok { + cniVersion, ok = rawVersion.(string) + if !ok { + return nil, fmt.Errorf("error parsing configuration list: invalid cniVersion type %T", rawVersion) + } + } + + list := &NetworkConfigList{ + Name: name, + CNIVersion: cniVersion, + Bytes: bytes, + } + + var plugins []interface{} + plug, ok := rawList["plugins"] + if !ok { + return nil, fmt.Errorf("error parsing configuration list: no 'plugins' key") + } + plugins, ok = plug.([]interface{}) + if !ok { + return nil, fmt.Errorf("error parsing configuration list: invalid 'plugins' type %T", plug) + } + if len(plugins) == 0 { + return nil, fmt.Errorf("error parsing configuration list: no plugins in list") + } + + for i, conf := range plugins { + newBytes, err := json.Marshal(conf) + if err != nil { + return nil, fmt.Errorf("Failed to marshal plugin config %d: %v", i, err) + } + netConf, err := ConfFromBytes(newBytes) + if err != nil { + return nil, fmt.Errorf("Failed to parse plugin config %d: %v", i, err) + } + list.Plugins = append(list.Plugins, netConf) + } + + return list, nil +} + +func ConfListFromFile(filename string) (*NetworkConfigList, error) { + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("error reading %s: %s", filename, err) + } + return ConfListFromBytes(bytes) +} + +func ConfFiles(dir string, extensions []string) ([]string, error) { + // In part, adapted from rkt/networking/podenv.go#listFiles + files, err := ioutil.ReadDir(dir) + switch { + case err == nil: // break + case os.IsNotExist(err): + return nil, nil + default: + return nil, err + } + + confFiles := []string{} + for _, f := range files { + if f.IsDir() { + continue + } + fileExt := filepath.Ext(f.Name()) + for _, ext := range extensions { + if fileExt == ext { + confFiles = append(confFiles, filepath.Join(dir, f.Name())) + } + } + } + return confFiles, nil +} + +func LoadConf(dir, name string) (*NetworkConfig, error) { + files, err := ConfFiles(dir, []string{".conf", ".json"}) + switch { + case err != nil: + return nil, err + case len(files) == 0: + return nil, NoConfigsFoundError{Dir: dir} + } + sort.Strings(files) + + for _, confFile := range files { + conf, err := ConfFromFile(confFile) + if err != nil { + return nil, err + } + if conf.Network.Name == name { + return conf, nil + } + } + return nil, NotFoundError{dir, name} +} + +func LoadConfList(dir, name string) (*NetworkConfigList, error) { + files, err := ConfFiles(dir, []string{".conflist"}) + if err != nil { + return nil, err + } + sort.Strings(files) + + for _, confFile := range files { + conf, err := ConfListFromFile(confFile) + if err != nil { + return nil, err + } + if conf.Name == name { + return conf, nil + } + } + + // Try and load a network configuration file (instead of list) + // from the same name, then upconvert. + singleConf, err := LoadConf(dir, name) + if err != nil { + // A little extra logic so the error makes sense + if _, ok := err.(NoConfigsFoundError); len(files) != 0 && ok { + // Config lists found but no config files found + return nil, NotFoundError{dir, name} + } + + return nil, err + } + return ConfListFromConf(singleConf) +} + +func InjectConf(original *NetworkConfig, newValues map[string]interface{}) (*NetworkConfig, error) { + config := make(map[string]interface{}) + err := json.Unmarshal(original.Bytes, &config) + if err != nil { + return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range newValues { + if key == "" { + return nil, fmt.Errorf("keys cannot be empty") + } + + if value == nil { + return nil, fmt.Errorf("key '%s' value must not be nil", key) + } + + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, err + } + + return ConfFromBytes(newBytes) +} + +// ConfListFromConf "upconverts" a network config in to a NetworkConfigList, +// with the single network as the only entry in the list. +func ConfListFromConf(original *NetworkConfig) (*NetworkConfigList, error) { + // Re-deserialize the config's json, then make a raw map configlist. + // This may seem a bit strange, but it's to make the Bytes fields + // actually make sense. Otherwise, the generated json is littered with + // golang default values. + + rawConfig := make(map[string]interface{}) + if err := json.Unmarshal(original.Bytes, &rawConfig); err != nil { + return nil, err + } + + rawConfigList := map[string]interface{}{ + "name": original.Network.Name, + "cniVersion": original.Network.CNIVersion, + "plugins": []interface{}{rawConfig}, + } + + b, err := json.Marshal(rawConfigList) + if err != nil { + return nil, err + } + return ConfListFromBytes(b) +} diff --git a/vendor/github.com/coreos/go-iptables/LICENSE b/vendor/github.com/coreos/go-iptables/LICENSE new file mode 100644 index 00000000..37ec93a1 --- /dev/null +++ b/vendor/github.com/coreos/go-iptables/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/coreos/go-iptables/iptables/iptables.go b/vendor/github.com/coreos/go-iptables/iptables/iptables.go index 4b2f2f2f..c073837e 100644 --- a/vendor/github.com/coreos/go-iptables/iptables/iptables.go +++ b/vendor/github.com/coreos/go-iptables/iptables/iptables.go @@ -39,29 +39,52 @@ func (e *Error) Error() string { return fmt.Sprintf("exit status %v: %v", e.ExitStatus(), e.msg) } +// Protocol to differentiate between IPv4 and IPv6 +type Protocol byte + +const ( + ProtocolIPv4 Protocol = iota + ProtocolIPv6 +) + type IPTables struct { path string + proto Protocol hasCheck bool hasWait bool } +// New creates a new IPTables. +// For backwards compatibility, this always uses IPv4, i.e. "iptables". func New() (*IPTables, error) { - path, err := exec.LookPath("iptables") + return NewWithProtocol(ProtocolIPv4) +} + +// New creates a new IPTables for the given proto. +// The proto will determine which command is used, either "iptables" or "ip6tables". +func NewWithProtocol(proto Protocol) (*IPTables, error) { + path, err := exec.LookPath(getIptablesCommand(proto)) if err != nil { return nil, err } - checkPresent, waitPresent, err := getIptablesCommandSupport() + checkPresent, waitPresent, err := getIptablesCommandSupport(path) if err != nil { return nil, fmt.Errorf("error checking iptables version: %v", err) } ipt := IPTables{ path: path, + proto: proto, hasCheck: checkPresent, hasWait: waitPresent, } return &ipt, nil } +// Proto returns the protocol used by this IPTables. +func (ipt *IPTables) Proto() Protocol { + return ipt.proto +} + // Exists checks if given rulespec in specified table/chain exists func (ipt *IPTables) Exists(table, chain string, rulespec ...string) (bool, error) { if !ipt.hasCheck { @@ -116,6 +139,41 @@ func (ipt *IPTables) Delete(table, chain string, rulespec ...string) error { // List rules in specified table/chain func (ipt *IPTables) List(table, chain string) ([]string, error) { args := []string{"-t", table, "-S", chain} + return ipt.executeList(args) +} + +// List rules (with counters) in specified table/chain +func (ipt *IPTables) ListWithCounters(table, chain string) ([]string, error) { + args := []string{"-t", table, "-v", "-S", chain} + return ipt.executeList(args) +} + +// ListChains returns a slice containing the name of each chain in the specified table. +func (ipt *IPTables) ListChains(table string) ([]string, error) { + args := []string{"-t", table, "-S"} + + result, err := ipt.executeList(args) + if err != nil { + return nil, err + } + + // Iterate over rules to find all default (-P) and user-specified (-N) chains. + // Chains definition always come before rules. + // Format is the following: + // -P OUTPUT ACCEPT + // -N Custom + var chains []string + for _, val := range result { + if strings.HasPrefix(val, "-P") || strings.HasPrefix(val, "-N") { + chains = append(chains, strings.Fields(val)[1]) + } else { + break + } + } + return chains, nil +} + +func (ipt *IPTables) executeList(args []string) ([]string, error) { var stdout bytes.Buffer if err := ipt.runWithOutput(args, &stdout); err != nil { return nil, err @@ -129,6 +187,8 @@ func (ipt *IPTables) List(table, chain string) ([]string, error) { return rules, nil } +// NewChain creates a new chain in the specified table. +// If the chain already exists, it will result in an error. func (ipt *IPTables) NewChain(table, chain string) error { return ipt.run("-t", table, "-N", chain) } @@ -200,9 +260,18 @@ func (ipt *IPTables) runWithOutput(args []string, stdout io.Writer) error { return nil } +// getIptablesCommand returns the correct command for the given protocol, either "iptables" or "ip6tables". +func getIptablesCommand(proto Protocol) string { + if proto == ProtocolIPv6 { + return "ip6tables" + } else { + return "iptables" + } +} + // Checks if iptables has the "-C" and "--wait" flag -func getIptablesCommandSupport() (bool, bool, error) { - vstring, err := getIptablesVersionString() +func getIptablesCommandSupport(path string) (bool, bool, error) { + vstring, err := getIptablesVersionString(path) if err != nil { return false, false, err } @@ -243,8 +312,8 @@ func extractIptablesVersion(str string) (int, int, int, error) { } // Runs "iptables --version" to get the version string -func getIptablesVersionString() (string, error) { - cmd := exec.Command("iptables", "--version") +func getIptablesVersionString(path string) (string, error) { + cmd := exec.Command(path, "--version") var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() diff --git a/vendor/github.com/mattn/go-shellwords/.travis.yml b/vendor/github.com/mattn/go-shellwords/.travis.yml new file mode 100644 index 00000000..16d1430a --- /dev/null +++ b/vendor/github.com/mattn/go-shellwords/.travis.yml @@ -0,0 +1,8 @@ +language: go +go: + - tip +before_install: + - go get github.com/mattn/goveralls + - go get golang.org/x/tools/cmd/cover +script: + - $HOME/gopath/bin/goveralls -repotoken 2FMhp57u8LcstKL9B190fLTcEnBtAAiEL diff --git a/vendor/github.com/mattn/go-shellwords/LICENSE b/vendor/github.com/mattn/go-shellwords/LICENSE new file mode 100644 index 00000000..740fa931 --- /dev/null +++ b/vendor/github.com/mattn/go-shellwords/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Yasuhiro Matsumoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/mattn/go-shellwords/README.md b/vendor/github.com/mattn/go-shellwords/README.md new file mode 100644 index 00000000..b1d235c7 --- /dev/null +++ b/vendor/github.com/mattn/go-shellwords/README.md @@ -0,0 +1,47 @@ +# go-shellwords + +[![Coverage Status](https://coveralls.io/repos/mattn/go-shellwords/badge.png?branch=master)](https://coveralls.io/r/mattn/go-shellwords?branch=master) +[![Build Status](https://travis-ci.org/mattn/go-shellwords.svg?branch=master)](https://travis-ci.org/mattn/go-shellwords) + +Parse line as shell words. + +## Usage + +```go +args, err := shellwords.Parse("./foo --bar=baz") +// args should be ["./foo", "--bar=baz"] +``` + +```go +os.Setenv("FOO", "bar") +p := shellwords.NewParser() +p.ParseEnv = true +args, err := p.Parse("./foo $FOO") +// args should be ["./foo", "bar"] +``` + +```go +p := shellwords.NewParser() +p.ParseBacktick = true +args, err := p.Parse("./foo `echo $SHELL`") +// args should be ["./foo", "/bin/bash"] +``` + +```go +shellwords.ParseBacktick = true +p := shellwords.NewParser() +args, err := p.Parse("./foo `echo $SHELL`") +// args should be ["./foo", "/bin/bash"] +``` + +# Thanks + +This is based on cpan module [Parse::CommandLine](https://metacpan.org/pod/Parse::CommandLine). + +# License + +under the MIT License: http://mattn.mit-license.org/2017 + +# Author + +Yasuhiro Matsumoto (a.k.a mattn) diff --git a/vendor/github.com/mattn/go-shellwords/shellwords.go b/vendor/github.com/mattn/go-shellwords/shellwords.go new file mode 100644 index 00000000..10780392 --- /dev/null +++ b/vendor/github.com/mattn/go-shellwords/shellwords.go @@ -0,0 +1,145 @@ +package shellwords + +import ( + "errors" + "os" + "regexp" +) + +var ( + ParseEnv bool = false + ParseBacktick bool = false +) + +var envRe = regexp.MustCompile(`\$({[a-zA-Z0-9_]+}|[a-zA-Z0-9_]+)`) + +func isSpace(r rune) bool { + switch r { + case ' ', '\t', '\r', '\n': + return true + } + return false +} + +func replaceEnv(s string) string { + return envRe.ReplaceAllStringFunc(s, func(s string) string { + s = s[1:] + if s[0] == '{' { + s = s[1 : len(s)-1] + } + return os.Getenv(s) + }) +} + +type Parser struct { + ParseEnv bool + ParseBacktick bool + Position int +} + +func NewParser() *Parser { + return &Parser{ParseEnv, ParseBacktick, 0} +} + +func (p *Parser) Parse(line string) ([]string, error) { + args := []string{} + buf := "" + var escaped, doubleQuoted, singleQuoted, backQuote bool + backtick := "" + + pos := -1 + got := false + +loop: + for i, r := range line { + if escaped { + buf += string(r) + escaped = false + continue + } + + if r == '\\' { + if singleQuoted { + buf += string(r) + } else { + escaped = true + } + continue + } + + if isSpace(r) { + if singleQuoted || doubleQuoted || backQuote { + buf += string(r) + backtick += string(r) + } else if got { + if p.ParseEnv { + buf = replaceEnv(buf) + } + args = append(args, buf) + buf = "" + got = false + } + continue + } + + switch r { + case '`': + if !singleQuoted && !doubleQuoted { + if p.ParseBacktick { + if backQuote { + out, err := shellRun(backtick) + if err != nil { + return nil, err + } + buf = out + } + backtick = "" + backQuote = !backQuote + continue + } + backtick = "" + backQuote = !backQuote + } + case '"': + if !singleQuoted { + doubleQuoted = !doubleQuoted + continue + } + case '\'': + if !doubleQuoted { + singleQuoted = !singleQuoted + continue + } + case ';', '&', '|', '<', '>': + if !(escaped || singleQuoted || doubleQuoted || backQuote) { + pos = i + break loop + } + } + + got = true + buf += string(r) + if backQuote { + backtick += string(r) + } + } + + if got { + if p.ParseEnv { + buf = replaceEnv(buf) + } + args = append(args, buf) + } + + if escaped || singleQuoted || doubleQuoted || backQuote { + return nil, errors.New("invalid command line string") + } + + p.Position = pos + + return args, nil +} + +func Parse(line string) ([]string, error) { + return NewParser().Parse(line) +} diff --git a/vendor/github.com/mattn/go-shellwords/util_posix.go b/vendor/github.com/mattn/go-shellwords/util_posix.go new file mode 100644 index 00000000..4f8ac55e --- /dev/null +++ b/vendor/github.com/mattn/go-shellwords/util_posix.go @@ -0,0 +1,19 @@ +// +build !windows + +package shellwords + +import ( + "errors" + "os" + "os/exec" + "strings" +) + +func shellRun(line string) (string, error) { + shell := os.Getenv("SHELL") + b, err := exec.Command(shell, "-c", line).Output() + if err != nil { + return "", errors.New(err.Error() + ":" + string(b)) + } + return strings.TrimSpace(string(b)), nil +} diff --git a/vendor/github.com/mattn/go-shellwords/util_windows.go b/vendor/github.com/mattn/go-shellwords/util_windows.go new file mode 100644 index 00000000..7cad4cf0 --- /dev/null +++ b/vendor/github.com/mattn/go-shellwords/util_windows.go @@ -0,0 +1,17 @@ +package shellwords + +import ( + "errors" + "os" + "os/exec" + "strings" +) + +func shellRun(line string) (string, error) { + shell := os.Getenv("COMSPEC") + b, err := exec.Command(shell, "/c", line).Output() + if err != nil { + return "", errors.New(err.Error() + ":" + string(b)) + } + return strings.TrimSpace(string(b)), nil +}