Merge pull request #1 from squeed/portmap-plugin
iptables-based portmapping plugin
This commit is contained in:
14
Godeps/Godeps.json
generated
14
Godeps/Godeps.json
generated
@ -6,6 +6,11 @@
|
|||||||
"./..."
|
"./..."
|
||||||
],
|
],
|
||||||
"Deps": [
|
"Deps": [
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/containernetworking/cni/libcni",
|
||||||
|
"Comment": "v0.5.2",
|
||||||
|
"Rev": "137b4975ecab6e1f0c24c1e3c228a50a3cfba75e"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/containernetworking/cni/pkg/invoke",
|
"ImportPath": "github.com/containernetworking/cni/pkg/invoke",
|
||||||
"Comment": "v0.5.2",
|
"Comment": "v0.5.2",
|
||||||
@ -38,8 +43,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/coreos/go-iptables/iptables",
|
"ImportPath": "github.com/coreos/go-iptables/iptables",
|
||||||
"Comment": "v0.1.0",
|
"Comment": "v0.1.0-9-g197187d",
|
||||||
"Rev": "fbb73372b87f6e89951c2b6b31470c2c9d5cfae3"
|
"Rev": "197187d414d7704f99ea52a692b9672e76f063bf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/coreos/go-systemd/activation",
|
"ImportPath": "github.com/coreos/go-systemd/activation",
|
||||||
@ -54,6 +59,11 @@
|
|||||||
"ImportPath": "github.com/d2g/dhcp4client",
|
"ImportPath": "github.com/d2g/dhcp4client",
|
||||||
"Rev": "bed07e1bc5b85f69c6f0fd73393aa35ec68ed892"
|
"Rev": "bed07e1bc5b85f69c6f0fd73393aa35ec68ed892"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/mattn/go-shellwords",
|
||||||
|
"Comment": "v1.0.3",
|
||||||
|
"Rev": "02e3cf038dcea8290e44424da473dd12be796a8a"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/onsi/ginkgo",
|
"ImportPath": "github.com/onsi/ginkgo",
|
||||||
"Comment": "v1.2.0-29-g7f8ab55",
|
"Comment": "v1.2.0-29-g7f8ab55",
|
||||||
|
@ -17,7 +17,7 @@ Some CNI network plugins, maintained by the containernetworking team. For more i
|
|||||||
### Meta: other plugins
|
### Meta: other plugins
|
||||||
* `flannel`: generates an interface corresponding to a flannel config file
|
* `flannel`: generates an interface corresponding to a flannel config file
|
||||||
* `tuning`: Tweaks sysctl parameters of an existing interface
|
* `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
|
### Sample
|
||||||
The sample plugin provides an example for building your own plugin.
|
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"
|
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
|
# user has not provided PKG override
|
||||||
if [ -z "$PKG" ]; then
|
if [ -z "$PKG" ]; then
|
||||||
|
219
vendor/github.com/containernetworking/cni/libcni/api.go
generated
vendored
Normal file
219
vendor/github.com/containernetworking/cni/libcni/api.go
generated
vendored
Normal file
@ -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)),
|
||||||
|
}
|
||||||
|
}
|
256
vendor/github.com/containernetworking/cni/libcni/conf.go
generated
vendored
Normal file
256
vendor/github.com/containernetworking/cni/libcni/conf.go
generated
vendored
Normal file
@ -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)
|
||||||
|
}
|
191
vendor/github.com/coreos/go-iptables/LICENSE
generated
vendored
Normal file
191
vendor/github.com/coreos/go-iptables/LICENSE
generated
vendored
Normal file
@ -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.
|
81
vendor/github.com/coreos/go-iptables/iptables/iptables.go
generated
vendored
81
vendor/github.com/coreos/go-iptables/iptables/iptables.go
generated
vendored
@ -39,29 +39,52 @@ func (e *Error) Error() string {
|
|||||||
return fmt.Sprintf("exit status %v: %v", e.ExitStatus(), e.msg)
|
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 {
|
type IPTables struct {
|
||||||
path string
|
path string
|
||||||
|
proto Protocol
|
||||||
hasCheck bool
|
hasCheck bool
|
||||||
hasWait bool
|
hasWait bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new IPTables.
|
||||||
|
// For backwards compatibility, this always uses IPv4, i.e. "iptables".
|
||||||
func New() (*IPTables, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
checkPresent, waitPresent, err := getIptablesCommandSupport()
|
checkPresent, waitPresent, err := getIptablesCommandSupport(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error checking iptables version: %v", err)
|
return nil, fmt.Errorf("error checking iptables version: %v", err)
|
||||||
}
|
}
|
||||||
ipt := IPTables{
|
ipt := IPTables{
|
||||||
path: path,
|
path: path,
|
||||||
|
proto: proto,
|
||||||
hasCheck: checkPresent,
|
hasCheck: checkPresent,
|
||||||
hasWait: waitPresent,
|
hasWait: waitPresent,
|
||||||
}
|
}
|
||||||
return &ipt, nil
|
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
|
// Exists checks if given rulespec in specified table/chain exists
|
||||||
func (ipt *IPTables) Exists(table, chain string, rulespec ...string) (bool, error) {
|
func (ipt *IPTables) Exists(table, chain string, rulespec ...string) (bool, error) {
|
||||||
if !ipt.hasCheck {
|
if !ipt.hasCheck {
|
||||||
@ -116,6 +139,41 @@ func (ipt *IPTables) Delete(table, chain string, rulespec ...string) error {
|
|||||||
// List rules in specified table/chain
|
// List rules in specified table/chain
|
||||||
func (ipt *IPTables) List(table, chain string) ([]string, error) {
|
func (ipt *IPTables) List(table, chain string) ([]string, error) {
|
||||||
args := []string{"-t", table, "-S", chain}
|
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
|
var stdout bytes.Buffer
|
||||||
if err := ipt.runWithOutput(args, &stdout); err != nil {
|
if err := ipt.runWithOutput(args, &stdout); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -129,6 +187,8 @@ func (ipt *IPTables) List(table, chain string) ([]string, error) {
|
|||||||
return rules, nil
|
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 {
|
func (ipt *IPTables) NewChain(table, chain string) error {
|
||||||
return ipt.run("-t", table, "-N", chain)
|
return ipt.run("-t", table, "-N", chain)
|
||||||
}
|
}
|
||||||
@ -200,9 +260,18 @@ func (ipt *IPTables) runWithOutput(args []string, stdout io.Writer) error {
|
|||||||
return nil
|
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
|
// Checks if iptables has the "-C" and "--wait" flag
|
||||||
func getIptablesCommandSupport() (bool, bool, error) {
|
func getIptablesCommandSupport(path string) (bool, bool, error) {
|
||||||
vstring, err := getIptablesVersionString()
|
vstring, err := getIptablesVersionString(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, err
|
return false, false, err
|
||||||
}
|
}
|
||||||
@ -243,8 +312,8 @@ func extractIptablesVersion(str string) (int, int, int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Runs "iptables --version" to get the version string
|
// Runs "iptables --version" to get the version string
|
||||||
func getIptablesVersionString() (string, error) {
|
func getIptablesVersionString(path string) (string, error) {
|
||||||
cmd := exec.Command("iptables", "--version")
|
cmd := exec.Command(path, "--version")
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
cmd.Stdout = &out
|
cmd.Stdout = &out
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
8
vendor/github.com/mattn/go-shellwords/.travis.yml
generated
vendored
Normal file
8
vendor/github.com/mattn/go-shellwords/.travis.yml
generated
vendored
Normal file
@ -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
|
21
vendor/github.com/mattn/go-shellwords/LICENSE
generated
vendored
Normal file
21
vendor/github.com/mattn/go-shellwords/LICENSE
generated
vendored
Normal file
@ -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.
|
47
vendor/github.com/mattn/go-shellwords/README.md
generated
vendored
Normal file
47
vendor/github.com/mattn/go-shellwords/README.md
generated
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# go-shellwords
|
||||||
|
|
||||||
|
[](https://coveralls.io/r/mattn/go-shellwords?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)
|
145
vendor/github.com/mattn/go-shellwords/shellwords.go
generated
vendored
Normal file
145
vendor/github.com/mattn/go-shellwords/shellwords.go
generated
vendored
Normal file
@ -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)
|
||||||
|
}
|
19
vendor/github.com/mattn/go-shellwords/util_posix.go
generated
vendored
Normal file
19
vendor/github.com/mattn/go-shellwords/util_posix.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
17
vendor/github.com/mattn/go-shellwords/util_windows.go
generated
vendored
Normal file
17
vendor/github.com/mattn/go-shellwords/util_windows.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
Reference in New Issue
Block a user