Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
62b36d2fbc | |||
e5fdd449dd | |||
8db5e4d41b | |||
ec8f6c99d0 | |||
7dea2a4c1b | |||
5a02c5bc61 | |||
bf8f171041 | |||
3603738c6a | |||
d8b1289098 | |||
6551165853 | |||
10a01b09ae | |||
497560f35f | |||
58dd90b996 |
@ -18,6 +18,7 @@ env:
|
|||||||
- TARGET=arm64
|
- TARGET=arm64
|
||||||
- TARGET=ppc64le
|
- TARGET=ppc64le
|
||||||
- TARGET=s390x
|
- TARGET=s390x
|
||||||
|
- TARGET=mips64le
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
# Owners
|
# Owners
|
||||||
This is the official list of the CNI network plugins owners:
|
This is the official list of the CNI network plugins owners:
|
||||||
|
- Bruce Ma <brucema19901024@gmail.com> (@mars1024)
|
||||||
- Bryan Boreham <bryan@weave.works> (@bboreham)
|
- Bryan Boreham <bryan@weave.works> (@bboreham)
|
||||||
- Casey Callendrello <casey.callendrello@coreos.com> (@squeed)
|
- Casey Callendrello <casey.callendrello@coreos.com> (@squeed)
|
||||||
- Dan Williams <dcbw@redhat.com> (@dcbw)
|
- Dan Williams <dcbw@redhat.com> (@dcbw)
|
||||||
- Gabe Rosenhouse <grosenhouse@pivotal.io> (@rosenhouse)
|
- Gabe Rosenhouse <grosenhouse@pivotal.io> (@rosenhouse)
|
||||||
- Matt Dupre <matt@tigera.io> (@matthewdupre)
|
- Matt Dupre <matt@tigera.io> (@matthewdupre)
|
||||||
|
- Piotr Skarmuk <piotr.skarmuk@gmail.com> (@jellonek)
|
||||||
- Stefan Junker <stefan.junker@coreos.com> (@steveeJ)
|
- Stefan Junker <stefan.junker@coreos.com> (@steveeJ)
|
2
go.mod
2
go.mod
@ -8,7 +8,7 @@ require (
|
|||||||
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae
|
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae
|
||||||
github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44
|
github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44
|
||||||
github.com/containernetworking/cni v0.7.1
|
github.com/containernetworking/cni v0.7.1
|
||||||
github.com/coreos/go-iptables v0.4.2
|
github.com/coreos/go-iptables v0.4.5
|
||||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7
|
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7
|
||||||
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c
|
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c
|
||||||
github.com/d2g/dhcp4client v1.0.0
|
github.com/d2g/dhcp4client v1.0.0
|
||||||
|
4
go.sum
4
go.sum
@ -10,6 +10,10 @@ github.com/containernetworking/cni v0.7.1 h1:fE3r16wpSEyaqY4Z4oFrLMmIGfBYIKpPrHK
|
|||||||
github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
|
github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
|
||||||
github.com/coreos/go-iptables v0.4.2 h1:KH0EwId05JwWIfb96gWvkiT2cbuOu8ygqUaB+yPAwIg=
|
github.com/coreos/go-iptables v0.4.2 h1:KH0EwId05JwWIfb96gWvkiT2cbuOu8ygqUaB+yPAwIg=
|
||||||
github.com/coreos/go-iptables v0.4.2/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
|
github.com/coreos/go-iptables v0.4.2/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
|
||||||
|
github.com/coreos/go-iptables v0.4.4 h1:5oOUvU7Fk53Hn/rkdJ0zcYGCffotqXpyi4ADCkO1TJ8=
|
||||||
|
github.com/coreos/go-iptables v0.4.4/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
|
||||||
|
github.com/coreos/go-iptables v0.4.5 h1:DpHb9vJrZQEFMcVLFKAAGMUVX0XoRC0ptCthinRYm38=
|
||||||
|
github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
|
||||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM=
|
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM=
|
||||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c h1:Xo2rK1pzOm0jO6abTPIQwbAmqBIOj132otexc1mmzFc=
|
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c h1:Xo2rK1pzOm0jO6abTPIQwbAmqBIOj132otexc1mmzFc=
|
||||||
|
121
pkg/utils/iptables.go
Normal file
121
pkg/utils/iptables.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
// 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 utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
)
|
||||||
|
|
||||||
|
const statusChainExists = 1
|
||||||
|
|
||||||
|
// EnsureChain idempotently creates the iptables chain. It does not
|
||||||
|
// return an error if the chain already exists.
|
||||||
|
func EnsureChain(ipt *iptables.IPTables, table, chain string) error {
|
||||||
|
if ipt == nil {
|
||||||
|
return errors.New("failed to ensure iptable chain: IPTables was nil")
|
||||||
|
}
|
||||||
|
exists, err := ChainExists(ipt, table, chain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list iptables chains: %v", err)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
err = ipt.NewChain(table, chain)
|
||||||
|
if err != nil {
|
||||||
|
eerr, eok := err.(*iptables.Error)
|
||||||
|
if eok && eerr.ExitStatus() != statusChainExists {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChainExists checks whether an iptables chain exists.
|
||||||
|
func ChainExists(ipt *iptables.IPTables, table, chain string) (bool, error) {
|
||||||
|
if ipt == nil {
|
||||||
|
return false, errors.New("failed to check iptable chain: IPTables was nil")
|
||||||
|
}
|
||||||
|
chains, err := ipt.ListChains(table)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ch := range chains {
|
||||||
|
if ch == chain {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRule idempotently delete the iptables rule in the specified table/chain.
|
||||||
|
// It does not return an error if the referring chain doesn't exist
|
||||||
|
func DeleteRule(ipt *iptables.IPTables, table, chain string, rulespec ...string) error {
|
||||||
|
if ipt == nil {
|
||||||
|
return errors.New("failed to ensure iptable chain: IPTables was nil")
|
||||||
|
}
|
||||||
|
if err := ipt.Delete(table, chain, rulespec...); err != nil {
|
||||||
|
eerr, eok := err.(*iptables.Error)
|
||||||
|
switch {
|
||||||
|
case eok && eerr.IsNotExist():
|
||||||
|
// swallow here, the chain was already deleted
|
||||||
|
return nil
|
||||||
|
case eok && eerr.ExitStatus() == 2:
|
||||||
|
// swallow here, invalid command line parameter because the referring rule is missing
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Failed to delete referring rule %s %s: %v", table, chain, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteChain idempotently deletes the specified table/chain.
|
||||||
|
// It does not return an errors if the chain does not exist
|
||||||
|
func DeleteChain(ipt *iptables.IPTables, table, chain string) error {
|
||||||
|
if ipt == nil {
|
||||||
|
return errors.New("failed to ensure iptable chain: IPTables was nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ipt.DeleteChain(table, chain)
|
||||||
|
eerr, eok := err.(*iptables.Error)
|
||||||
|
switch {
|
||||||
|
case eok && eerr.IsNotExist():
|
||||||
|
// swallow here, the chain was already deleted
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearChain idempotently clear the iptables rules in the specified table/chain.
|
||||||
|
// If the chain does not exist, a new one will be created
|
||||||
|
func ClearChain(ipt *iptables.IPTables, table, chain string) error {
|
||||||
|
if ipt == nil {
|
||||||
|
return errors.New("failed to ensure iptable chain: IPTables was nil")
|
||||||
|
}
|
||||||
|
err := ipt.ClearChain(table, chain)
|
||||||
|
eerr, eok := err.(*iptables.Error)
|
||||||
|
switch {
|
||||||
|
case eok && eerr.IsNotExist():
|
||||||
|
// swallow here, the chain was already deleted
|
||||||
|
return EnsureChain(ipt, table, chain)
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
97
pkg/utils/iptables_test.go
Normal file
97
pkg/utils/iptables_test.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
// Copyright 2017-2018 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 utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/containernetworking/plugins/pkg/ns"
|
||||||
|
"github.com/containernetworking/plugins/pkg/testutils"
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TABLE = "filter" // We'll monkey around here
|
||||||
|
|
||||||
|
var _ = Describe("chain tests", func() {
|
||||||
|
var testChain string
|
||||||
|
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 := testutils.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
testChain = fmt.Sprintf("cni-test-%d", rand.Intn(10000000))
|
||||||
|
|
||||||
|
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
runtime.LockOSThread()
|
||||||
|
err = testNs.Set()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
cleanup = func() {
|
||||||
|
if ipt == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ipt.ClearChain(TABLE, testChain)
|
||||||
|
ipt.DeleteChain(TABLE, testChain)
|
||||||
|
currNs.Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("EnsureChain", func() {
|
||||||
|
It("creates chains idempotently", func() {
|
||||||
|
err := EnsureChain(ipt, TABLE, testChain)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Create it again!
|
||||||
|
err = EnsureChain(ipt, TABLE, testChain)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("DeleteChain", func() {
|
||||||
|
It("delete chains idempotently", func() {
|
||||||
|
// Create chain
|
||||||
|
err := EnsureChain(ipt, TABLE, testChain)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Delete chain
|
||||||
|
err = DeleteChain(ipt, TABLE, testChain)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Delete it again!
|
||||||
|
err = DeleteChain(ipt, TABLE, testChain)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
@ -14,17 +14,18 @@ If the bridge is missing, the plugin will create one on first use and, if gatewa
|
|||||||
## Example configuration
|
## Example configuration
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"name": "mynet",
|
"cniVersion": "0.3.1",
|
||||||
"type": "bridge",
|
"name": "mynet",
|
||||||
"bridge": "mynet0",
|
"type": "bridge",
|
||||||
"isDefaultGateway": true,
|
"bridge": "mynet0",
|
||||||
"forceAddress": false,
|
"isDefaultGateway": true,
|
||||||
"ipMasq": true,
|
"forceAddress": false,
|
||||||
"hairpinMode": true,
|
"ipMasq": true,
|
||||||
"ipam": {
|
"hairpinMode": true,
|
||||||
"type": "host-local",
|
"ipam": {
|
||||||
"subnet": "10.10.0.0/16"
|
"type": "host-local",
|
||||||
}
|
"subnet": "10.10.0.0/16"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -32,10 +33,10 @@ If the bridge is missing, the plugin will create one on first use and, if gatewa
|
|||||||
```
|
```
|
||||||
{
|
{
|
||||||
"cniVersion": "0.3.1",
|
"cniVersion": "0.3.1",
|
||||||
"name": "mynet",
|
"name": "mynet",
|
||||||
"type": "bridge",
|
"type": "bridge",
|
||||||
"bridge": "mynet0",
|
"bridge": "mynet0",
|
||||||
"ipam": {}
|
"ipam": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -270,6 +270,13 @@ var _ = Describe("firewall plugin iptables backend", func() {
|
|||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
validateFullRuleset(fullConf)
|
validateFullRuleset(fullConf)
|
||||||
|
|
||||||
|
// ensure creation is idempotent
|
||||||
|
_, _, err = testutils.CmdAdd(targetNS.Path(), args.ContainerID, IFNAME, fullConf, func() error {
|
||||||
|
return cmdAdd(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/containernetworking/cni/pkg/types/current"
|
"github.com/containernetworking/cni/pkg/types/current"
|
||||||
|
"github.com/containernetworking/plugins/pkg/utils"
|
||||||
"github.com/coreos/go-iptables/iptables"
|
"github.com/coreos/go-iptables/iptables"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,20 +33,6 @@ func getPrivChainRules(ip string) [][]string {
|
|||||||
return rules
|
return rules
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureChain(ipt *iptables.IPTables, table, chain string) error {
|
|
||||||
chains, err := ipt.ListChains(table)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to list iptables chains: %v", err)
|
|
||||||
}
|
|
||||||
for _, ch := range chains {
|
|
||||||
if ch == chain {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ipt.NewChain(table, chain)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateFilterRule(privChainName string) []string {
|
func generateFilterRule(privChainName string) []string {
|
||||||
return []string{"-m", "comment", "--comment", "CNI firewall plugin rules", "-j", privChainName}
|
return []string{"-m", "comment", "--comment", "CNI firewall plugin rules", "-j", privChainName}
|
||||||
}
|
}
|
||||||
@ -73,10 +60,10 @@ func (ib *iptablesBackend) setupChains(ipt *iptables.IPTables) error {
|
|||||||
adminRule := generateFilterRule(ib.adminChainName)
|
adminRule := generateFilterRule(ib.adminChainName)
|
||||||
|
|
||||||
// Ensure our private chains exist
|
// Ensure our private chains exist
|
||||||
if err := ensureChain(ipt, "filter", ib.privChainName); err != nil {
|
if err := utils.EnsureChain(ipt, "filter", ib.privChainName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := ensureChain(ipt, "filter", ib.adminChainName); err != nil {
|
if err := utils.EnsureChain(ipt, "filter", ib.adminChainName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,10 +147,10 @@ func (ib *iptablesBackend) checkRules(conf *FirewallNetConf, result *current.Res
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure our private chains exist
|
// Ensure our private chains exist
|
||||||
if err := ensureChain(ipt, "filter", ib.privChainName); err != nil {
|
if err := utils.EnsureChain(ipt, "filter", ib.privChainName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := ensureChain(ipt, "filter", ib.adminChainName); err != nil {
|
if err := utils.EnsureChain(ipt, "filter", ib.adminChainName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containernetworking/plugins/pkg/utils"
|
||||||
"github.com/coreos/go-iptables/iptables"
|
"github.com/coreos/go-iptables/iptables"
|
||||||
"github.com/mattn/go-shellwords"
|
"github.com/mattn/go-shellwords"
|
||||||
)
|
)
|
||||||
@ -35,16 +36,11 @@ type chain struct {
|
|||||||
|
|
||||||
// setup idempotently creates the chain. It will not error if the chain exists.
|
// setup idempotently creates the chain. It will not error if the chain exists.
|
||||||
func (c *chain) setup(ipt *iptables.IPTables) error {
|
func (c *chain) setup(ipt *iptables.IPTables) error {
|
||||||
// create the chain
|
|
||||||
exists, err := chainExists(ipt, c.table, c.name)
|
err := utils.EnsureChain(ipt, c.table, c.name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !exists {
|
|
||||||
if err := ipt.NewChain(c.table, c.name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the rules to the chain
|
// Add the rules to the chain
|
||||||
for _, rule := range c.rules {
|
for _, rule := range c.rules {
|
||||||
@ -74,7 +70,7 @@ func (c *chain) teardown(ipt *iptables.IPTables) error {
|
|||||||
// flush the chain
|
// flush the chain
|
||||||
// This will succeed *and create the chain* if it does not exist.
|
// This will succeed *and create the chain* if it does not exist.
|
||||||
// If the chain doesn't exist, the next checks will fail.
|
// If the chain doesn't exist, the next checks will fail.
|
||||||
if err := ipt.ClearChain(c.table, c.name); err != nil {
|
if err := utils.ClearChain(ipt, c.table, c.name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,17 +90,15 @@ func (c *chain) teardown(ipt *iptables.IPTables) error {
|
|||||||
}
|
}
|
||||||
chainParts = chainParts[2:] // List results always include an -A CHAINNAME
|
chainParts = chainParts[2:] // List results always include an -A CHAINNAME
|
||||||
|
|
||||||
if err := ipt.Delete(c.table, entryChain, chainParts...); err != nil {
|
if err := utils.DeleteRule(ipt, c.table, entryChain, chainParts...); err != nil {
|
||||||
return fmt.Errorf("Failed to delete referring rule %s %s: %v", c.table, entryChainRule, err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ipt.DeleteChain(c.table, c.name); err != nil {
|
return utils.DeleteChain(ipt, c.table, c.name)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertUnique will add a rule to a chain if it does not already exist.
|
// insertUnique will add a rule to a chain if it does not already exist.
|
||||||
@ -125,24 +119,10 @@ func insertUnique(ipt *iptables.IPTables, table, chain string, prepend bool, rul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the chain.
|
// check the chain.
|
||||||
func (c *chain) check(ipt *iptables.IPTables) error {
|
func (c *chain) check(ipt *iptables.IPTables) error {
|
||||||
|
|
||||||
exists, err := chainExists(ipt, c.table, c.name)
|
exists, err := utils.ChainExists(ipt, c.table, c.name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/containernetworking/plugins/pkg/ns"
|
"github.com/containernetworking/plugins/pkg/ns"
|
||||||
"github.com/containernetworking/plugins/pkg/testutils"
|
"github.com/containernetworking/plugins/pkg/testutils"
|
||||||
@ -32,6 +33,7 @@ const TABLE = "filter" // We'll monkey around here
|
|||||||
var _ = Describe("chain tests", func() {
|
var _ = Describe("chain tests", func() {
|
||||||
var testChain chain
|
var testChain chain
|
||||||
var ipt *iptables.IPTables
|
var ipt *iptables.IPTables
|
||||||
|
var testNs ns.NetNS
|
||||||
var cleanup func()
|
var cleanup func()
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
@ -41,7 +43,7 @@ var _ = Describe("chain tests", func() {
|
|||||||
currNs, err := ns.GetCurrentNS()
|
currNs, err := ns.GetCurrentNS()
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
testNs, err := testutils.NewNS()
|
testNs, err = testutils.NewNS()
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
tlChainName := fmt.Sprintf("cni-test-%d", rand.Intn(10000000))
|
tlChainName := fmt.Sprintf("cni-test-%d", rand.Intn(10000000))
|
||||||
@ -195,4 +197,38 @@ var _ = Describe("chain tests", func() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("deletes chains idempotently in parallel", func() {
|
||||||
|
defer cleanup()
|
||||||
|
// number of parallel executions
|
||||||
|
N := 10
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
err := testChain.setup(ipt)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
errCh := make(chan error, N)
|
||||||
|
for i := 0; i < N; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
// teardown chain
|
||||||
|
errCh <- testNs.Do(func(ns.NetNS) error {
|
||||||
|
return testChain.teardown(ipt)
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
close(errCh)
|
||||||
|
for err := range errCh {
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
}
|
||||||
|
|
||||||
|
chains, err := ipt.ListChains(TABLE)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
for _, chain := range chains {
|
||||||
|
if chain == testChain.name {
|
||||||
|
Fail("Chain was not deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -124,7 +124,7 @@ func checkPorts(config *PortMapConf, containerIP net.IP) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ip4t != nil {
|
if ip4t != nil {
|
||||||
exists, err := chainExists(ip4t, dnatChain.table, dnatChain.name)
|
exists, err := utils.ChainExists(ip4t, dnatChain.table, dnatChain.name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -137,7 +137,7 @@ func checkPorts(config *PortMapConf, containerIP net.IP) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ip6t != nil {
|
if ip6t != nil {
|
||||||
exists, err := chainExists(ip6t, dnatChain.table, dnatChain.name)
|
exists, err := utils.ChainExists(ip6t, dnatChain.table, dnatChain.name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -96,119 +96,133 @@ var _ = Describe("portmap integration tests", func() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// This needs to be done using Ginkgo's asynchronous testing mode.
|
Describe("Creating an interface in a namespace with the ptp plugin", func() {
|
||||||
It("forwards a TCP port on ipv4", func(done Done) {
|
// This needs to be done using Ginkgo's asynchronous testing mode.
|
||||||
var err error
|
It("forwards a TCP port on ipv4", func(done Done) {
|
||||||
hostPort := rand.Intn(10000) + 1025
|
var err error
|
||||||
runtimeConfig := libcni.RuntimeConf{
|
hostPort := rand.Intn(10000) + 1025
|
||||||
ContainerID: fmt.Sprintf("unit-test-%d", hostPort),
|
runtimeConfig := libcni.RuntimeConf{
|
||||||
NetNS: targetNS.Path(),
|
ContainerID: fmt.Sprintf("unit-test-%d", hostPort),
|
||||||
IfName: "eth0",
|
NetNS: targetNS.Path(),
|
||||||
CapabilityArgs: map[string]interface{}{
|
IfName: "eth0",
|
||||||
"portMappings": []map[string]interface{}{
|
CapabilityArgs: map[string]interface{}{
|
||||||
{
|
"portMappings": []map[string]interface{}{
|
||||||
"hostPort": hostPort,
|
{
|
||||||
"containerPort": containerPort,
|
"hostPort": hostPort,
|
||||||
"protocol": "tcp",
|
"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(context.TODO(), configList, &runtimeConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// we'll also manually check the iptables chains
|
// Make delete idempotent, so we can clean up on failure
|
||||||
ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
netDeleted := false
|
||||||
Expect(err).NotTo(HaveOccurred())
|
deleteNetwork := func() error {
|
||||||
dnatChainName := genDnatChain("cni-portmap-unit-test", runtimeConfig.ContainerID).name
|
if netDeleted {
|
||||||
|
return nil
|
||||||
// Create the network
|
}
|
||||||
resI, err := cniConf.AddNetworkList(context.TODO(), configList, &runtimeConfig)
|
netDeleted = true
|
||||||
Expect(err).NotTo(HaveOccurred())
|
return cniConf.DelNetworkList(context.TODO(), configList, &runtimeConfig)
|
||||||
defer deleteNetwork()
|
|
||||||
|
|
||||||
// Undo Docker's forwarding policy
|
|
||||||
cmd := exec.Command("iptables", "-t", "filter",
|
|
||||||
"-P", "FORWARD", "ACCEPT")
|
|
||||||
cmd.Stderr = GinkgoWriter
|
|
||||||
err = cmd.Run()
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
intfIndex := *ip.Interface
|
|
||||||
if result.Interfaces[intfIndex].Sandbox == "" {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
contIP = ip.Address.IP
|
|
||||||
}
|
|
||||||
if contIP == nil {
|
|
||||||
Fail("could not determine container IP")
|
|
||||||
}
|
|
||||||
|
|
||||||
hostIP := getLocalIP()
|
// we'll also manually check the iptables chains
|
||||||
fmt.Fprintf(GinkgoWriter, "hostIP: %s:%d, contIP: %s:%d\n",
|
ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
hostIP, hostPort, contIP, containerPort)
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
dnatChainName := genDnatChain("cni-portmap-unit-test", runtimeConfig.ContainerID).name
|
||||||
|
|
||||||
// dump iptables-save output for debugging
|
// Create the network
|
||||||
cmd = exec.Command("iptables-save")
|
resI, err := cniConf.AddNetworkList(context.TODO(), configList, &runtimeConfig)
|
||||||
cmd.Stderr = GinkgoWriter
|
Expect(err).NotTo(HaveOccurred())
|
||||||
cmd.Stdout = GinkgoWriter
|
defer deleteNetwork()
|
||||||
Expect(cmd.Run()).To(Succeed())
|
|
||||||
|
|
||||||
// Sanity check: verify that the container is reachable directly
|
// Undo Docker's forwarding policy
|
||||||
contOK := testEchoServer(contIP.String(), containerPort, "")
|
cmd := exec.Command("iptables", "-t", "filter",
|
||||||
|
"-P", "FORWARD", "ACCEPT")
|
||||||
|
cmd.Stderr = GinkgoWriter
|
||||||
|
err = cmd.Run()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
// Verify that a connection to the forwarded port works
|
// Check the chain exists
|
||||||
dnatOK := testEchoServer(hostIP, hostPort, "")
|
_, err = ipt.List("nat", dnatChainName)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
// Verify that a connection to localhost works
|
result, err := current.GetResult(resI)
|
||||||
snatOK := testEchoServer("127.0.0.1", hostPort, "")
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
var contIP net.IP
|
||||||
|
|
||||||
// verify that hairpin works
|
for _, ip := range result.IPs {
|
||||||
hairpinOK := testEchoServer(hostIP, hostPort, targetNS.Path())
|
intfIndex := *ip.Interface
|
||||||
|
if result.Interfaces[intfIndex].Sandbox == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contIP = ip.Address.IP
|
||||||
|
}
|
||||||
|
if contIP == nil {
|
||||||
|
Fail("could not determine container IP")
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup
|
hostIP := getLocalIP()
|
||||||
session.Terminate()
|
fmt.Fprintf(GinkgoWriter, "hostIP: %s:%d, contIP: %s:%d\n",
|
||||||
err = deleteNetwork()
|
hostIP, hostPort, contIP, containerPort)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
// Verify iptables rules are gone
|
// dump iptables-save output for debugging
|
||||||
_, err = ipt.List("nat", dnatChainName)
|
cmd = exec.Command("iptables-save")
|
||||||
Expect(err).To(MatchError(ContainSubstring("iptables: No chain/target/match by that name.")))
|
cmd.Stderr = GinkgoWriter
|
||||||
|
cmd.Stdout = GinkgoWriter
|
||||||
|
Expect(cmd.Run()).To(Succeed())
|
||||||
|
|
||||||
// Check that everything succeeded *after* we clean up the network
|
// dump ip routes output for debugging
|
||||||
if !contOK {
|
cmd = exec.Command("ip", "route")
|
||||||
Fail("connection direct to " + contIP.String() + " failed")
|
cmd.Stderr = GinkgoWriter
|
||||||
}
|
cmd.Stdout = GinkgoWriter
|
||||||
if !dnatOK {
|
Expect(cmd.Run()).To(Succeed())
|
||||||
Fail("Connection to " + hostIP + " was not forwarded")
|
|
||||||
}
|
|
||||||
if !snatOK {
|
|
||||||
Fail("connection to 127.0.0.1 was not forwarded")
|
|
||||||
}
|
|
||||||
if !hairpinOK {
|
|
||||||
Fail("Hairpin connection failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
close(done)
|
// dump ip addresses output for debugging
|
||||||
|
cmd = exec.Command("ip", "addr")
|
||||||
|
cmd.Stderr = GinkgoWriter
|
||||||
|
cmd.Stdout = GinkgoWriter
|
||||||
|
Expect(cmd.Run()).To(Succeed())
|
||||||
|
|
||||||
}, TIMEOUT*9)
|
// Sanity check: verify that the container is reachable directly
|
||||||
|
contOK := testEchoServer(contIP.String(), containerPort, "")
|
||||||
|
|
||||||
|
// Verify that a connection to the forwarded port works
|
||||||
|
dnatOK := testEchoServer(hostIP, hostPort, "")
|
||||||
|
|
||||||
|
// Verify that a connection to localhost works
|
||||||
|
snatOK := testEchoServer("127.0.0.1", hostPort, "")
|
||||||
|
|
||||||
|
// verify that hairpin works
|
||||||
|
hairpinOK := testEchoServer(hostIP, hostPort, targetNS.Path())
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
session.Terminate()
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
if !hairpinOK {
|
||||||
|
Fail("Hairpin connection failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
close(done)
|
||||||
|
|
||||||
|
}, TIMEOUT*9)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// testEchoServer returns true if we found an echo server on the port
|
// testEchoServer returns true if we found an echo server on the port
|
||||||
|
@ -21,7 +21,7 @@ $DOCKER run -ti -v ${SRC_DIR}:/go/src/github.com/containernetworking/plugins --r
|
|||||||
apk --no-cache add bash tar;
|
apk --no-cache add bash tar;
|
||||||
cd /go/src/github.com/containernetworking/plugins; umask 0022;
|
cd /go/src/github.com/containernetworking/plugins; umask 0022;
|
||||||
|
|
||||||
for arch in amd64 arm arm64 ppc64le s390x; do \
|
for arch in amd64 arm arm64 ppc64le s390x mips64le; do \
|
||||||
rm -f ${OUTPUT_DIR}/*; \
|
rm -f ${OUTPUT_DIR}/*; \
|
||||||
CGO_ENABLED=0 GOARCH=\$arch ./build_linux.sh ${BUILDFLAGS}; \
|
CGO_ENABLED=0 GOARCH=\$arch ./build_linux.sh ${BUILDFLAGS}; \
|
||||||
for format in tgz; do \
|
for format in tgz; do \
|
||||||
|
29
vendor/github.com/coreos/go-iptables/iptables/iptables.go
generated
vendored
29
vendor/github.com/coreos/go-iptables/iptables/iptables.go
generated
vendored
@ -48,9 +48,13 @@ func (e *Error) Error() string {
|
|||||||
|
|
||||||
// IsNotExist returns true if the error is due to the chain or rule not existing
|
// IsNotExist returns true if the error is due to the chain or rule not existing
|
||||||
func (e *Error) IsNotExist() bool {
|
func (e *Error) IsNotExist() bool {
|
||||||
return e.ExitStatus() == 1 &&
|
if e.ExitStatus() != 1 {
|
||||||
(e.msg == fmt.Sprintf("%s: Bad rule (does a matching rule exist in that chain?).\n", getIptablesCommand(e.proto)) ||
|
return false
|
||||||
e.msg == fmt.Sprintf("%s: No chain/target/match by that name.\n", getIptablesCommand(e.proto)))
|
}
|
||||||
|
cmdIptables := getIptablesCommand(e.proto)
|
||||||
|
msgNoRuleExist := fmt.Sprintf("%s: Bad rule (does a matching rule exist in that chain?).\n", cmdIptables)
|
||||||
|
msgNoChainExist := fmt.Sprintf("%s: No chain/target/match by that name.\n", cmdIptables)
|
||||||
|
return strings.Contains(e.msg, msgNoRuleExist) || strings.Contains(e.msg, msgNoChainExist)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Protocol to differentiate between IPv4 and IPv6
|
// Protocol to differentiate between IPv4 and IPv6
|
||||||
@ -101,7 +105,13 @@ func NewWithProtocol(proto Protocol) (*IPTables, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
vstring, err := getIptablesVersionString(path)
|
vstring, err := getIptablesVersionString(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get iptables version: %v", err)
|
||||||
|
}
|
||||||
v1, v2, v3, mode, err := extractIptablesVersion(vstring)
|
v1, v2, v3, mode, err := extractIptablesVersion(vstring)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract iptables version from [%s]: %v", vstring, err)
|
||||||
|
}
|
||||||
|
|
||||||
checkPresent, waitPresent, randomFullyPresent := getIptablesCommandSupport(v1, v2, v3)
|
checkPresent, waitPresent, randomFullyPresent := getIptablesCommandSupport(v1, v2, v3)
|
||||||
|
|
||||||
@ -348,18 +358,6 @@ func (ipt *IPTables) executeList(args []string) ([]string, error) {
|
|||||||
rules = rules[:len(rules)-1]
|
rules = rules[:len(rules)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
// nftables mode doesn't return an error code when listing a non-existent
|
|
||||||
// chain. Patch that up.
|
|
||||||
if len(rules) == 0 && ipt.mode == "nf_tables" {
|
|
||||||
v := 1
|
|
||||||
return nil, &Error{
|
|
||||||
cmd: exec.Cmd{Args: args},
|
|
||||||
msg: fmt.Sprintf("%s: No chain/target/match by that name.\n", getIptablesCommand(ipt.proto)),
|
|
||||||
proto: ipt.proto,
|
|
||||||
exitStatus: &v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, rule := range rules {
|
for i, rule := range rules {
|
||||||
rules[i] = filterRuleOutput(rule)
|
rules[i] = filterRuleOutput(rule)
|
||||||
}
|
}
|
||||||
@ -437,6 +435,7 @@ func (ipt *IPTables) runWithOutput(args []string, stdout io.Writer) error {
|
|||||||
}
|
}
|
||||||
ul, err := fmu.tryLock()
|
ul, err := fmu.tryLock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
syscall.Close(fmu.fd)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer ul.Unlock()
|
defer ul.Unlock()
|
||||||
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@ -32,7 +32,7 @@ github.com/containernetworking/cni/pkg/skel
|
|||||||
github.com/containernetworking/cni/pkg/version
|
github.com/containernetworking/cni/pkg/version
|
||||||
github.com/containernetworking/cni/pkg/types/020
|
github.com/containernetworking/cni/pkg/types/020
|
||||||
github.com/containernetworking/cni/libcni
|
github.com/containernetworking/cni/libcni
|
||||||
# github.com/coreos/go-iptables v0.4.2
|
# github.com/coreos/go-iptables v0.4.5
|
||||||
github.com/coreos/go-iptables/iptables
|
github.com/coreos/go-iptables/iptables
|
||||||
# github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7
|
# github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7
|
||||||
github.com/coreos/go-systemd/activation
|
github.com/coreos/go-systemd/activation
|
||||||
|
Reference in New Issue
Block a user