plugins/meta/portmap: add an iptables-based host port mapping plugin
This commit is contained in:
parent
22cda76afb
commit
a7aaf0e377
@ -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.
|
||||
|
117
plugins/meta/portmap/README.md
Normal file
117
plugins/meta/portmap/README.md
Normal file
@ -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.
|
127
plugins/meta/portmap/chain.go
Normal file
127
plugins/meta/portmap/chain.go
Normal file
@ -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
|
||||
}
|
203
plugins/meta/portmap/chain_test.go
Normal file
203
plugins/meta/portmap/chain_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
159
plugins/meta/portmap/main.go
Normal file
159
plugins/meta/portmap/main.go
Normal file
@ -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
|
||||
}
|
294
plugins/meta/portmap/portmap.go
Normal file
294
plugins/meta/portmap/portmap.go
Normal file
@ -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
|
||||
}
|
225
plugins/meta/portmap/portmap_integ_test.go
Normal file
225
plugins/meta/portmap/portmap_integ_test.go
Normal file
@ -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 ""
|
||||
}
|
103
plugins/meta/portmap/portmap_suite_test.go
Normal file
103
plugins/meta/portmap/portmap_suite_test.go
Normal file
@ -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
|
||||
}
|
136
plugins/meta/portmap/portmap_test.go
Normal file
136
plugins/meta/portmap/portmap_test.go
Normal file
@ -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"},
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
67
plugins/meta/portmap/utils.go
Normal file
67
plugins/meta/portmap/utils.go
Normal file
@ -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]
|
||||
}
|
2
test.sh
2
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user