Compare commits

...

55 Commits
v1.5.1 ... main

Author SHA1 Message Date
dependabot[bot]
6e7fb60738 build(deps): bump golang.org/x/sys in the golang group
Bumps the golang group with 1 update: [golang.org/x/sys](https://github.com/golang/sys).


Updates `golang.org/x/sys` from 0.28.0 to 0.29.0
- [Commits](https://github.com/golang/sys/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-04 14:56:44 +01:00
Or Mergi
7c122fabb4 bridge: Add option to enable port isolation
Enable bridge CNI plugin setting port-isolation [1] the interface.
When port-isolation is enabled, containers connected to the network
cannot communicate with each other over the linux-bridge.
Communication will be enable depending on the gateway appliance according
to its restrictions / policies.

For example: in a scenario the env connected to smart switch, enabling
port-isolation ensure traffic will go outbound, allowing the
smart-switch routing the traffic according to policies.

Add "portIsolation" flag to bridge plugin.
When true, configure the node interface with port-isolation [1].
Default is false.

[1] https://man7.org/linux/man-pages/man8/bridge.8.html (see "isolated" option)

Signed-off-by: Or Mergi <ormergi@redhat.com>
2025-01-29 16:10:47 +01:00
Casey Callendrello
e4ca66b414 build: split CI and go.mod version
Downstream users would like to lower the minimum required go version,
but it would be nice to test and release with the latest go. So, use a
placeholder go version file for CI.

Signed-off-by: Casey Callendrello <c1@caseyc.net>
2025-01-21 13:36:19 +01:00
Tomofumi Hayashi
abfac4a938
Remove scripts/release.sh because of no longer used (#1137)
scripts/release.sh is used for release plugins manually (by
maintainer's hand), previously. Now we introduced automated release
process by github action, hence it is no longer used and no longer
maintained. This change removes this file. Thanks, release.sh for a
long time!

Signed-off-by: Tomofumi Hayashi <tohayash@redhat.com>
2025-01-15 09:51:48 +09:00
dependabot[bot]
eded0afca8 build(deps): bump the golang group across 1 directory with 3 updates
Bumps the golang group with 1 update in the / directory: [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo).


Updates `github.com/onsi/ginkgo/v2` from 2.22.0 to 2.22.2
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.22.0...v2.22.2)

Updates `github.com/onsi/gomega` from 1.36.0 to 1.36.2
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.36.0...v1.36.2)

Updates `golang.org/x/sys` from 0.27.0 to 0.28.0
- [Commits](https://github.com/golang/sys/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: golang
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: golang
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-14 22:02:20 +01:00
dependabot[bot]
41d548592d build(deps): bump alpine in /.github/actions/retest-action
Bumps alpine from 3.20 to 3.21.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-14 17:50:13 +01:00
Casey Callendrello
e8c7d9b930 test: enable unpriv user namespaces
These are disabled by default in some distros; we would like to test
rootless, however.

Signed-off-by: Casey Callendrello <c1@caseyc.net>
2025-01-14 17:49:22 +01:00
jingyuanliang
ba8bc7d0c7
Bump Go version to 1.23 in scripts/release.sh (#1123)
Signed-off-by: Jingyuan Liang <jingyuanliang@google.com>
2025-01-07 01:39:11 +09:00
Etienne Champetier
7f756b411e portmap: fix iptables conditions detection
As show in the docs, iptables conditions can also start with '!'

Fixes 01a94e17c77e6ff8e5019e15c42d8d92cf87194f

Signed-off-by: Etienne Champetier <e.champetier@ateme.com>
2024-12-02 17:06:11 +01:00
dependabot[bot]
3ffc42cdfd build(deps): bump the golang group across 1 directory with 7 updates
Bumps the golang group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github.com/Microsoft/hcsshim](https://github.com/Microsoft/hcsshim) | `0.12.7` | `0.12.9` |
| [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) | `2.20.2` | `2.22.0` |
| [github.com/onsi/gomega](https://github.com/onsi/gomega) | `1.34.2` | `1.36.0` |
| [github.com/opencontainers/selinux](https://github.com/opencontainers/selinux) | `1.11.0` | `1.11.1` |
| [github.com/safchain/ethtool](https://github.com/safchain/ethtool) | `0.4.1` | `0.5.9` |
| [sigs.k8s.io/knftables](https://github.com/kubernetes-sigs/knftables) | `0.0.17` | `0.0.18` |



Updates `github.com/Microsoft/hcsshim` from 0.12.7 to 0.12.9
- [Release notes](https://github.com/Microsoft/hcsshim/releases)
- [Commits](https://github.com/Microsoft/hcsshim/compare/v0.12.7...v0.12.9)

Updates `github.com/onsi/ginkgo/v2` from 2.20.2 to 2.22.0
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.20.2...v2.22.0)

Updates `github.com/onsi/gomega` from 1.34.2 to 1.36.0
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.34.2...v1.36.0)

Updates `github.com/opencontainers/selinux` from 1.11.0 to 1.11.1
- [Release notes](https://github.com/opencontainers/selinux/releases)
- [Commits](https://github.com/opencontainers/selinux/compare/v1.11.0...v1.11.1)

Updates `github.com/safchain/ethtool` from 0.4.1 to 0.5.9
- [Release notes](https://github.com/safchain/ethtool/releases)
- [Commits](https://github.com/safchain/ethtool/compare/v0.4.1...v0.5.9)

Updates `golang.org/x/sys` from 0.26.0 to 0.27.0
- [Commits](https://github.com/golang/sys/compare/v0.26.0...v0.27.0)

Updates `sigs.k8s.io/knftables` from 0.0.17 to 0.0.18
- [Changelog](https://github.com/kubernetes-sigs/knftables/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes-sigs/knftables/compare/v0.0.17...v0.0.18)

---
updated-dependencies:
- dependency-name: github.com/Microsoft/hcsshim
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: golang
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: github.com/opencontainers/selinux
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: golang
- dependency-name: github.com/safchain/ethtool
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: sigs.k8s.io/knftables
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 17:04:12 +01:00
Etienne Champetier
6de8a9853c ipmasq: fix nftables backend
Rename
SetupIPMasqForNetwork -> SetupIPMasqForNetworks
TeardownIPMasqForNetwork -> TeardownIPMasqForNetworks
and have them take []*net.IPNet instead of *net.IPNet.

This allow the nftables backend to cleanup stale rules and recreate all
needed rules in a single transaction, where previously the stale rules
cleanup was breaking all but the last IPNet.

Fixes 61d078645a6d2a2391a1555ecda3d0a080a45831

Signed-off-by: Etienne Champetier <e.champetier@ateme.com>
2024-11-21 20:23:25 +01:00
Etienne Champetier
9296c5f80a portmap: fix nftables backend
We can't use dnat from the input hook,
depending on nftables (and kernel ?) version we get
"Error: Could not process rule: Operation not supported"
iptables backend also uses prerouting.

Also 'ip6 protocol tcp' is invalid, so rework / simplify the rules

Fixes 01a94e17c77e6ff8e5019e15c42d8d92cf87194f

Signed-off-by: Etienne Champetier <e.champetier@ateme.com>
2024-11-18 17:04:37 +01:00
Lionel Jouin
fec2d62676 Pass status along ipam update
Signed-off-by: Lionel Jouin <lionel.jouin@est.tech>
2024-10-15 10:22:10 +02:00
Songmin Li
a4fc6f93c7 feat(dhcp): Cancel backoff retry on stop
Signed-off-by: Songmin Li <lisongmin@protonmail.com>
2024-10-14 17:42:30 +02:00
Songmin Li
d61e7e5e1f fix(dhcp): can not renew an ip address
The dhcp server is systemd-networkd, and the dhcp
plugin can request an ip but can not renew it.
The systemd-networkd just ignore the renew request.

```
2024/09/14 21:46:00 no DHCP packet received within 10s
2024/09/14 21:46:00 retrying in 31.529038 seconds
2024/09/14 21:46:42 no DHCP packet received within 10s
2024/09/14 21:46:42 retrying in 63.150490 seconds
2024/09/14 21:47:45 98184616c91f15419f5cacd012697f85afaa2daeb5d3233e28b0ec21589fb45a/iot/eth1: no more tries
2024/09/14 21:47:45 98184616c91f15419f5cacd012697f85afaa2daeb5d3233e28b0ec21589fb45a/iot/eth1: renewal time expired, rebinding
2024/09/14 21:47:45 Link "eth1" down. Attempting to set up
2024/09/14 21:47:45 98184616c91f15419f5cacd012697f85afaa2daeb5d3233e28b0ec21589fb45a/iot/eth1: lease rebound, expiration is 2024-09-14 22:47:45.309270751 +0800 CST m=+11730.048516519
```

Follow the https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.6,
following options must not be sent in renew

- Requested IP Address
- Server Identifier

Since the upstream code has been inactive for 6 years,
we should switch to another dhcpv4 library.
The new selected one is https://github.com/insomniacslk/dhcp.

Signed-off-by: Songmin Li <lisongmin@protonmail.com>
2024-10-14 17:42:30 +02:00
dependabot[bot]
e4950728ce build(deps): bump golang.org/x/sys in the golang group
Bumps the golang group with 1 update: [golang.org/x/sys](https://github.com/golang/sys).


Updates `golang.org/x/sys` from 0.25.0 to 0.26.0
- [Commits](https://github.com/golang/sys/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 11:49:45 +02:00
Lionel Jouin
93d197c455 VRF: Wait for the local/host routes to be added
Without waiting for the local/host routes to be added
by the kernel after the IP address is being added to
an interface. The routes requiring the local/host routes
may failed. This caused flaky e2e tests, but could also
happen during the execution of the VRF plugin when the
IPv6 addresses were being re-added to the interface and
when the route were being moved to the VRF table.

Signed-off-by: Lionel Jouin <lionel.jouin@est.tech>
2024-10-14 11:49:25 +02:00
h0nIg
c52e02bccf add problem hint
Signed-off-by: h0nIg <h0nIg@users.noreply.github.com>
2024-10-14 11:47:24 +02:00
h0nIg
24b0bf96af make test working again
Signed-off-by: h0nIg <h0nIg@users.noreply.github.com>
2024-10-14 11:47:24 +02:00
h0nIg
d44bbf28af Revert "Merge pull request #921 from oOraph/dev/exclude_subnets_from_traffic_shapping2"
This reverts commit ef076afac1af0b9a8446f72e3343666567bc04dc, reversing
changes made to 597408952e3e7247fb0deef26a3a935c405aa0cf.

Signed-off-by: h0nIg <h0nIg@users.noreply.github.com>
2024-10-14 11:47:24 +02:00
h0nIg
8ad0361964 resolve merge conflicts
Signed-off-by: h0nIg <h0nIg@users.noreply.github.com>
2024-10-14 11:47:24 +02:00
dependabot[bot]
8324a2e5a4 build(deps): bump the golang group across 1 directory with 2 updates
Bumps the golang group with 1 update in the / directory: [github.com/Microsoft/hcsshim](https://github.com/Microsoft/hcsshim).


Updates `github.com/Microsoft/hcsshim` from 0.12.6 to 0.12.7
- [Release notes](https://github.com/Microsoft/hcsshim/releases)
- [Commits](https://github.com/Microsoft/hcsshim/compare/v0.12.6...v0.12.7)

Updates `golang.org/x/sys` from 0.24.0 to 0.25.0
- [Commits](https://github.com/golang/sys/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: github.com/Microsoft/hcsshim
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: golang
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-07 04:57:38 +02:00
Etienne Champetier
a4b80cc634 host-device: use temp network namespace for rename
Using a temporary name / doing a fast rename causes
some race conditions with udev and NetworkManager:
https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/1599

Signed-off-by: Etienne Champetier <e.champetier@ateme.com>
2024-10-02 10:30:27 +02:00
Gudmundur Bjarni Olafsson
3a49cff1f6 Fix txqueuelen being accidentally set to zero
TxQLen was unintentionally set to 0 due to a struct literal.

Signed-off-by: Gudmundur Bjarni Olafsson <gudmundur.bjarni@gmail.com>
2024-10-02 10:01:11 +02:00
Lionel Jouin
c11ed48733 Ignore link-local routes in SBR tests
The tests were flaky due to a route with the link-local IP being
automatically added after the test run saves the initial state
(routes before SBR plugin is ran). When the SBR plugin is ran,
the new state is compared with the old state. The new state will
then contain the route with the link-local IP (that has been
added after saving the old state), the old state was not
containing it, so the tests were failing

The solution here is to ignore routes with the link-local IP
for the tests.

fixes: #1096

Signed-off-by: Lionel Jouin <lionel.jouin@est.tech>
2024-10-01 00:36:30 +02:00
dependabot[bot]
fa737f82b2 build(deps): bump the golang group with 3 updates
Bumps the golang group with 3 updates: [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo), [github.com/onsi/gomega](https://github.com/onsi/gomega) and [golang.org/x/sys](https://github.com/golang/sys).


Updates `github.com/onsi/ginkgo/v2` from 2.20.1 to 2.20.2
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.20.1...v2.20.2)

Updates `github.com/onsi/gomega` from 1.34.1 to 1.34.2
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.34.1...v1.34.2)

Updates `golang.org/x/sys` from 0.23.0 to 0.24.0
- [Commits](https://github.com/golang/sys/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: golang
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: golang
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-17 13:14:06 +02:00
Casey Callendrello
e5df283ab3
ci, go.mod: bump to go 1.23 (#1094)
* ci, go.mod: bump to go 1.23

Now that go.mod matches our go version, we can stop setting go version
in CI separately.

Signed-off-by: Casey Callendrello <c1@caseyc.net>

* minor: fix lint errors

Bumping golangci-lint to v1.61 introduced some new reasonable checks;
fix the errors they found.

Signed-off-by: Casey Callendrello <c1@caseyc.net>

* ci: bump golangci-lint to v1.61.0

Also, fix some deprecated config directives.

Signed-off-by: Casey Callendrello <c1@caseyc.net>

---------

Signed-off-by: Casey Callendrello <c1@caseyc.net>
2024-09-17 12:28:55 +02:00
Songmin Li
cc8b1bd80c dhcp: Add priority option to dhcp.
Currently, we can not set the metric of routes in dhcp.
It's ok if there is only a network interface.

But if there are multiple network interfaces, and both have a default route,
We need to set the metric of the route to make the traffic
go through the correct network interface.

For host-local and static, we can set the metric with the route.priority option.
But there is no such option for dhcp.

Signed-off-by: Songmin Li <lisongmin@protonmail.com>
2024-09-17 11:47:37 +02:00
Casey Callendrello
03712a572b .github: add check to verify vendor directory
Make sure we don't slip any changes in there accidentally.

Signed-off-by: Casey Callendrello <c1@caseyc.net>
2024-09-16 22:12:54 +02:00
Dan Winship
01a94e17c7 Add nftables backend to portmap
Signed-off-by: Dan Winship <danwinship@redhat.com>
2024-09-16 21:17:49 +02:00
Dan Winship
3d1968c152 Fix portmap unit tests
Use `conditionsV4` and `conditionsV6` values that actually look like
valid iptables conditions.

Signed-off-by: Dan Winship <danwinship@redhat.com>
2024-09-16 21:17:49 +02:00
Dan Winship
a3ccebc6ec Add a backend abstraction to the portmap plugin
Signed-off-by: Dan Winship <danwinship@redhat.com>
2024-09-16 21:17:49 +02:00
Dan Winship
61d078645a Add nftables implementation of ipmasq
Signed-off-by: Dan Winship <danwinship@redhat.com>
2024-09-16 21:17:49 +02:00
Dan Winship
729dd23c40 Vendor nftables library, add utils.SupportsIPTables and utils.SupportsNFTables
Signed-off-by: Dan Winship <danwinship@redhat.com>
2024-09-16 21:17:49 +02:00
Lionel Jouin
a6d6efa5ca Use of Scope for routes in IPAM
Add Scope for routes for cni spec v1.1

Signed-off-by: Lionel Jouin <lionel.jouin@est.tech>
2024-09-16 17:06:21 +02:00
Lionel Jouin
01b3db8e01
SBR: option to pass the table id (#1088)
* Use of Table ID in IPAM

Signed-off-by: Lionel Jouin <lionel.jouin@est.tech>

* SBR: option to pass the table id

Using the option to set the table number in the SBR meta plugin will
create a policy route for each IP added for the interface returned by
the main plugin.
Unlike the default behavior, the routes will not be moved to the table.
The default behavior of the SBR plugin is kept if the table id is not set.

Signed-off-by: Lionel Jouin <lionel.jouin@est.tech>

---------

Signed-off-by: Lionel Jouin <lionel.jouin@est.tech>
2024-09-09 17:07:23 +02:00
Casey Callendrello
20f31e5e88
Merge pull request #1083 from danwinship/update-deps
update deps (go-iptables, cni)
2024-08-29 12:28:03 +02:00
Dan Winship
06ba001d84 Update containernetworking/cni to v1.2.3 for GC
Signed-off-by: Dan Winship <danwinship@redhat.com>
2024-08-28 12:17:48 -04:00
Dan Winship
deb8ef63f4 Update go-iptables
Signed-off-by: Dan Winship <danwinship@redhat.com>
2024-08-28 08:59:45 -04:00
Casey Callendrello
720b1e9811
Merge pull request #1074 from champtar/macvlan-bcqueuelen
macvlan: add bcqueuelen setting
2024-08-27 17:01:05 +02:00
Etienne Champetier
bdb6814fe2 macvlan: add bcqueuelen setting
This setting was introduced in Linux 5.11
d4bff72c84
42f5642a40

Signed-off-by: Etienne Champetier <e.champetier@ateme.com>
2024-08-27 09:21:29 -04:00
Casey Callendrello
3653221fad
Merge pull request #1076 from lisongmin/main
Fix unnecessary retrying when the link is down in dhcp
2024-08-27 10:31:03 +02:00
Casey Callendrello
0d2780f0e7
Merge branch 'main' into main 2024-08-27 10:20:16 +02:00
Casey Callendrello
5def33291f
Merge pull request #1081 from containernetworking/dependabot/go_modules/golang-9c8d4662b4
build(deps): bump the golang group across 1 directory with 4 updates
2024-08-27 10:18:17 +02:00
dependabot[bot]
07bd325095
build(deps): bump the golang group across 1 directory with 4 updates
Bumps the golang group with 2 updates in the / directory: [github.com/Microsoft/hcsshim](https://github.com/Microsoft/hcsshim) and [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo).


Updates `github.com/Microsoft/hcsshim` from 0.12.4 to 0.12.6
- [Release notes](https://github.com/Microsoft/hcsshim/releases)
- [Commits](https://github.com/Microsoft/hcsshim/compare/v0.12.4...v0.12.6)

Updates `github.com/onsi/ginkgo/v2` from 2.19.0 to 2.20.1
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.19.0...v2.20.1)

Updates `github.com/onsi/gomega` from 1.33.1 to 1.34.1
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.33.1...v1.34.1)

Updates `golang.org/x/sys` from 0.21.0 to 0.23.0
- [Commits](https://github.com/golang/sys/compare/v0.21.0...v0.23.0)

---
updated-dependencies:
- dependency-name: github.com/Microsoft/hcsshim
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: golang
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-27 07:43:01 +00:00
Casey Callendrello
7cff5db82a
Merge pull request #1080 from champtar/update-netlink
build: update github.com/vishvananda/netlink to 1.3.0
2024-08-27 09:41:58 +02:00
Etienne Champetier
d924f05e12 build: update github.com/vishvananda/netlink to 1.3.0
This includes a breaking change:
acdc658b86
route.Dst is now a zero IPNet instead of nil

Signed-off-by: Etienne Champetier <e.champetier@ateme.com>
2024-08-26 14:27:30 -04:00
Songmin Li
6269f399a5
Fix unnecessary retrying when the link is down in dhcp.
From the dhcp daemon log, we can see that dhcp will fail to acquire
the lease when the link is down, and success on retry.

```
2024/08/21 21:30:44 macvlan-dhcp/eth1: acquiring lease
2024/08/21 21:30:44 Link "eth1" down. Attempting to set up
2024/08/21 21:30:44 network is down
2024/08/21 21:30:44 retrying in 2.641696 seconds
2024/08/21 21:30:49 macvlan-dhcp/eth1: lease acquired, expiration is 2024-08-22 09:30:49.755367962 +0800 CST m=+43205.712107889
```

After move the code of set up link to the beginning of the function, the
dhcp success on first time.

```
2024/08/21 22:04:02 macvlan-dhcp/eth1: acquiring lease
2024/08/21 22:04:02 Link "eth1" down. Attempting to set up
2024/08/21 22:04:05 macvlan-dhcp/eth1: lease acquired, expiration is 2024-08-22 10:04:05.297887726 +0800 CST m=+43203.081141304
```

Signed-off-by: Songmin Li <lisongmin@protonmail.com>
2024-08-24 19:54:34 +08:00
Mike Zappa
5188dc8a19
Merge pull request #1065 from squeed/bump-go
.github: bump go
2024-08-22 16:11:01 -06:00
Casey Callendrello
675ca92261 test: bump go version
We were using the go.mod version, which we don't change as frequently.
Switch to use the GO_VERSION defined in the workflow file.

Signed-off-by: Casey Callendrello <c1@caseyc.net>
2024-07-15 16:58:48 +02:00
Casey Callendrello
30078e1cfd .github: fix double-triggering CI
We were accidentally running CI twice.

Signed-off-by: Casey Callendrello <c1@caseyc.net>
2024-07-15 16:53:23 +02:00
Casey Callendrello
acf8ddc8e1
Merge pull request #1058 from s1061123/fix/1053b
Change chown to change current dir as well
2024-06-24 17:20:49 +02:00
Tomofumi Hayashi
352c8b7ab5 Change chown to change current dir as well
Signed-off-by: Tomofumi Hayashi <tohayash@redhat.com>
2024-06-24 16:57:53 +02:00
Casey Callendrello
11ad58cf20
Merge pull request #1060 from containernetworking/dependabot/go_modules/golang-6263b2479d
build(deps): bump the golang group with 2 updates
2024-06-24 12:11:17 +02:00
dependabot[bot]
d5f9ad99d7
build(deps): bump the golang group with 2 updates
Bumps the golang group with 2 updates: [github.com/Microsoft/hcsshim](https://github.com/Microsoft/hcsshim) and [github.com/safchain/ethtool](https://github.com/safchain/ethtool).


Updates `github.com/Microsoft/hcsshim` from 0.12.3 to 0.12.4
- [Release notes](https://github.com/Microsoft/hcsshim/releases)
- [Commits](https://github.com/Microsoft/hcsshim/compare/v0.12.3...v0.12.4)

Updates `github.com/safchain/ethtool` from 0.4.0 to 0.4.1
- [Release notes](https://github.com/safchain/ethtool/releases)
- [Commits](https://github.com/safchain/ethtool/compare/v0.4.0...v0.4.1)

---
updated-dependencies:
- dependency-name: github.com/Microsoft/hcsshim
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: golang
- dependency-name: github.com/safchain/ethtool
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-24 02:12:28 +00:00
833 changed files with 53664 additions and 30802 deletions

View File

@ -1,4 +1,4 @@
FROM alpine:3.20
FROM alpine:3.21
RUN apk add --no-cache curl jq

1
.github/go-version vendored Normal file
View File

@ -0,0 +1 @@
1.23

View File

@ -13,13 +13,13 @@ jobs:
matrix:
goarch: [amd64, arm, arm64, mips64le, ppc64le, riscv64, s390x]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.22
- name: Checkout code
uses: actions/checkout@v4
go-version-file: .github/go-version
- name: Build
env:
@ -32,7 +32,7 @@ jobs:
- name: Change plugin file ownership
working-directory: ./bin
run: sudo chown root:root ./*
run: sudo chown -R root:root .
- name: Create dist directory
run: mkdir dist
@ -68,13 +68,13 @@ jobs:
- name: Install dos2unix
run: sudo apt-get install dos2unix
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.21
- name: Checkout code
uses: actions/checkout@v4
go-version-file: .github/go-version
- name: Build
env:
@ -87,7 +87,7 @@ jobs:
- name: Change plugin file ownership
working-directory: ./bin
run: sudo chown root:root ./*
run: sudo chown -R root:root .
- name: Create dist directory
run: mkdir dist

View File

@ -1,10 +1,10 @@
---
name: test
on: ["push", "pull_request"]
on:
pull_request: {}
env:
GO_VERSION: "1.22"
LINUX_ARCHES: "amd64 386 arm arm64 s390x mips64le ppc64le riscv64"
jobs:
@ -16,15 +16,28 @@ jobs:
- name: setup go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
go-version-file: .github/go-version
- uses: ibiqlik/action-yamllint@v3
with:
format: auto
- uses: golangci/golangci-lint-action@v6
with:
version: v1.55.2
version: v1.61.0
args: -v
skip-cache: true
verify-vendor:
name: Verify vendor directory
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
with:
go-version-file: .github/go-version
- name: Check module vendoring
run: |
go mod tidy
go mod vendor
test -z "$(git status --porcelain)" || (echo "please run 'go mod tidy && go mod vendor', and submit your changes"; exit 1)
build:
name: Build all linux architectures
needs: lint
@ -34,7 +47,7 @@ jobs:
- name: setup go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
go-version-file: .github/go-version
- name: Build on all supported architectures
run: |
set -e
@ -54,11 +67,15 @@ jobs:
sudo apt-get install linux-modules-extra-$(uname -r)
- name: Install nftables
run: sudo apt-get install nftables
- name: Install dnsmasq(dhcp server)
run: |
sudo apt-get install dnsmasq
sudo systemctl disable --now dnsmasq
- uses: actions/checkout@v4
- name: setup go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
go-version-file: .github/go-version
- name: Set up Go for root
run: |
sudo ln -sf `which go` `sudo which go` || true
@ -89,6 +106,6 @@ jobs:
- name: setup go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
go-version-file: .github/go-version
- name: test
run: bash ./test_windows.sh

View File

@ -40,6 +40,5 @@ linters-settings:
- prefix(github.com/containernetworking)
run:
skip-dirs:
- vendor
timeout: 5m
modules-download-mode: vendor

56
go.mod
View File

@ -1,48 +1,54 @@
module github.com/containernetworking/plugins
go 1.20
go 1.23
require (
github.com/Microsoft/hcsshim v0.12.3
github.com/Microsoft/hcsshim v0.12.9
github.com/alexflint/go-filemutex v1.3.0
github.com/buger/jsonparser v1.1.1
github.com/containernetworking/cni v1.1.2
github.com/coreos/go-iptables v0.7.0
github.com/containernetworking/cni v1.2.3
github.com/coreos/go-iptables v0.8.0
github.com/coreos/go-systemd/v22 v22.5.0
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c
github.com/d2g/dhcp4client v1.0.0
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5
github.com/godbus/dbus/v5 v5.1.0
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475
github.com/mattn/go-shellwords v1.0.12
github.com/networkplumbing/go-nft v0.4.0
github.com/onsi/ginkgo/v2 v2.19.0
github.com/onsi/gomega v1.33.1
github.com/opencontainers/selinux v1.11.0
github.com/safchain/ethtool v0.4.0
github.com/vishvananda/netlink v1.2.1-beta.2
golang.org/x/sys v0.21.0
github.com/onsi/ginkgo/v2 v2.22.2
github.com/onsi/gomega v1.36.2
github.com/opencontainers/selinux v1.11.1
github.com/safchain/ethtool v0.5.9
github.com/vishvananda/netlink v1.3.0
golang.org/x/sys v0.29.0
sigs.k8s.io/knftables v0.0.18
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/containerd/cgroups/v3 v3.0.2 // indirect
github.com/containerd/errdefs v0.1.0 // indirect
github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/containerd/cgroups/v3 v3.0.3 // indirect
github.com/containerd/errdefs v0.3.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/typeurl/v2 v2.2.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/mdlayher/packet v1.1.2 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/tools v0.21.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/grpc v1.62.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.28.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.67.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

170
go.sum
View File

@ -2,36 +2,29 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.12.3 h1:LS9NXqXhMoqNCplK1ApmVSfB4UnVLRDWRapB6EIlxE0=
github.com/Microsoft/hcsshim v0.12.3/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ=
github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg=
github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y=
github.com/alexflint/go-filemutex v1.3.0 h1:LgE+nTUWnQCyRKbpoceKZsPQbs84LivvgwUymZXdOcM=
github.com/alexflint/go-filemutex v1.3.0/go.mod h1:U0+VA/i30mGBlLCrFPGtTe9y6wGQfNAWPBTekHQ+c8A=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0=
github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE=
github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM=
github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0=
github.com/containernetworking/cni v1.1.2 h1:wtRGZVv7olUHMOqouPpn3cXJWpJgM6+EUl31EQbXALQ=
github.com/containernetworking/cni v1.1.2/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8dMxakj/u+i9ht9vw=
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0=
github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0=
github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4=
github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso=
github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g=
github.com/containernetworking/cni v1.2.3 h1:hhOcjNVUQTnzdRJ6alC5XF+wd9mfGIUaj8FuJbEslXM=
github.com/containernetworking/cni v1.2.3/go.mod h1:DuLgF+aPd3DzcTQTtp/Nvl1Kim23oFKdm2okJzBQA5M=
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c h1:Xo2rK1pzOm0jO6abTPIQwbAmqBIOj132otexc1mmzFc=
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
github.com/d2g/dhcp4client v1.0.0 h1:suYBsYZIkSlUMEz4TAYCczKf62IA2UWC+O8+KtdOhCo=
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5 h1:+CpLbZIeUn94m02LdEKPcgErLJ347NUwxPKs5u8ieiY=
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4 h1:itqmmf1PFpC4n5JW+j4BU7X4MTfVurhYRTjODoPb2Y8=
github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -39,16 +32,15 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
@ -62,70 +54,70 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 h1:hxST5pwMBEOWmxpkX20w9oZG+hXdhKmAIPQ3NGGAxas=
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY=
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/networkplumbing/go-nft v0.4.0 h1:kExVMwXW48DOAukkBwyI16h4uhE5lN9iMvQd52lpTyU=
github.com/networkplumbing/go-nft v0.4.0/go.mod h1:HnnM+tYvlGAsMU7yoYwXEVLLiDW9gdMmb5HoGcwpuQs=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/opencontainers/selinux v1.11.1 h1:nHFvthhM0qY8/m+vfhJylliSshm8G1jJ2jDMcgULaH8=
github.com/opencontainers/selinux v1.11.1/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/safchain/ethtool v0.4.0 h1:vq1i2HCjshJNywOXFZ1BpwIjyeFR/kvNdHiRzqSElDI=
github.com/safchain/ethtool v0.4.0/go.mod h1:XLLnZmy4OCRTkksP/UiMjij96YmIsBfmBQcs7H6tA48=
github.com/safchain/ethtool v0.5.9 h1://6RvaOKFf3nQ0rl5+8zBbE4/72455VC9Jq61pfq67E=
github.com/safchain/ethtool v0.5.9/go.mod h1:w8oSsZeowyRaM7xJJBAbubzzrOkwO8TBgPSEqPP/5mg=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
@ -137,65 +129,60 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -205,15 +192,15 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -223,20 +210,15 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
sigs.k8s.io/knftables v0.0.18 h1:6Duvmu0s/HwGifKrtl6G3AyAPYlWiZqTgS8bkVMiyaE=
sigs.k8s.io/knftables v0.0.18/go.mod h1:f/5ZLKYEUPUhVjUCg6l80ACdL7CIIyeL0DxfgojGRTk=

View File

@ -181,20 +181,20 @@ var _ = Describe("Basic PTP using cnitool", func() {
By(fmt.Sprintf("starting echo server in %s\n\n", contNS2.ShortName()))
basicBridgePort, basicBridgeSession = startEchoServerInNamespace(contNS2)
packetInBytes := 3000
packetInBytes := 20000 // The shaper needs to 'warm'. Send enough to cause it to throttle,
// balanced by run time.
By(fmt.Sprintf("sending tcp traffic to the chained, bridged, traffic shaped container on ip address '%s:%d'\n\n", chainedBridgeIP, chainedBridgeBandwidthPort))
start := time.Now()
makeTCPClientInNS(hostNS.ShortName(), chainedBridgeIP, chainedBridgeBandwidthPort, packetInBytes)
runtimeWithLimit := time.Since(start)
log.Printf("Runtime with qos limit %.2f seconds", runtimeWithLimit.Seconds())
By(fmt.Sprintf("sending tcp traffic to the basic bridged container on ip address '%s:%d'\n\n", basicBridgeIP, basicBridgePort))
start = time.Now()
makeTCPClientInNS(hostNS.ShortName(), basicBridgeIP, basicBridgePort, packetInBytes)
runtimeWithoutLimit := time.Since(start)
log.Printf("Runtime without qos limit %.2f seconds", runtimeWithLimit.Seconds())
log.Printf("Runtime without qos limit %.2f seconds", runtimeWithoutLimit.Seconds())
Expect(runtimeWithLimit).To(BeNumerically(">", runtimeWithoutLimit+1000*time.Millisecond))
})

View File

@ -43,7 +43,7 @@ func TestAnnotate(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if !reflect.DeepEqual(Annotatef(test.existingErr, test.contextMessage), test.expectedErr) {
if !reflect.DeepEqual(Annotate(test.existingErr, test.contextMessage), test.expectedErr) {
t.Errorf("test case %s fails", test.name)
return
}

View File

@ -0,0 +1,180 @@
// 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 ip
import (
"errors"
"fmt"
"net"
"strings"
"github.com/coreos/go-iptables/iptables"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/plugins/pkg/utils"
)
// setupIPMasqIPTables is the iptables-based implementation of SetupIPMasqForNetworks
func setupIPMasqIPTables(ipns []*net.IPNet, network, _, containerID string) error {
// Note: for historical reasons, the iptables implementation ignores ifname.
chain := utils.FormatChainName(network, containerID)
comment := utils.FormatComment(network, containerID)
for _, ip := range ipns {
if err := SetupIPMasq(ip, chain, comment); err != nil {
return err
}
}
return nil
}
// SetupIPMasq installs iptables rules to masquerade traffic
// coming from ip of ipn and going outside of ipn.
// Deprecated: This function only supports iptables. Use SetupIPMasqForNetworks, which
// supports both iptables and nftables.
func SetupIPMasq(ipn *net.IPNet, chain string, comment string) error {
isV6 := ipn.IP.To4() == nil
var ipt *iptables.IPTables
var err error
var multicastNet string
if isV6 {
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
multicastNet = "ff00::/8"
} else {
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv4)
multicastNet = "224.0.0.0/4"
}
if err != nil {
return fmt.Errorf("failed to locate iptables: %v", err)
}
// Create chain if doesn't exist
exists := false
chains, err := ipt.ListChains("nat")
if err != nil {
return fmt.Errorf("failed to list chains: %v", err)
}
for _, ch := range chains {
if ch == chain {
exists = true
break
}
}
if !exists {
if err = ipt.NewChain("nat", chain); err != nil {
return err
}
}
// Packets to this network should not be touched
if err := ipt.AppendUnique("nat", chain, "-d", ipn.String(), "-j", "ACCEPT", "-m", "comment", "--comment", comment); err != nil {
return err
}
// Don't masquerade multicast - pods should be able to talk to other pods
// on the local network via multicast.
if err := ipt.AppendUnique("nat", chain, "!", "-d", multicastNet, "-j", "MASQUERADE", "-m", "comment", "--comment", comment); err != nil {
return err
}
// Packets from the specific IP of this network will hit the chain
return ipt.AppendUnique("nat", "POSTROUTING", "-s", ipn.IP.String(), "-j", chain, "-m", "comment", "--comment", comment)
}
// teardownIPMasqIPTables is the iptables-based implementation of TeardownIPMasqForNetworks
func teardownIPMasqIPTables(ipns []*net.IPNet, network, _, containerID string) error {
// Note: for historical reasons, the iptables implementation ignores ifname.
chain := utils.FormatChainName(network, containerID)
comment := utils.FormatComment(network, containerID)
var errs []string
for _, ipn := range ipns {
err := TeardownIPMasq(ipn, chain, comment)
if err != nil {
errs = append(errs, err.Error())
}
}
if errs == nil {
return nil
}
return errors.New(strings.Join(errs, "\n"))
}
// TeardownIPMasq undoes the effects of SetupIPMasq.
// Deprecated: This function only supports iptables. Use TeardownIPMasqForNetworks, which
// supports both iptables and nftables.
func TeardownIPMasq(ipn *net.IPNet, chain string, comment string) error {
isV6 := ipn.IP.To4() == nil
var ipt *iptables.IPTables
var err error
if isV6 {
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
} else {
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv4)
}
if err != nil {
return fmt.Errorf("failed to locate iptables: %v", err)
}
err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.IP.String(), "-j", chain, "-m", "comment", "--comment", comment)
if err != nil && !isNotExist(err) {
return err
}
// for downward compatibility
err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain, "-m", "comment", "--comment", comment)
if err != nil && !isNotExist(err) {
return err
}
err = ipt.ClearChain("nat", chain)
if err != nil && !isNotExist(err) {
return err
}
err = ipt.DeleteChain("nat", chain)
if err != nil && !isNotExist(err) {
return err
}
return nil
}
// gcIPMasqIPTables is the iptables-based implementation of GCIPMasqForNetwork
func gcIPMasqIPTables(_ string, _ []types.GCAttachment) error {
// FIXME: The iptables implementation does not support GC.
//
// (In theory, it _could_ backward-compatibly support it, by adding a no-op rule
// with a comment indicating the network to each chain it creates, so that it
// could later figure out which chains corresponded to which networks; older
// implementations would ignore the extra rule but would still correctly delete
// the chain on teardown (because they ClearChain() before doing DeleteChain()).
return nil
}
// isNotExist returnst true if the error is from iptables indicating
// that the target does not exist.
func isNotExist(err error) bool {
e, ok := err.(*iptables.Error)
if !ok {
return false
}
return e.IsNotExist()
}

View File

@ -15,111 +15,78 @@
package ip
import (
"errors"
"fmt"
"net"
"strings"
"github.com/coreos/go-iptables/iptables"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/plugins/pkg/utils"
)
// SetupIPMasq installs iptables rules to masquerade traffic
// coming from ip of ipn and going outside of ipn
func SetupIPMasq(ipn *net.IPNet, chain string, comment string) error {
isV6 := ipn.IP.To4() == nil
var ipt *iptables.IPTables
var err error
var multicastNet string
if isV6 {
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
multicastNet = "ff00::/8"
} else {
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv4)
multicastNet = "224.0.0.0/4"
}
if err != nil {
return fmt.Errorf("failed to locate iptables: %v", err)
}
// Create chain if doesn't exist
exists := false
chains, err := ipt.ListChains("nat")
if err != nil {
return fmt.Errorf("failed to list chains: %v", err)
}
for _, ch := range chains {
if ch == chain {
exists = true
break
}
}
if !exists {
if err = ipt.NewChain("nat", chain); err != nil {
return err
// SetupIPMasqForNetworks installs rules to masquerade traffic coming from ips of ipns and
// going outside of ipns, using a chain name based on network, ifname, and containerID. The
// backend can be either "iptables" or "nftables"; if it is nil, then a suitable default
// implementation will be used.
func SetupIPMasqForNetworks(backend *string, ipns []*net.IPNet, network, ifname, containerID string) error {
if backend == nil {
// Prefer iptables, unless only nftables is available
defaultBackend := "iptables"
if !utils.SupportsIPTables() && utils.SupportsNFTables() {
defaultBackend = "nftables"
}
backend = &defaultBackend
}
// Packets to this network should not be touched
if err := ipt.AppendUnique("nat", chain, "-d", ipn.String(), "-j", "ACCEPT", "-m", "comment", "--comment", comment); err != nil {
return err
switch *backend {
case "iptables":
return setupIPMasqIPTables(ipns, network, ifname, containerID)
case "nftables":
return setupIPMasqNFTables(ipns, network, ifname, containerID)
default:
return fmt.Errorf("unknown ipmasq backend %q", *backend)
}
// Don't masquerade multicast - pods should be able to talk to other pods
// on the local network via multicast.
if err := ipt.AppendUnique("nat", chain, "!", "-d", multicastNet, "-j", "MASQUERADE", "-m", "comment", "--comment", comment); err != nil {
return err
}
// Packets from the specific IP of this network will hit the chain
return ipt.AppendUnique("nat", "POSTROUTING", "-s", ipn.IP.String(), "-j", chain, "-m", "comment", "--comment", comment)
}
// TeardownIPMasq undoes the effects of SetupIPMasq
func TeardownIPMasq(ipn *net.IPNet, chain string, comment string) error {
isV6 := ipn.IP.To4() == nil
// TeardownIPMasqForNetworks undoes the effects of SetupIPMasqForNetworks
func TeardownIPMasqForNetworks(ipns []*net.IPNet, network, ifname, containerID string) error {
var errs []string
var ipt *iptables.IPTables
var err error
// Do both the iptables and the nftables cleanup, since the pod may have been
// created with a different version of this plugin or a different configuration.
if isV6 {
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
} else {
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv4)
}
if err != nil {
return fmt.Errorf("failed to locate iptables: %v", err)
err := teardownIPMasqIPTables(ipns, network, ifname, containerID)
if err != nil && utils.SupportsIPTables() {
errs = append(errs, err.Error())
}
err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.IP.String(), "-j", chain, "-m", "comment", "--comment", comment)
if err != nil && !isNotExist(err) {
return err
err = teardownIPMasqNFTables(ipns, network, ifname, containerID)
if err != nil && utils.SupportsNFTables() {
errs = append(errs, err.Error())
}
// for downward compatibility
err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain, "-m", "comment", "--comment", comment)
if err != nil && !isNotExist(err) {
return err
if errs == nil {
return nil
}
err = ipt.ClearChain("nat", chain)
if err != nil && !isNotExist(err) {
return err
}
err = ipt.DeleteChain("nat", chain)
if err != nil && !isNotExist(err) {
return err
}
return nil
return errors.New(strings.Join(errs, "\n"))
}
// isNotExist returnst true if the error is from iptables indicating
// that the target does not exist.
func isNotExist(err error) bool {
e, ok := err.(*iptables.Error)
if !ok {
return false
// GCIPMasqForNetwork garbage collects stale IPMasq entries for network
func GCIPMasqForNetwork(network string, attachments []types.GCAttachment) error {
var errs []string
err := gcIPMasqIPTables(network, attachments)
if err != nil && utils.SupportsIPTables() {
errs = append(errs, err.Error())
}
return e.IsNotExist()
err = gcIPMasqNFTables(network, attachments)
if err != nil && utils.SupportsNFTables() {
errs = append(errs, err.Error())
}
if errs == nil {
return nil
}
return errors.New(strings.Join(errs, "\n"))
}

View File

@ -0,0 +1,231 @@
// Copyright 2023 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 ip
import (
"context"
"fmt"
"net"
"strings"
"sigs.k8s.io/knftables"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/plugins/pkg/utils"
)
const (
ipMasqTableName = "cni_plugins_masquerade"
ipMasqChainName = "masq_checks"
)
// The nftables ipmasq implementation is mostly like the iptables implementation, with
// minor updates to fix a bug (adding `ifname`) and to allow future GC support.
//
// We add a rule for each mapping, with a comment containing a hash of its identifiers,
// so that we can later reliably delete the rules we want. (This is important because in
// edge cases, it's possible the plugin might see "ADD container A with IP 192.168.1.3",
// followed by "ADD container B with IP 192.168.1.3" followed by "DEL container A with IP
// 192.168.1.3", and we need to make sure that the DEL causes us to delete the rule for
// container A, and not the rule for container B.)
//
// It would be more nftables-y to have a chain with a single rule doing a lookup against a
// set with an element per mapping, rather than having a chain with a rule per mapping.
// But there's no easy, non-racy way to say "delete the element 192.168.1.3 from the set,
// but only if it was added for container A, not if it was added for container B".
// hashForNetwork returns a unique hash for this network
func hashForNetwork(network string) string {
return utils.MustFormatHashWithPrefix(16, "", network)
}
// hashForInstance returns a unique hash identifying the rules for this
// network/ifname/containerID
func hashForInstance(network, ifname, containerID string) string {
return hashForNetwork(network) + "-" + utils.MustFormatHashWithPrefix(16, "", ifname+":"+containerID)
}
// commentForInstance returns a comment string that begins with a unique hash and
// ends with a (possibly-truncated) human-readable description.
func commentForInstance(network, ifname, containerID string) string {
comment := fmt.Sprintf("%s, net: %s, if: %s, id: %s",
hashForInstance(network, ifname, containerID),
strings.ReplaceAll(network, `"`, ``),
strings.ReplaceAll(ifname, `"`, ``),
strings.ReplaceAll(containerID, `"`, ``),
)
if len(comment) > knftables.CommentLengthMax {
comment = comment[:knftables.CommentLengthMax]
}
return comment
}
// setupIPMasqNFTables is the nftables-based implementation of SetupIPMasqForNetworks
func setupIPMasqNFTables(ipns []*net.IPNet, network, ifname, containerID string) error {
nft, err := knftables.New(knftables.InetFamily, ipMasqTableName)
if err != nil {
return err
}
return setupIPMasqNFTablesWithInterface(nft, ipns, network, ifname, containerID)
}
func setupIPMasqNFTablesWithInterface(nft knftables.Interface, ipns []*net.IPNet, network, ifname, containerID string) error {
staleRules, err := findRules(nft, hashForInstance(network, ifname, containerID))
if err != nil {
return err
}
tx := nft.NewTransaction()
// Ensure that our table and chains exist.
tx.Add(&knftables.Table{
Comment: knftables.PtrTo("Masquerading for plugins from github.com/containernetworking/plugins"),
})
tx.Add(&knftables.Chain{
Name: ipMasqChainName,
Comment: knftables.PtrTo("Masquerade traffic from certain IPs to any (non-multicast) IP outside their subnet"),
})
// Ensure that the postrouting chain exists and has the correct rules. (Has to be
// done after creating ipMasqChainName, so we can jump to it.)
tx.Add(&knftables.Chain{
Name: "postrouting",
Type: knftables.PtrTo(knftables.NATType),
Hook: knftables.PtrTo(knftables.PostroutingHook),
Priority: knftables.PtrTo(knftables.SNATPriority),
})
tx.Flush(&knftables.Chain{
Name: "postrouting",
})
tx.Add(&knftables.Rule{
Chain: "postrouting",
Rule: "ip daddr == 224.0.0.0/4 return",
})
tx.Add(&knftables.Rule{
Chain: "postrouting",
Rule: "ip6 daddr == ff00::/8 return",
})
tx.Add(&knftables.Rule{
Chain: "postrouting",
Rule: knftables.Concat(
"goto", ipMasqChainName,
),
})
// Delete stale rules, add new rules to masquerade chain
for _, rule := range staleRules {
tx.Delete(rule)
}
for _, ipn := range ipns {
ip := "ip"
if ipn.IP.To4() == nil {
ip = "ip6"
}
// e.g. if ipn is "192.168.1.4/24", then dstNet is "192.168.1.0/24"
dstNet := &net.IPNet{IP: ipn.IP.Mask(ipn.Mask), Mask: ipn.Mask}
tx.Add(&knftables.Rule{
Chain: ipMasqChainName,
Rule: knftables.Concat(
ip, "saddr", "==", ipn.IP,
ip, "daddr", "!=", dstNet,
"masquerade",
),
Comment: knftables.PtrTo(commentForInstance(network, ifname, containerID)),
})
}
return nft.Run(context.TODO(), tx)
}
// teardownIPMasqNFTables is the nftables-based implementation of TeardownIPMasqForNetworks
func teardownIPMasqNFTables(ipns []*net.IPNet, network, ifname, containerID string) error {
nft, err := knftables.New(knftables.InetFamily, ipMasqTableName)
if err != nil {
return err
}
return teardownIPMasqNFTablesWithInterface(nft, ipns, network, ifname, containerID)
}
func teardownIPMasqNFTablesWithInterface(nft knftables.Interface, _ []*net.IPNet, network, ifname, containerID string) error {
rules, err := findRules(nft, hashForInstance(network, ifname, containerID))
if err != nil {
return err
} else if len(rules) == 0 {
return nil
}
tx := nft.NewTransaction()
for _, rule := range rules {
tx.Delete(rule)
}
return nft.Run(context.TODO(), tx)
}
// gcIPMasqNFTables is the nftables-based implementation of GCIPMasqForNetwork
func gcIPMasqNFTables(network string, attachments []types.GCAttachment) error {
nft, err := knftables.New(knftables.InetFamily, ipMasqTableName)
if err != nil {
return err
}
return gcIPMasqNFTablesWithInterface(nft, network, attachments)
}
func gcIPMasqNFTablesWithInterface(nft knftables.Interface, network string, attachments []types.GCAttachment) error {
// Find all rules for the network
rules, err := findRules(nft, hashForNetwork(network))
if err != nil {
return err
} else if len(rules) == 0 {
return nil
}
// Compute the comments for all elements of attachments
validAttachments := map[string]bool{}
for _, attachment := range attachments {
validAttachments[commentForInstance(network, attachment.IfName, attachment.ContainerID)] = true
}
// Delete anything in rules that isn't in validAttachments
tx := nft.NewTransaction()
for _, rule := range rules {
if !validAttachments[*rule.Comment] {
tx.Delete(rule)
}
}
return nft.Run(context.TODO(), tx)
}
// findRules finds rules with comments that start with commentPrefix.
func findRules(nft knftables.Interface, commentPrefix string) ([]*knftables.Rule, error) {
rules, err := nft.ListRules(context.TODO(), ipMasqChainName)
if err != nil {
if knftables.IsNotFound(err) {
// If ipMasqChainName doesn't exist yet, that's fine
return nil, nil
}
return nil, err
}
matchingRules := make([]*knftables.Rule, 0, 1)
for _, rule := range rules {
if rule.Comment != nil && strings.HasPrefix(*rule.Comment, commentPrefix) {
matchingRules = append(matchingRules, rule)
}
}
return matchingRules, nil
}

View File

@ -0,0 +1,213 @@
// Copyright 2023 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 ip
import (
"net"
"strings"
"testing"
"github.com/vishvananda/netlink"
"sigs.k8s.io/knftables"
"github.com/containernetworking/cni/pkg/types"
)
func Test_setupIPMasqNFTables(t *testing.T) {
nft := knftables.NewFake(knftables.InetFamily, ipMasqTableName)
containers := []struct {
network string
ifname string
containerID string
addrs []string
}{
{
network: "unit-test",
ifname: "eth0",
containerID: "one",
addrs: []string{"192.168.1.1/24"},
},
{
network: "unit-test",
ifname: "eth0",
containerID: "two",
addrs: []string{"192.168.1.2/24", "2001:db8::2/64"},
},
{
network: "unit-test",
ifname: "eth0",
containerID: "three",
addrs: []string{"192.168.99.5/24"},
},
{
network: "alternate",
ifname: "net1",
containerID: "three",
addrs: []string{
"10.0.0.5/24",
"10.0.0.6/24",
"10.0.1.7/24",
"2001:db8::5/64",
"2001:db8::6/64",
"2001:db8:1::7/64",
},
},
}
for _, c := range containers {
ipns := []*net.IPNet{}
for _, addr := range c.addrs {
nladdr, err := netlink.ParseAddr(addr)
if err != nil {
t.Fatalf("failed to parse test addr: %v", err)
}
ipns = append(ipns, nladdr.IPNet)
}
err := setupIPMasqNFTablesWithInterface(nft, ipns, c.network, c.ifname, c.containerID)
if err != nil {
t.Fatalf("error from setupIPMasqNFTables: %v", err)
}
}
expected := strings.TrimSpace(`
add table inet cni_plugins_masquerade { comment "Masquerading for plugins from github.com/containernetworking/plugins" ; }
add chain inet cni_plugins_masquerade masq_checks { comment "Masquerade traffic from certain IPs to any (non-multicast) IP outside their subnet" ; }
add chain inet cni_plugins_masquerade postrouting { type nat hook postrouting priority 100 ; }
add rule inet cni_plugins_masquerade masq_checks ip saddr == 192.168.1.1 ip daddr != 192.168.1.0/24 masquerade comment "6fd94d501e58f0aa-287fc69eff0574a2, net: unit-test, if: eth0, id: one"
add rule inet cni_plugins_masquerade masq_checks ip saddr == 192.168.1.2 ip daddr != 192.168.1.0/24 masquerade comment "6fd94d501e58f0aa-d750b2c8f0f25d5f, net: unit-test, if: eth0, id: two"
add rule inet cni_plugins_masquerade masq_checks ip6 saddr == 2001:db8::2 ip6 daddr != 2001:db8::/64 masquerade comment "6fd94d501e58f0aa-d750b2c8f0f25d5f, net: unit-test, if: eth0, id: two"
add rule inet cni_plugins_masquerade masq_checks ip saddr == 192.168.99.5 ip daddr != 192.168.99.0/24 masquerade comment "6fd94d501e58f0aa-a4d4adb82b669cfe, net: unit-test, if: eth0, id: three"
add rule inet cni_plugins_masquerade masq_checks ip saddr == 10.0.0.5 ip daddr != 10.0.0.0/24 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip saddr == 10.0.0.6 ip daddr != 10.0.0.0/24 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip saddr == 10.0.1.7 ip daddr != 10.0.1.0/24 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip6 saddr == 2001:db8::5 ip6 daddr != 2001:db8::/64 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip6 saddr == 2001:db8::6 ip6 daddr != 2001:db8::/64 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip6 saddr == 2001:db8:1::7 ip6 daddr != 2001:db8:1::/64 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade postrouting ip daddr == 224.0.0.0/4 return
add rule inet cni_plugins_masquerade postrouting ip6 daddr == ff00::/8 return
add rule inet cni_plugins_masquerade postrouting goto masq_checks
`)
dump := strings.TrimSpace(nft.Dump())
if dump != expected {
t.Errorf("expected nftables state:\n%s\n\nactual:\n%s\n\n", expected, dump)
}
// Add a new container reusing "one"'s address, before deleting "one"
c := containers[0]
addr, err := netlink.ParseAddr(c.addrs[0])
if err != nil {
t.Fatalf("failed to parse test addr: %v", err)
}
err = setupIPMasqNFTablesWithInterface(nft, []*net.IPNet{addr.IPNet}, "unit-test", "eth0", "four")
if err != nil {
t.Fatalf("error from setupIPMasqNFTables: %v", err)
}
// Remove "one"
err = teardownIPMasqNFTablesWithInterface(nft, []*net.IPNet{addr.IPNet}, c.network, c.ifname, c.containerID)
if err != nil {
t.Fatalf("error from teardownIPMasqNFTables: %v", err)
}
// Check that "one" was deleted (and "four" wasn't)
expected = strings.TrimSpace(`
add table inet cni_plugins_masquerade { comment "Masquerading for plugins from github.com/containernetworking/plugins" ; }
add chain inet cni_plugins_masquerade masq_checks { comment "Masquerade traffic from certain IPs to any (non-multicast) IP outside their subnet" ; }
add chain inet cni_plugins_masquerade postrouting { type nat hook postrouting priority 100 ; }
add rule inet cni_plugins_masquerade masq_checks ip saddr == 192.168.1.2 ip daddr != 192.168.1.0/24 masquerade comment "6fd94d501e58f0aa-d750b2c8f0f25d5f, net: unit-test, if: eth0, id: two"
add rule inet cni_plugins_masquerade masq_checks ip6 saddr == 2001:db8::2 ip6 daddr != 2001:db8::/64 masquerade comment "6fd94d501e58f0aa-d750b2c8f0f25d5f, net: unit-test, if: eth0, id: two"
add rule inet cni_plugins_masquerade masq_checks ip saddr == 192.168.99.5 ip daddr != 192.168.99.0/24 masquerade comment "6fd94d501e58f0aa-a4d4adb82b669cfe, net: unit-test, if: eth0, id: three"
add rule inet cni_plugins_masquerade masq_checks ip saddr == 10.0.0.5 ip daddr != 10.0.0.0/24 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip saddr == 10.0.0.6 ip daddr != 10.0.0.0/24 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip saddr == 10.0.1.7 ip daddr != 10.0.1.0/24 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip6 saddr == 2001:db8::5 ip6 daddr != 2001:db8::/64 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip6 saddr == 2001:db8::6 ip6 daddr != 2001:db8::/64 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip6 saddr == 2001:db8:1::7 ip6 daddr != 2001:db8:1::/64 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip saddr == 192.168.1.1 ip daddr != 192.168.1.0/24 masquerade comment "6fd94d501e58f0aa-e766de567ef6c543, net: unit-test, if: eth0, id: four"
add rule inet cni_plugins_masquerade postrouting ip daddr == 224.0.0.0/4 return
add rule inet cni_plugins_masquerade postrouting ip6 daddr == ff00::/8 return
add rule inet cni_plugins_masquerade postrouting goto masq_checks
`)
dump = strings.TrimSpace(nft.Dump())
if dump != expected {
t.Errorf("expected nftables state:\n%s\n\nactual:\n%s\n\n", expected, dump)
}
// GC "four" from the "unit-test" network
err = gcIPMasqNFTablesWithInterface(nft, "unit-test", []types.GCAttachment{
{IfName: "eth0", ContainerID: "two"},
{IfName: "eth0", ContainerID: "three"},
// (irrelevant extra element)
{IfName: "eth0", ContainerID: "one"},
})
if err != nil {
t.Fatalf("error from gcIPMasqNFTables: %v", err)
}
// GC the "alternate" network without removing anything
err = gcIPMasqNFTablesWithInterface(nft, "alternate", []types.GCAttachment{
{IfName: "net1", ContainerID: "three"},
})
if err != nil {
t.Fatalf("error from gcIPMasqNFTables: %v", err)
}
// Re-dump
expected = strings.TrimSpace(`
add table inet cni_plugins_masquerade { comment "Masquerading for plugins from github.com/containernetworking/plugins" ; }
add chain inet cni_plugins_masquerade masq_checks { comment "Masquerade traffic from certain IPs to any (non-multicast) IP outside their subnet" ; }
add chain inet cni_plugins_masquerade postrouting { type nat hook postrouting priority 100 ; }
add rule inet cni_plugins_masquerade masq_checks ip saddr == 192.168.1.2 ip daddr != 192.168.1.0/24 masquerade comment "6fd94d501e58f0aa-d750b2c8f0f25d5f, net: unit-test, if: eth0, id: two"
add rule inet cni_plugins_masquerade masq_checks ip6 saddr == 2001:db8::2 ip6 daddr != 2001:db8::/64 masquerade comment "6fd94d501e58f0aa-d750b2c8f0f25d5f, net: unit-test, if: eth0, id: two"
add rule inet cni_plugins_masquerade masq_checks ip saddr == 192.168.99.5 ip daddr != 192.168.99.0/24 masquerade comment "6fd94d501e58f0aa-a4d4adb82b669cfe, net: unit-test, if: eth0, id: three"
add rule inet cni_plugins_masquerade masq_checks ip saddr == 10.0.0.5 ip daddr != 10.0.0.0/24 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip saddr == 10.0.0.6 ip daddr != 10.0.0.0/24 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip saddr == 10.0.1.7 ip daddr != 10.0.1.0/24 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip6 saddr == 2001:db8::5 ip6 daddr != 2001:db8::/64 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip6 saddr == 2001:db8::6 ip6 daddr != 2001:db8::/64 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade masq_checks ip6 saddr == 2001:db8:1::7 ip6 daddr != 2001:db8:1::/64 masquerade comment "82783ef24bdc7036-acb19d111858e348, net: alternate, if: net1, id: three"
add rule inet cni_plugins_masquerade postrouting ip daddr == 224.0.0.0/4 return
add rule inet cni_plugins_masquerade postrouting ip6 daddr == ff00::/8 return
add rule inet cni_plugins_masquerade postrouting goto masq_checks
`)
dump = strings.TrimSpace(nft.Dump())
if dump != expected {
t.Errorf("expected nftables state:\n%s\n\nactual:\n%s\n\n", expected, dump)
}
// GC everything
err = gcIPMasqNFTablesWithInterface(nft, "unit-test", []types.GCAttachment{})
if err != nil {
t.Fatalf("error from gcIPMasqNFTables: %v", err)
}
err = gcIPMasqNFTablesWithInterface(nft, "alternate", []types.GCAttachment{})
if err != nil {
t.Fatalf("error from gcIPMasqNFTables: %v", err)
}
expected = strings.TrimSpace(`
add table inet cni_plugins_masquerade { comment "Masquerading for plugins from github.com/containernetworking/plugins" ; }
add chain inet cni_plugins_masquerade masq_checks { comment "Masquerade traffic from certain IPs to any (non-multicast) IP outside their subnet" ; }
add chain inet cni_plugins_masquerade postrouting { type nat hook postrouting priority 100 ; }
add rule inet cni_plugins_masquerade postrouting ip daddr == 224.0.0.0/4 return
add rule inet cni_plugins_masquerade postrouting ip6 daddr == ff00::/8 return
add rule inet cni_plugins_masquerade postrouting goto masq_checks
`)
dump = strings.TrimSpace(nft.Dump())
if dump != expected {
t.Errorf("expected nftables state:\n%s\n\nactual:\n%s\n\n", expected, dump)
}
}

View File

@ -32,11 +32,12 @@ var ErrLinkNotFound = errors.New("link not found")
// makeVethPair is called from within the container's network namespace
func makeVethPair(name, peer string, mtu int, mac string, hostNS ns.NetNS) (netlink.Link, error) {
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = name
linkAttrs.MTU = mtu
veth := &netlink.Veth{
LinkAttrs: netlink.LinkAttrs{
Name: name,
MTU: mtu,
},
LinkAttrs: linkAttrs,
PeerName: peer,
PeerNamespace: netlink.NsFd(int(hostNS.Fd())),
}

View File

@ -50,3 +50,16 @@ func AddDefaultRoute(gw net.IP, dev netlink.Link) error {
}
return AddRoute(defNet, gw, dev)
}
// IsIPNetZero check if the IPNet is "0.0.0.0/0" or "::/0"
// This is needed as go-netlink replaces nil Dst with a '0' IPNet since
// https://github.com/vishvananda/netlink/commit/acdc658b8613655ddb69f978e9fb4cf413e2b830
func IsIPNetZero(ipnet *net.IPNet) bool {
if ipnet == nil {
return true
}
if ones, _ := ipnet.Mask.Size(); ones != 0 {
return false
}
return ipnet.IP.Equal(net.IPv4zero) || ipnet.IP.Equal(net.IPv6zero)
}

View File

@ -32,3 +32,7 @@ func ExecCheck(plugin string, netconf []byte) error {
func ExecDel(plugin string, netconf []byte) error {
return invoke.DelegateDel(context.TODO(), plugin, netconf, nil)
}
func ExecStatus(plugin string, netconf []byte) error {
return invoke.DelegateStatus(context.TODO(), plugin, netconf, nil)
}

View File

@ -117,10 +117,27 @@ func ConfigureIface(ifName string, res *current.Result) error {
Dst: &r.Dst,
LinkIndex: link.Attrs().Index,
Gw: gw,
Priority: r.Priority,
}
if r.Table != nil {
route.Table = *r.Table
}
if r.Scope != nil {
route.Scope = netlink.Scope(*r.Scope)
}
if r.Table != nil {
route.Table = *r.Table
}
if r.Scope != nil {
route.Scope = netlink.Scope(*r.Scope)
}
if err = netlink.RouteAddEcmp(&route); err != nil {
return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err)
return fmt.Errorf("failed to add route '%v via %v dev %v metric %d (Scope: %v, Table: %d)': %v", r.Dst, gw, ifName, r.Priority, route.Scope, route.Table, err)
}
}

View File

@ -41,9 +41,11 @@ func ipNetEqual(a, b *net.IPNet) bool {
var _ = Describe("ConfigureIface", func() {
var originalNS ns.NetNS
var ipv4, ipv6, routev4, routev6 *net.IPNet
var ipv4, ipv6, routev4, routev6, routev4Scope *net.IPNet
var ipgw4, ipgw6, routegwv4, routegwv6 net.IP
var routeScope int
var result *current.Result
var routeTable int
BeforeEach(func() {
// Create a new NetNS so we don't modify the host
@ -54,11 +56,12 @@ var _ = Describe("ConfigureIface", func() {
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = LINK_NAME
// Add master
err = netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: LINK_NAME,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
_, err = netlink.LinkByName(LINK_NAME)
@ -77,6 +80,10 @@ var _ = Describe("ConfigureIface", func() {
routegwv4 = net.ParseIP("1.2.3.5")
Expect(routegwv4).NotTo(BeNil())
_, routev4Scope, err = net.ParseCIDR("1.2.3.4/32")
Expect(err).NotTo(HaveOccurred())
Expect(routev4Scope).NotTo(BeNil())
ipgw4 = net.ParseIP("1.2.3.1")
Expect(ipgw4).NotTo(BeNil())
@ -93,6 +100,9 @@ var _ = Describe("ConfigureIface", func() {
ipgw6 = net.ParseIP("abcd:1234:ffff::1")
Expect(ipgw6).NotTo(BeNil())
routeTable := 5000
routeScope = 200
result = &current.Result{
Interfaces: []*current.Interface{
{
@ -121,6 +131,8 @@ var _ = Describe("ConfigureIface", func() {
Routes: []*types.Route{
{Dst: *routev4, GW: routegwv4},
{Dst: *routev6, GW: routegwv6},
{Dst: *routev4, GW: routegwv4, Table: &routeTable},
{Dst: *routev4Scope, Scope: &routeScope},
},
}
})
@ -162,7 +174,7 @@ var _ = Describe("ConfigureIface", func() {
routes, err := netlink.RouteList(link, 0)
Expect(err).NotTo(HaveOccurred())
var v4found, v6found bool
var v4found, v6found, v4Scopefound bool
for _, route := range routes {
isv4 := route.Dst.IP.To4() != nil
if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(routegwv4) {
@ -171,13 +183,17 @@ var _ = Describe("ConfigureIface", func() {
if !isv4 && ipNetEqual(route.Dst, routev6) && route.Gw.Equal(routegwv6) {
v6found = true
}
if isv4 && ipNetEqual(route.Dst, routev4Scope) && int(route.Scope) == routeScope {
v4Scopefound = true
}
if v4found && v6found {
if v4found && v6found && v4Scopefound {
break
}
}
Expect(v4found).To(BeTrue())
Expect(v6found).To(BeTrue())
Expect(v4Scopefound).To(BeTrue())
return nil
})
@ -201,7 +217,7 @@ var _ = Describe("ConfigureIface", func() {
routes, err := netlink.RouteList(link, 0)
Expect(err).NotTo(HaveOccurred())
var v4found, v6found bool
var v4found, v6found, v4Tablefound bool
for _, route := range routes {
isv4 := route.Dst.IP.To4() != nil
if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(ipgw4) {
@ -218,6 +234,29 @@ var _ = Describe("ConfigureIface", func() {
Expect(v4found).To(BeTrue())
Expect(v6found).To(BeTrue())
// Need to read all tables, so cannot use RouteList
routeFilter := &netlink.Route{
Table: routeTable,
}
routes, err = netlink.RouteListFiltered(netlink.FAMILY_ALL,
routeFilter,
netlink.RT_FILTER_TABLE)
Expect(err).NotTo(HaveOccurred())
for _, route := range routes {
isv4 := route.Dst.IP.To4() != nil
if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(ipgw4) {
v4Tablefound = true
}
if v4Tablefound {
break
}
}
Expect(v4Tablefound).To(BeTrue())
return nil
})
Expect(err).NotTo(HaveOccurred())

View File

@ -15,6 +15,7 @@
package link_test
import (
"errors"
"fmt"
"github.com/networkplumbing/go-nft/nft"
@ -301,10 +302,10 @@ type configurerStub struct {
func (a *configurerStub) Apply(c *nft.Config) (*nft.Config, error) {
a.applyCounter++
if a.failFirstApplyConfig && a.applyCounter == 1 {
return nil, fmt.Errorf(errorFirstApplyText)
return nil, errors.New(errorFirstApplyText)
}
if a.failSecondApplyConfig && a.applyCounter == 2 {
return nil, fmt.Errorf(errorSecondApplyText)
return nil, errors.New(errorSecondApplyText)
}
a.applyConfig = append(a.applyConfig, c)
if a.applyReturnNil {
@ -316,7 +317,7 @@ func (a *configurerStub) Apply(c *nft.Config) (*nft.Config, error) {
func (a *configurerStub) Read(_ ...string) (*nft.Config, error) {
a.readCalled = true
if a.failReadConfig {
return nil, fmt.Errorf(errorReadText)
return nil, errors.New(errorReadText)
}
return a.readConfig, nil
}

View File

@ -13,10 +13,10 @@ The `ns.Do()` method provides **partial** control over network namespaces for yo
```go
err = targetNs.Do(func(hostNs ns.NetNS) error {
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = "dummy0"
dummy := &netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: "dummy0",
},
LinkAttrs: linkAttrs,
}
return netlink.LinkAdd(dummy)
})

View File

@ -31,6 +31,10 @@ func GetCurrentNS() (NetNS, error) {
// return an unexpected network namespace.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
return getCurrentNSNoLock()
}
func getCurrentNSNoLock() (NetNS, error) {
return GetNS(getCurrentThreadNetNSPath())
}
@ -152,6 +156,54 @@ func GetNS(nspath string) (NetNS, error) {
return &netNS{file: fd}, nil
}
// Returns a new empty NetNS.
// Calling Close() let the kernel garbage collect the network namespace.
func TempNetNS() (NetNS, error) {
var tempNS NetNS
var err error
var wg sync.WaitGroup
wg.Add(1)
// Create the new namespace in a new goroutine so that if we later fail
// to switch the namespace back to the original one, we can safely
// leave the thread locked to die without a risk of the current thread
// left lingering with incorrect namespace.
go func() {
defer wg.Done()
runtime.LockOSThread()
var threadNS NetNS
// save a handle to current network namespace
threadNS, err = getCurrentNSNoLock()
if err != nil {
err = fmt.Errorf("failed to open current namespace: %v", err)
return
}
defer threadNS.Close()
// create the temporary network namespace
err = unix.Unshare(unix.CLONE_NEWNET)
if err != nil {
return
}
// get a handle to the temporary network namespace
tempNS, err = getCurrentNSNoLock()
err2 := threadNS.Set()
if err2 == nil {
// Unlock the current thread only when we successfully switched back
// to the original namespace; otherwise leave the thread locked which
// will force the runtime to scrap the current thread, that is maybe
// not as optimal but at least always safe to do.
runtime.UnlockOSThread()
}
}()
wg.Wait()
return tempNS, err
}
func (ns *netNS) Path() string {
return ns.file.Name()
}
@ -173,7 +225,7 @@ func (ns *netNS) Do(toRun func(NetNS) error) error {
}
containedCall := func(hostNS NetNS) error {
threadNS, err := GetCurrentNS()
threadNS, err := getCurrentNSNoLock()
if err != nil {
return fmt.Errorf("failed to open current netns: %v", err)
}

View File

@ -114,3 +114,12 @@ func CmdDel(cniNetns, cniContainerID, cniIfname string, f func() error) error {
func CmdDelWithArgs(args *skel.CmdArgs, f func() error) error {
return CmdDel(args.Netns, args.ContainerID, args.IfName, f)
}
func CmdStatus(f func() error) error {
os.Setenv("CNI_COMMAND", "STATUS")
os.Setenv("CNI_PATH", os.Getenv("PATH"))
os.Setenv("CNI_NETNS_OVERRIDE", "1")
defer envCleanup()
return f()
}

View File

@ -19,7 +19,7 @@ import (
)
// AllSpecVersions contains all CNI spec version numbers
var AllSpecVersions = [...]string{"0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0"}
var AllSpecVersions = [...]string{"0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0", "1.1.0"}
// SpecVersionHasIPVersion returns true if the given CNI specification version
// includes the "version" field in the IP address elements
@ -39,6 +39,13 @@ func SpecVersionHasCHECK(ver string) bool {
return ok
}
// SpecVersionHasSTATUS returns true if the given CNI specification version
// supports the STATUS command
func SpecVersionHasSTATUS(ver string) bool {
ok, _ := version.GreaterThanOrEqualTo(ver, "1.1.0")
return ok
}
// SpecVersionHasChaining returns true if the given CNI specification version
// supports plugin chaining
func SpecVersionHasChaining(ver string) bool {

View File

@ -51,7 +51,7 @@ func DeleteConntrackEntriesForDstIP(dstIP string, protocol uint8) error {
filter.AddIP(netlink.ConntrackOrigDstIP, ip)
filter.AddProtocol(protocol)
_, err := netlink.ConntrackDeleteFilter(netlink.ConntrackTable, family, filter)
_, err := netlink.ConntrackDeleteFilters(netlink.ConntrackTable, family, filter)
if err != nil {
return fmt.Errorf("error deleting connection tracking state for protocol: %d IP: %s, error: %v", protocol, ip, err)
}
@ -65,7 +65,7 @@ func DeleteConntrackEntriesForDstPort(port uint16, protocol uint8, family netlin
filter.AddProtocol(protocol)
filter.AddPort(netlink.ConntrackOrigDstPort, port)
_, err := netlink.ConntrackDeleteFilter(netlink.ConntrackTable, family, filter)
_, err := netlink.ConntrackDeleteFilters(netlink.ConntrackTable, family, filter)
if err != nil {
return fmt.Errorf("error deleting connection tracking state for protocol: %d Port: %d, error: %v", protocol, port, err)
}

46
pkg/utils/netfilter.go Normal file
View File

@ -0,0 +1,46 @@
// Copyright 2023 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 (
"github.com/coreos/go-iptables/iptables"
"sigs.k8s.io/knftables"
)
// SupportsIPTables tests whether the system supports using netfilter via the iptables API
// (whether via "iptables-legacy" or "iptables-nft"). (Note that this returns true if it
// is *possible* to use iptables; it does not test whether any other components on the
// system are *actually* using iptables.)
func SupportsIPTables() bool {
ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
if err != nil {
return false
}
// We don't care whether the chain actually exists, only whether we can *check*
// whether it exists.
_, err = ipt.ChainExists("filter", "INPUT")
return err == nil
}
// SupportsNFTables tests whether the system supports using netfilter via the nftables API
// (ie, not via "iptables-nft"). (Note that this returns true if it is *possible* to use
// nftables; it does not test whether any other components on the system are *actually*
// using nftables.)
func SupportsNFTables() bool {
// knftables.New() does sanity checks so we don't need any further test like in
// the iptables case.
_, err := knftables.New(knftables.IPv4Family, "supports_nftables_test")
return err == nil
}

View File

@ -0,0 +1,52 @@
// Copyright 2023 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 (
"os"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("netfilter support", func() {
When("it is available", func() {
It("reports that iptables is supported", func() {
Expect(SupportsIPTables()).To(BeTrue(), "This test should only fail if iptables is not available, but the test suite as a whole requires it to be available.")
})
It("reports that nftables is supported", func() {
Expect(SupportsNFTables()).To(BeTrue(), "This test should only fail if nftables is not available, but the test suite as a whole requires it to be available.")
})
})
// These are Serial because os.Setenv has process-wide effect
When("it is not available", Serial, func() {
var origPath string
BeforeEach(func() {
origPath = os.Getenv("PATH")
os.Setenv("PATH", "/does-not-exist")
})
AfterEach(func() {
os.Setenv("PATH", origPath)
})
It("reports that iptables is not supported", func() {
Expect(SupportsIPTables()).To(BeFalse(), "found iptables outside of PATH??")
})
It("reports that nftables is not supported", func() {
Expect(SupportsNFTables()).To(BeFalse(), "found nftables outside of PATH??")
})
})
})

View File

@ -48,11 +48,11 @@ var _ = Describe("Sysctl tests", func() {
Expect(err).NotTo(HaveOccurred())
testIfaceName = fmt.Sprintf("cnitest.%d", rand.Intn(100000))
testLinkAttrs := netlink.NewLinkAttrs()
testLinkAttrs.Name = testIfaceName
testLinkAttrs.Namespace = netlink.NsFd(int(testNs.Fd()))
testIface := &netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: testIfaceName,
Namespace: netlink.NsFd(int(testNs.Fd())),
},
LinkAttrs: testLinkAttrs,
}
err = netlink.LinkAdd(testIface)

View File

@ -1,135 +0,0 @@
// Copyright 2021 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 (
"github.com/d2g/dhcp4"
"github.com/d2g/dhcp4client"
)
const (
MaxDHCPLen = 576
)
// Send the Discovery Packet to the Broadcast Channel
func DhcpSendDiscoverPacket(c *dhcp4client.Client, options dhcp4.Options) (dhcp4.Packet, error) {
discoveryPacket := c.DiscoverPacket()
for opt, data := range options {
discoveryPacket.AddOption(opt, data)
}
discoveryPacket.PadToMinSize()
return discoveryPacket, c.SendPacket(discoveryPacket)
}
// Send Request Based On the offer Received.
func DhcpSendRequest(c *dhcp4client.Client, options dhcp4.Options, offerPacket *dhcp4.Packet) (dhcp4.Packet, error) {
requestPacket := c.RequestPacket(offerPacket)
for opt, data := range options {
requestPacket.AddOption(opt, data)
}
requestPacket.PadToMinSize()
return requestPacket, c.SendPacket(requestPacket)
}
// Send Decline to the received acknowledgement.
func DhcpSendDecline(c *dhcp4client.Client, acknowledgementPacket *dhcp4.Packet, options dhcp4.Options) (dhcp4.Packet, error) {
declinePacket := c.DeclinePacket(acknowledgementPacket)
for opt, data := range options {
declinePacket.AddOption(opt, data)
}
declinePacket.PadToMinSize()
return declinePacket, c.SendPacket(declinePacket)
}
// Lets do a Full DHCP Request.
func DhcpRequest(c *dhcp4client.Client, options dhcp4.Options) (bool, dhcp4.Packet, error) {
discoveryPacket, err := DhcpSendDiscoverPacket(c, options)
if err != nil {
return false, discoveryPacket, err
}
offerPacket, err := c.GetOffer(&discoveryPacket)
if err != nil {
return false, offerPacket, err
}
requestPacket, err := DhcpSendRequest(c, options, &offerPacket)
if err != nil {
return false, requestPacket, err
}
acknowledgement, err := c.GetAcknowledgement(&requestPacket)
if err != nil {
return false, acknowledgement, err
}
acknowledgementOptions := acknowledgement.ParseOptions()
if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK {
return false, acknowledgement, nil
}
return true, acknowledgement, nil
}
// Renew a lease backed on the Acknowledgement Packet.
// Returns Successful, The AcknoledgementPacket, Any Errors
func DhcpRenew(c *dhcp4client.Client, acknowledgement dhcp4.Packet, options dhcp4.Options) (bool, dhcp4.Packet, error) {
renewRequest := c.RenewalRequestPacket(&acknowledgement)
for opt, data := range options {
renewRequest.AddOption(opt, data)
}
renewRequest.PadToMinSize()
err := c.SendPacket(renewRequest)
if err != nil {
return false, renewRequest, err
}
newAcknowledgement, err := c.GetAcknowledgement(&renewRequest)
if err != nil {
return false, newAcknowledgement, err
}
newAcknowledgementOptions := newAcknowledgement.ParseOptions()
if dhcp4.MessageType(newAcknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK {
return false, newAcknowledgement, nil
}
return true, newAcknowledgement, nil
}
// Release a lease backed on the Acknowledgement Packet.
// Returns Any Errors
func DhcpRelease(c *dhcp4client.Client, acknowledgement dhcp4.Packet, options dhcp4.Options) error {
release := c.ReleasePacket(&acknowledgement)
for opt, data := range options {
release.AddOption(opt, data)
}
release.PadToMinSize()
return c.SendPacket(release)
}

View File

@ -39,19 +39,21 @@ import (
var errNoMoreTries = errors.New("no more tries")
type DHCP struct {
mux sync.Mutex
leases map[string]*DHCPLease
hostNetnsPrefix string
clientTimeout time.Duration
clientResendMax time.Duration
broadcast bool
mux sync.Mutex
leases map[string]*DHCPLease
hostNetnsPrefix string
clientTimeout time.Duration
clientResendMax time.Duration
clientResendTimeout time.Duration
broadcast bool
}
func newDHCP(clientTimeout, clientResendMax time.Duration) *DHCP {
func newDHCP(clientTimeout, clientResendMax time.Duration, resendTimeout time.Duration) *DHCP {
return &DHCP{
leases: make(map[string]*DHCPLease),
clientTimeout: clientTimeout,
clientResendMax: clientResendMax,
leases: make(map[string]*DHCPLease),
clientTimeout: clientTimeout,
clientResendMax: clientResendMax,
clientResendTimeout: resendTimeout,
}
}
@ -74,7 +76,7 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
return fmt.Errorf("error parsing netconf: %v", err)
}
optsRequesting, optsProviding, err := prepareOptions(args.Args, conf.IPAM.ProvideOptions, conf.IPAM.RequestOptions)
opts, err := prepareOptions(args.Args, conf.IPAM.ProvideOptions, conf.IPAM.RequestOptions)
if err != nil {
return err
}
@ -89,8 +91,8 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
} else {
hostNetns := d.hostNetnsPrefix + args.Netns
l, err = AcquireLease(clientID, hostNetns, args.IfName,
optsRequesting, optsProviding,
d.clientTimeout, d.clientResendMax, d.broadcast)
opts,
d.clientTimeout, d.clientResendMax, d.clientResendTimeout, d.broadcast)
if err != nil {
return err
}
@ -109,6 +111,11 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
Gateway: l.Gateway(),
}}
result.Routes = l.Routes()
if conf.IPAM.Priority != 0 {
for _, r := range result.Routes {
r.Priority = conf.IPAM.Priority
}
}
return nil
}
@ -185,7 +192,8 @@ func getListener(socketPath string) (net.Listener, error) {
func runDaemon(
pidfilePath, hostPrefix, socketPath string,
dhcpClientTimeout time.Duration, resendMax time.Duration, broadcast bool,
dhcpClientTimeout time.Duration, resendMax time.Duration, resendTimeout time.Duration,
broadcast bool,
) error {
// since other goroutines (on separate threads) will change namespaces,
// ensure the RPC server does not get scheduled onto those
@ -220,7 +228,7 @@ func runDaemon(
done <- true
}()
dhcp := newDHCP(dhcpClientTimeout, resendMax)
dhcp := newDHCP(dhcpClientTimeout, resendMax, resendTimeout)
dhcp.hostNetnsPrefix = hostPrefix
dhcp.broadcast = broadcast
rpc.Register(dhcp)

View File

@ -61,13 +61,12 @@ var _ = Describe("DHCP Multiple Lease Operations", func() {
})
// Start the DHCP server
dhcpServerDone, err = dhcpServerStart(originalNS, 2, dhcpServerStopCh)
Expect(err).NotTo(HaveOccurred())
dhcpServerDone = dhcpServerStart(originalNS, 2, dhcpServerStopCh)
// Start the DHCP client daemon
dhcpPluginPath, err := exec.LookPath("dhcp")
Expect(err).NotTo(HaveOccurred())
clientCmd = exec.Command(dhcpPluginPath, "daemon", "-socketpath", socketPath)
clientCmd = exec.Command(dhcpPluginPath, "daemon", "-socketpath", socketPath, "--timeout", "2s", "--resendtimeout", "8s")
err = clientCmd.Start()
Expect(err).NotTo(HaveOccurred())
Expect(clientCmd.Process).NotTo(BeNil())

View File

@ -25,10 +25,6 @@ import (
"sync"
"time"
"github.com/d2g/dhcp4"
"github.com/d2g/dhcp4server"
"github.com/d2g/dhcp4server/leasepool"
"github.com/d2g/dhcp4server/leasepool/memorypool"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/vishvananda/netlink"
@ -48,31 +44,52 @@ func getTmpDir() (string, error) {
return tmpDir, err
}
func dhcpServerStart(netns ns.NetNS, numLeases int, stopCh <-chan bool) (*sync.WaitGroup, error) {
// Add the expected IP to the pool
lp := memorypool.MemoryPool{}
type DhcpServer struct {
cmd *exec.Cmd
lock sync.Mutex
Expect(numLeases).To(BeNumerically(">", 0))
// Currently tests only need at most 2
Expect(numLeases).To(BeNumerically("<=", 2))
startAddr net.IP
endAddr net.IP
leaseTime time.Duration
}
// tests expect first lease to be at address 192.168.1.5
for i := 5; i < numLeases+5; i++ {
err := lp.AddLease(leasepool.Lease{IP: dhcp4.IPAdd(net.IPv4(192, 168, 1, byte(i)), 0)})
if err != nil {
return nil, fmt.Errorf("error adding IP to DHCP pool: %v", err)
}
func (s *DhcpServer) Serve() error {
if err := s.Start(); err != nil {
return err
}
return s.cmd.Wait()
}
dhcpServer, err := dhcp4server.New(
net.IPv4(192, 168, 1, 1),
&lp,
dhcp4server.SetLocalAddr(net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 67}),
dhcp4server.SetRemoteAddr(net.UDPAddr{IP: net.IPv4bcast, Port: 68}),
dhcp4server.LeaseDuration(time.Minute*15),
func (s *DhcpServer) Start() error {
s.lock.Lock()
defer s.lock.Unlock()
s.cmd = exec.Command(
"dnsmasq",
"--no-daemon",
"--dhcp-sequential-ip", // allocate IPs sequentially
"--port=0", // disable DNS
"--conf-file=-", // Do not read /etc/dnsmasq.conf
fmt.Sprintf("--dhcp-range=%s,%s,%d", s.startAddr, s.endAddr, int(s.leaseTime.Seconds())),
)
if err != nil {
return nil, fmt.Errorf("failed to create DHCP server: %v", err)
s.cmd.Stdin = bytes.NewBufferString("")
s.cmd.Stdout = os.Stdout
s.cmd.Stderr = os.Stderr
return s.cmd.Start()
}
func (s *DhcpServer) Stop() error {
s.lock.Lock()
defer s.lock.Unlock()
return s.cmd.Process.Kill()
}
func dhcpServerStart(netns ns.NetNS, numLeases int, stopCh <-chan bool) *sync.WaitGroup {
dhcpServer := &DhcpServer{
startAddr: net.IPv4(192, 168, 1, 5),
endAddr: net.IPv4(192, 168, 1, 5+uint8(numLeases)-1),
leaseTime: 5 * time.Minute,
}
stopWg := sync.WaitGroup{}
@ -84,9 +101,10 @@ func dhcpServerStart(netns ns.NetNS, numLeases int, stopCh <-chan bool) (*sync.W
go func() {
defer GinkgoRecover()
err = netns.Do(func(ns.NetNS) error {
err := netns.Do(func(ns.NetNS) error {
startWg.Done()
if err := dhcpServer.ListenAndServe(); err != nil {
if err := dhcpServer.Serve(); err != nil {
// Log, but don't trap errors; the server will
// always report an error when stopped
GinkgoT().Logf("DHCP server finished with error: %v", err)
@ -103,12 +121,12 @@ func dhcpServerStart(netns ns.NetNS, numLeases int, stopCh <-chan bool) (*sync.W
go func() {
startWg.Done()
<-stopCh
dhcpServer.Shutdown()
dhcpServer.Stop()
stopWg.Done()
}()
startWg.Wait()
return &stopWg, nil
return &stopWg
}
const (
@ -155,11 +173,11 @@ var _ = Describe("DHCP Operations", func() {
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = hostVethName
err = netlink.LinkAdd(&netlink.Veth{
LinkAttrs: netlink.LinkAttrs{
Name: hostVethName,
},
PeerName: contVethName,
LinkAttrs: linkAttrs,
PeerName: contVethName,
})
Expect(err).NotTo(HaveOccurred())
@ -200,8 +218,7 @@ var _ = Describe("DHCP Operations", func() {
})
// Start the DHCP server
dhcpServerDone, err = dhcpServerStart(originalNS, 1, dhcpServerStopCh)
Expect(err).NotTo(HaveOccurred())
dhcpServerDone = dhcpServerStart(originalNS, 1, dhcpServerStopCh)
// Start the DHCP client daemon
dhcpPluginPath, err := exec.LookPath("dhcp")
@ -394,11 +411,11 @@ func dhcpSetupOriginalNS() (chan bool, string, ns.NetNS, ns.NetNS, error) {
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = hostBridgeName
// Create bridge in the "host" (original) NS
br = &netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{
Name: hostBridgeName,
},
LinkAttrs: linkAttrs,
}
err = netlink.LinkAdd(br)
@ -517,8 +534,7 @@ var _ = Describe("DHCP Lease Unavailable Operations", func() {
})
// Start the DHCP server
dhcpServerDone, err = dhcpServerStart(originalNS, 1, dhcpServerStopCh)
Expect(err).NotTo(HaveOccurred())
dhcpServerDone = dhcpServerStart(originalNS, 1, dhcpServerStopCh)
// Start the DHCP client daemon
dhcpPluginPath, err := exec.LookPath("dhcp")
@ -528,7 +544,7 @@ var _ = Describe("DHCP Lease Unavailable Operations", func() {
// `go test` timeout with default delays. Since our DHCP server
// and client daemon are local processes anyway, we can depend on
// them to respond very quickly.
clientCmd = exec.Command(dhcpPluginPath, "daemon", "-socketpath", socketPath, "-timeout", "2s", "-resendmax", "8s")
clientCmd = exec.Command(dhcpPluginPath, "daemon", "-socketpath", socketPath, "-timeout", "2s", "-resendmax", "8s", "--resendtimeout", "10s")
// copy dhcp client's stdout/stderr to test stdout
var b bytes.Buffer

View File

@ -15,6 +15,7 @@
package main
import (
"context"
"fmt"
"log"
"math/rand"
@ -24,8 +25,8 @@ import (
"sync/atomic"
"time"
"github.com/d2g/dhcp4"
"github.com/d2g/dhcp4client"
dhcp4 "github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
"github.com/vishvananda/netlink"
"github.com/containernetworking/cni/pkg/types"
@ -35,8 +36,10 @@ import (
// RFC 2131 suggests using exponential backoff, starting with 4sec
// and randomized to +/- 1sec
const (
resendDelay0 = 4 * time.Second
resendDelayMax = 62 * time.Second
resendDelay0 = 4 * time.Second
resendDelayMax = 62 * time.Second
defaultLeaseTime = 60 * time.Minute
defaultResendTimeout = 208 * time.Second // fast resend + backoff resend
)
// To speed up the retry for first few failures, we retry without
@ -60,34 +63,35 @@ const (
type DHCPLease struct {
clientID string
ack *dhcp4.Packet
opts dhcp4.Options
latestLease *nclient4.Lease
link netlink.Link
renewalTime time.Time
rebindingTime time.Time
expireTime time.Time
timeout time.Duration
resendMax time.Duration
resendTimeout time.Duration
broadcast bool
stopping uint32
stop chan struct{}
check chan struct{}
wg sync.WaitGroup
cancelFunc context.CancelFunc
ctx context.Context
// list of requesting and providing options and if they are necessary / their value
optsRequesting map[dhcp4.OptionCode]bool
optsProviding map[dhcp4.OptionCode][]byte
opts []dhcp4.Option
}
var requestOptionsDefault = map[dhcp4.OptionCode]bool{
dhcp4.OptionRouter: true,
dhcp4.OptionSubnetMask: true,
var requestOptionsDefault = []dhcp4.OptionCode{
dhcp4.OptionRouter,
dhcp4.OptionSubnetMask,
}
func prepareOptions(cniArgs string, provideOptions []ProvideOption, requestOptions []RequestOption) (
map[dhcp4.OptionCode]bool, map[dhcp4.OptionCode][]byte, error,
[]dhcp4.Option, error,
) {
var optsRequesting map[dhcp4.OptionCode]bool
var optsProviding map[dhcp4.OptionCode][]byte
var opts []dhcp4.Option
var err error
// parse CNI args
cniArgsParsed := map[string]string{}
@ -100,46 +104,51 @@ func prepareOptions(cniArgs string, provideOptions []ProvideOption, requestOptio
// parse providing options map
var optParsed dhcp4.OptionCode
optsProviding = make(map[dhcp4.OptionCode][]byte)
for _, opt := range provideOptions {
optParsed, err = parseOptionName(string(opt.Option))
if err != nil {
return nil, nil, fmt.Errorf("Can not parse option %q: %w", opt.Option, err)
return nil, fmt.Errorf("Can not parse option %q: %w", opt.Option, err)
}
if len(opt.Value) > 0 {
if len(opt.Value) > 255 {
return nil, nil, fmt.Errorf("value too long for option %q: %q", opt.Option, opt.Value)
return nil, fmt.Errorf("value too long for option %q: %q", opt.Option, opt.Value)
}
optsProviding[optParsed] = []byte(opt.Value)
opts = append(opts, dhcp4.Option{Code: optParsed, Value: dhcp4.String(opt.Value)})
}
if value, ok := cniArgsParsed[opt.ValueFromCNIArg]; ok {
if len(value) > 255 {
return nil, nil, fmt.Errorf("value too long for option %q from CNI_ARGS %q: %q", opt.Option, opt.ValueFromCNIArg, opt.Value)
return nil, fmt.Errorf("value too long for option %q from CNI_ARGS %q: %q", opt.Option, opt.ValueFromCNIArg, opt.Value)
}
optsProviding[optParsed] = []byte(value)
opts = append(opts, dhcp4.Option{Code: optParsed, Value: dhcp4.String(value)})
}
}
// parse necessary options map
optsRequesting = make(map[dhcp4.OptionCode]bool)
var optsRequesting dhcp4.OptionCodeList
skipRequireDefault := false
for _, opt := range requestOptions {
if opt.SkipDefault {
skipRequireDefault = true
}
if opt.Option == "" {
continue
}
optParsed, err = parseOptionName(string(opt.Option))
if err != nil {
return nil, nil, fmt.Errorf("Can not parse option %q: %w", opt.Option, err)
return nil, fmt.Errorf("Can not parse option %q: %w", opt.Option, err)
}
optsRequesting[optParsed] = true
optsRequesting.Add(optParsed)
}
for k, v := range requestOptionsDefault {
// only set if not skipping default and this value does not exists
if _, ok := optsRequesting[k]; !ok && !skipRequireDefault {
optsRequesting[k] = v
if !skipRequireDefault {
for _, opt := range requestOptionsDefault {
optsRequesting.Add(opt)
}
}
return optsRequesting, optsProviding, err
if len(optsRequesting) > 0 {
opts = append(opts, dhcp4.Option{Code: dhcp4.OptionParameterRequestList, Value: optsRequesting})
}
return opts, err
}
// AcquireLease gets an DHCP lease and then maintains it in the background
@ -147,19 +156,25 @@ func prepareOptions(cniArgs string, provideOptions []ProvideOption, requestOptio
// calling DHCPLease.Stop()
func AcquireLease(
clientID, netns, ifName string,
optsRequesting map[dhcp4.OptionCode]bool, optsProviding map[dhcp4.OptionCode][]byte,
timeout, resendMax time.Duration, broadcast bool,
opts []dhcp4.Option,
timeout, resendMax time.Duration, resendTimeout time.Duration, broadcast bool,
) (*DHCPLease, error) {
errCh := make(chan error, 1)
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
l := &DHCPLease{
clientID: clientID,
stop: make(chan struct{}),
check: make(chan struct{}),
timeout: timeout,
resendMax: resendMax,
broadcast: broadcast,
optsRequesting: optsRequesting,
optsProviding: optsProviding,
clientID: clientID,
stop: make(chan struct{}),
check: make(chan struct{}),
timeout: timeout,
resendMax: resendMax,
resendTimeout: resendTimeout,
broadcast: broadcast,
opts: opts,
cancelFunc: cancel,
ctx: ctx,
}
log.Printf("%v: acquiring lease", clientID)
@ -201,6 +216,7 @@ func AcquireLease(
func (l *DHCPLease) Stop() {
if atomic.CompareAndSwapUint32(&l.stopping, 0, 1) {
close(l.stop)
l.cancelFunc()
}
l.wg.Wait()
}
@ -209,92 +225,65 @@ func (l *DHCPLease) Check() {
l.check <- struct{}{}
}
func (l *DHCPLease) getOptionsWithClientID() dhcp4.Options {
opts := make(dhcp4.Options)
opts[dhcp4.OptionClientIdentifier] = []byte(l.clientID)
// client identifier's first byte is "type"
newClientID := []byte{0}
newClientID = append(newClientID, opts[dhcp4.OptionClientIdentifier]...)
opts[dhcp4.OptionClientIdentifier] = newClientID
return opts
func withClientID(clientID string) dhcp4.Modifier {
return func(d *dhcp4.DHCPv4) {
optClientID := []byte{0}
optClientID = append(optClientID, []byte(clientID)...)
d.Options.Update(dhcp4.OptClientIdentifier(optClientID))
}
}
func (l *DHCPLease) getAllOptions() dhcp4.Options {
opts := l.getOptionsWithClientID()
for k, v := range l.optsProviding {
opts[k] = v
func withAllOptions(l *DHCPLease) dhcp4.Modifier {
return func(d *dhcp4.DHCPv4) {
for _, opt := range l.opts {
d.Options.Update(opt)
}
}
opts[dhcp4.OptionParameterRequestList] = []byte{}
for k := range l.optsRequesting {
opts[dhcp4.OptionParameterRequestList] = append(opts[dhcp4.OptionParameterRequestList], byte(k))
}
return opts
}
func (l *DHCPLease) acquire() error {
c, err := newDHCPClient(l.link, l.timeout, l.broadcast)
if (l.link.Attrs().Flags & net.FlagUp) != net.FlagUp {
log.Printf("Link %q down. Attempting to set up", l.link.Attrs().Name)
if err := netlink.LinkSetUp(l.link); err != nil {
return err
}
}
c, err := newDHCPClient(l.link, l.timeout)
if err != nil {
return err
}
defer c.Close()
if (l.link.Attrs().Flags & net.FlagUp) != net.FlagUp {
log.Printf("Link %q down. Attempting to set up", l.link.Attrs().Name)
if err = netlink.LinkSetUp(l.link); err != nil {
return err
}
}
opts := l.getAllOptions()
pkt, err := backoffRetry(l.resendMax, func() (*dhcp4.Packet, error) {
ok, ack, err := DhcpRequest(c, opts)
switch {
case err != nil:
return nil, err
case !ok:
return nil, fmt.Errorf("DHCP server NACK'd own offer")
default:
return &ack, nil
}
timeoutCtx, cancel := context.WithTimeoutCause(l.ctx, l.resendTimeout, errNoMoreTries)
defer cancel()
pkt, err := backoffRetry(timeoutCtx, l.resendMax, func() (*nclient4.Lease, error) {
return c.Request(
timeoutCtx,
withClientID(l.clientID),
withAllOptions(l),
)
})
if err != nil {
return err
}
return l.commit(pkt)
l.commit(pkt)
return nil
}
func (l *DHCPLease) commit(ack *dhcp4.Packet) error {
opts := ack.ParseOptions()
func (l *DHCPLease) commit(lease *nclient4.Lease) {
l.latestLease = lease
ack := lease.ACK
leaseTime, err := parseLeaseTime(opts)
if err != nil {
return err
}
rebindingTime, err := parseRebindingTime(opts)
if err != nil || rebindingTime > leaseTime {
// Per RFC 2131 Section 4.4.5, it should default to 85% of lease time
rebindingTime = leaseTime * 85 / 100
}
renewalTime, err := parseRenewalTime(opts)
if err != nil || renewalTime > rebindingTime {
// Per RFC 2131 Section 4.4.5, it should default to 50% of lease time
renewalTime = leaseTime / 2
}
leaseTime := ack.IPAddressLeaseTime(defaultLeaseTime)
rebindingTime := ack.IPAddressRebindingTime(leaseTime * 85 / 100)
renewalTime := ack.IPAddressRenewalTime(leaseTime / 2)
now := time.Now()
l.expireTime = now.Add(leaseTime)
l.renewalTime = now.Add(renewalTime)
l.rebindingTime = now.Add(rebindingTime)
l.ack = ack
l.opts = opts
return nil
}
func (l *DHCPLease) maintain() {
@ -362,44 +351,40 @@ func (l *DHCPLease) downIface() {
}
func (l *DHCPLease) renew() error {
c, err := newDHCPClient(l.link, l.timeout, l.broadcast)
c, err := newDHCPClient(l.link, l.timeout)
if err != nil {
return err
}
defer c.Close()
opts := l.getAllOptions()
pkt, err := backoffRetry(l.resendMax, func() (*dhcp4.Packet, error) {
ok, ack, err := DhcpRenew(c, *l.ack, opts)
switch {
case err != nil:
return nil, err
case !ok:
return nil, fmt.Errorf("DHCP server did not renew lease")
default:
return &ack, nil
}
timeoutCtx, cancel := context.WithTimeoutCause(l.ctx, l.resendTimeout, errNoMoreTries)
defer cancel()
lease, err := backoffRetry(timeoutCtx, l.resendMax, func() (*nclient4.Lease, error) {
return c.Renew(
timeoutCtx,
l.latestLease,
withClientID(l.clientID),
withAllOptions(l),
)
})
if err != nil {
return err
}
l.commit(pkt)
l.commit(lease)
return nil
}
func (l *DHCPLease) release() error {
log.Printf("%v: releasing lease", l.clientID)
c, err := newDHCPClient(l.link, l.timeout, l.broadcast)
c, err := newDHCPClient(l.link, l.timeout)
if err != nil {
return err
}
defer c.Close()
opts := l.getOptionsWithClientID()
if err = DhcpRelease(c, *l.ack, opts); err != nil {
if err = c.Release(l.latestLease, withClientID(l.clientID)); err != nil {
return fmt.Errorf("failed to send DHCPRELEASE")
}
@ -407,33 +392,47 @@ func (l *DHCPLease) release() error {
}
func (l *DHCPLease) IPNet() (*net.IPNet, error) {
mask := parseSubnetMask(l.opts)
ack := l.latestLease.ACK
mask := ack.SubnetMask()
if mask == nil {
return nil, fmt.Errorf("DHCP option Subnet Mask not found in DHCPACK")
}
return &net.IPNet{
IP: l.ack.YIAddr(),
IP: ack.YourIPAddr,
Mask: mask,
}, nil
}
func (l *DHCPLease) Gateway() net.IP {
return parseRouter(l.opts)
ack := l.latestLease.ACK
gws := ack.Router()
if len(gws) > 0 {
return gws[0]
}
return nil
}
func (l *DHCPLease) Routes() []*types.Route {
routes := []*types.Route{}
ack := l.latestLease.ACK
// RFC 3442 states that if Classless Static Routes (option 121)
// exist, we ignore Static Routes (option 33) and the Router/Gateway.
opt121Routes := parseCIDRRoutes(l.opts)
opt121Routes := ack.ClasslessStaticRoute()
if len(opt121Routes) > 0 {
return append(routes, opt121Routes...)
for _, r := range opt121Routes {
routes = append(routes, &types.Route{Dst: *r.Dest, GW: r.Router})
}
return routes
}
// Append Static Routes
routes = append(routes, parseRoutes(l.opts)...)
if ack.Options.Has(dhcp4.OptionStaticRoutingTable) {
routes = append(routes, parseRoutes(ack.Options.Get(dhcp4.OptionStaticRoutingTable))...)
}
// The CNI spec says even if there is a gateway specified, we must
// add a default route in the routes section.
@ -450,7 +449,7 @@ func jitter(span time.Duration) time.Duration {
return time.Duration(float64(span) * (2.0*rand.Float64() - 1.0))
}
func backoffRetry(resendMax time.Duration, f func() (*dhcp4.Packet, error)) (*dhcp4.Packet, error) {
func backoffRetry(ctx context.Context, resendMax time.Duration, f func() (*nclient4.Lease, error)) (*nclient4.Lease, error) {
baseDelay := resendDelay0
var sleepTime time.Duration
fastRetryLimit := resendFastMax
@ -471,33 +470,23 @@ func backoffRetry(resendMax time.Duration, f func() (*dhcp4.Packet, error)) (*dh
log.Printf("retrying in %f seconds", sleepTime.Seconds())
time.Sleep(sleepTime)
// only adjust delay time if we are in normal backoff stage
if baseDelay < resendMax && fastRetryLimit == 0 {
baseDelay *= 2
} else if fastRetryLimit == 0 { // only break if we are at normal delay
break
select {
case <-ctx.Done():
return nil, context.Cause(ctx)
case <-time.After(sleepTime):
// only adjust delay time if we are in normal backoff stage
if baseDelay < resendMax && fastRetryLimit == 0 {
baseDelay *= 2
}
}
}
return nil, errNoMoreTries
}
func newDHCPClient(
link netlink.Link,
timeout time.Duration,
broadcast bool,
) (*dhcp4client.Client, error) {
pktsock, err := dhcp4client.NewPacketSock(link.Attrs().Index)
if err != nil {
return nil, err
}
return dhcp4client.New(
dhcp4client.HardwareAddr(link.Attrs().HardwareAddr),
dhcp4client.Timeout(timeout),
dhcp4client.Broadcast(broadcast),
dhcp4client.Connection(pktsock),
)
clientOpts ...nclient4.ClientOpt,
) (*nclient4.Client, error) {
clientOpts = append(clientOpts, nclient4.WithTimeout(timeout))
return nclient4.New(link.Attrs().Name, clientOpts...)
}

View File

@ -51,6 +51,8 @@ type IPAMConfig struct {
// To override default requesting fields, set `skipDefault` to `false`.
// If an field is not optional, but the server failed to provide it, error will be raised.
RequestOptions []RequestOption `json:"request"`
// The metric of routes
Priority int `json:"priority,omitempty"`
}
// DHCPOption represents a DHCP option. It can be a number, or a string defined in manual dhcp-options(5).
@ -78,25 +80,33 @@ func main() {
var broadcast bool
var timeout time.Duration
var resendMax time.Duration
var resendTimeout time.Duration
daemonFlags := flag.NewFlagSet("daemon", flag.ExitOnError)
daemonFlags.StringVar(&pidfilePath, "pidfile", "", "optional path to write daemon PID to")
daemonFlags.StringVar(&hostPrefix, "hostprefix", "", "optional prefix to host root")
daemonFlags.StringVar(&socketPath, "socketpath", "", "optional dhcp server socketpath")
daemonFlags.BoolVar(&broadcast, "broadcast", false, "broadcast DHCP leases")
daemonFlags.DurationVar(&timeout, "timeout", 10*time.Second, "optional dhcp client timeout duration")
daemonFlags.DurationVar(&resendMax, "resendmax", resendDelayMax, "optional dhcp client resend max duration")
daemonFlags.DurationVar(&timeout, "timeout", 10*time.Second, "optional dhcp client timeout duration for each request")
daemonFlags.DurationVar(&resendMax, "resendmax", resendDelayMax, "optional dhcp client max resend delay between requests")
daemonFlags.DurationVar(&resendTimeout, "resendtimeout", defaultResendTimeout, "optional dhcp client resend timeout, no more retries after this timeout")
daemonFlags.Parse(os.Args[2:])
if socketPath == "" {
socketPath = defaultSocketPath
}
if err := runDaemon(pidfilePath, hostPrefix, socketPath, timeout, resendMax, broadcast); err != nil {
if err := runDaemon(pidfilePath, hostPrefix, socketPath, timeout, resendMax, resendTimeout, broadcast); err != nil {
log.Print(err.Error())
os.Exit(1)
}
} else {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("dhcp"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
/* FIXME GC */
/* FIXME Status */
}, version.All, bv.BuildString("dhcp"))
}
}

View File

@ -15,13 +15,11 @@
package main
import (
"encoding/binary"
"fmt"
"net"
"strconv"
"time"
"github.com/d2g/dhcp4"
dhcp4 "github.com/insomniacslk/dhcp/dhcpv4"
"github.com/containernetworking/cni/pkg/types"
)
@ -31,8 +29,8 @@ var optionNameToID = map[string]dhcp4.OptionCode{
"subnet-mask": dhcp4.OptionSubnetMask,
"routers": dhcp4.OptionRouter,
"host-name": dhcp4.OptionHostName,
"user-class": dhcp4.OptionUserClass,
"vendor-class-identifier": dhcp4.OptionVendorClassIdentifier,
"user-class": dhcp4.OptionUserClassInformation,
"vendor-class-identifier": dhcp4.OptionClassIdentifier,
}
func parseOptionName(option string) (dhcp4.OptionCode, error) {
@ -41,18 +39,9 @@ func parseOptionName(option string) (dhcp4.OptionCode, error) {
}
i, err := strconv.ParseUint(option, 10, 8)
if err != nil {
return 0, fmt.Errorf("Can not parse option: %w", err)
return dhcp4.OptionPad, fmt.Errorf("Can not parse option: %w", err)
}
return dhcp4.OptionCode(i), nil
}
func parseRouter(opts dhcp4.Options) net.IP {
if opts, ok := opts[dhcp4.OptionRouter]; ok {
if len(opts) == 4 {
return net.IP(opts)
}
}
return nil
return dhcp4.GenericOptionCode(i), nil
}
func classfulSubnet(sn net.IP) net.IPNet {
@ -62,100 +51,22 @@ func classfulSubnet(sn net.IP) net.IPNet {
}
}
func parseRoutes(opts dhcp4.Options) []*types.Route {
func parseRoutes(opt []byte) []*types.Route {
// StaticRoutes format: pairs of:
// Dest = 4 bytes; Classful IP subnet
// Router = 4 bytes; IP address of router
routes := []*types.Route{}
if opt, ok := opts[dhcp4.OptionStaticRoute]; ok {
for len(opt) >= 8 {
sn := opt[0:4]
r := opt[4:8]
rt := &types.Route{
Dst: classfulSubnet(sn),
GW: r,
}
routes = append(routes, rt)
opt = opt[8:]
for len(opt) >= 8 {
sn := opt[0:4]
r := opt[4:8]
rt := &types.Route{
Dst: classfulSubnet(sn),
GW: r,
}
routes = append(routes, rt)
opt = opt[8:]
}
return routes
}
func parseCIDRRoutes(opts dhcp4.Options) []*types.Route {
// See RFC4332 for format (http://tools.ietf.org/html/rfc3442)
routes := []*types.Route{}
if opt, ok := opts[dhcp4.OptionClasslessRouteFormat]; ok {
for len(opt) >= 5 {
width := int(opt[0])
if width > 32 {
// error: can't have more than /32
return nil
}
// network bits are compacted to avoid zeros
octets := 0
if width > 0 {
octets = (width-1)/8 + 1
}
if len(opt) < 1+octets+4 {
// error: too short
return nil
}
sn := make([]byte, 4)
copy(sn, opt[1:octets+1])
gw := net.IP(opt[octets+1 : octets+5])
rt := &types.Route{
Dst: net.IPNet{
IP: net.IP(sn),
Mask: net.CIDRMask(width, 32),
},
GW: gw,
}
routes = append(routes, rt)
opt = opt[octets+5:]
}
}
return routes
}
func parseSubnetMask(opts dhcp4.Options) net.IPMask {
mask, ok := opts[dhcp4.OptionSubnetMask]
if !ok {
return nil
}
return net.IPMask(mask)
}
func parseDuration(opts dhcp4.Options, code dhcp4.OptionCode, optName string) (time.Duration, error) {
val, ok := opts[code]
if !ok {
return 0, fmt.Errorf("option %v not found", optName)
}
if len(val) != 4 {
return 0, fmt.Errorf("option %v is not 4 bytes", optName)
}
secs := binary.BigEndian.Uint32(val)
return time.Duration(secs) * time.Second, nil
}
func parseLeaseTime(opts dhcp4.Options) (time.Duration, error) {
return parseDuration(opts, dhcp4.OptionIPAddressLeaseTime, "LeaseTime")
}
func parseRenewalTime(opts dhcp4.Options) (time.Duration, error) {
return parseDuration(opts, dhcp4.OptionRenewalTimeValue, "RenewalTime")
}
func parseRebindingTime(opts dhcp4.Options) (time.Duration, error) {
return parseDuration(opts, dhcp4.OptionRebindingTimeValue, "RebindingTime")
}

View File

@ -19,7 +19,7 @@ import (
"reflect"
"testing"
"github.com/d2g/dhcp4"
dhcp4 "github.com/insomniacslk/dhcp/dhcpv4"
"github.com/containernetworking/cni/pkg/types"
)
@ -61,17 +61,8 @@ func validateRoutes(t *testing.T, routes []*types.Route) {
}
func TestParseRoutes(t *testing.T) {
opts := make(dhcp4.Options)
opts[dhcp4.OptionStaticRoute] = []byte{10, 0, 0, 0, 10, 1, 2, 3, 192, 168, 1, 0, 192, 168, 2, 3}
routes := parseRoutes(opts)
validateRoutes(t, routes)
}
func TestParseCIDRRoutes(t *testing.T) {
opts := make(dhcp4.Options)
opts[dhcp4.OptionClasslessRouteFormat] = []byte{8, 10, 10, 1, 2, 3, 24, 192, 168, 1, 192, 168, 2, 3}
routes := parseCIDRRoutes(opts)
data := []byte{10, 0, 0, 0, 10, 1, 2, 3, 192, 168, 1, 0, 192, 168, 2, 3}
routes := parseRoutes(data)
validateRoutes(t, routes)
}
@ -87,10 +78,10 @@ func TestParseOptionName(t *testing.T) {
"hostname", "host-name", dhcp4.OptionHostName, false,
},
{
"hostname in number", "12", dhcp4.OptionHostName, false,
"hostname in number", "12", dhcp4.GenericOptionCode(12), false,
},
{
"random string", "doNotparseMe", 0, true,
"random string", "doNotparseMe", dhcp4.OptionPad, true,
},
}
for _, tt := range tests {

View File

@ -15,6 +15,7 @@
package main
import (
"errors"
"fmt"
"net"
"strings"
@ -29,7 +30,13 @@ import (
)
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("host-local"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
/* FIXME GC */
/* FIXME Status */
}, version.All, bv.BuildString("host-local"))
}
func cmdCheck(args *skel.CmdArgs) error {
@ -124,7 +131,7 @@ func cmdAdd(args *skel.CmdArgs) error {
for _, ip := range requestedIPs {
errstr = errstr + " " + ip.String()
}
return fmt.Errorf(errstr)
return errors.New(errstr)
}
result.Routes = ipamConf.Routes
@ -145,18 +152,18 @@ func cmdDel(args *skel.CmdArgs) error {
defer store.Close()
// Loop through all ranges, releasing all IPs, even if an error occurs
var errors []string
var errs []string
for idx, rangeset := range ipamConf.Ranges {
ipAllocator := allocator.NewIPAllocator(&rangeset, store, idx)
err := ipAllocator.Release(args.ContainerID, args.IfName)
if err != nil {
errors = append(errors, err.Error())
errs = append(errs, err.Error())
}
}
if errors != nil {
return fmt.Errorf(strings.Join(errors, ";"))
if errs != nil {
return errors.New(strings.Join(errs, ";"))
}
return nil
}

View File

@ -68,7 +68,13 @@ type Address struct {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("static"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
/* FIXME GC */
/* FIXME Status */
}, version.All, bv.BuildString("static"))
}
func loadNetConf(bytes []byte) (*types.NetConf, string, error) {

View File

@ -35,7 +35,6 @@ import (
"github.com/containernetworking/plugins/pkg/ipam"
"github.com/containernetworking/plugins/pkg/link"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containernetworking/plugins/pkg/utils"
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
"github.com/containernetworking/plugins/pkg/utils/sysctl"
)
@ -52,6 +51,7 @@ type NetConf struct {
IsDefaultGW bool `json:"isDefaultGateway"`
ForceAddress bool `json:"forceAddress"`
IPMasq bool `json:"ipMasq"`
IPMasqBackend *string `json:"ipMasqBackend,omitempty"`
MTU int `json:"mtu"`
HairpinMode bool `json:"hairpinMode"`
PromiscMode bool `json:"promiscMode"`
@ -61,6 +61,7 @@ type NetConf struct {
MacSpoofChk bool `json:"macspoofchk,omitempty"`
EnableDad bool `json:"enabledad,omitempty"`
DisableContainerInterface bool `json:"disableContainerInterface,omitempty"`
PortIsolation bool `json:"portIsolation,omitempty"`
Args struct {
Cni BridgeArgs `json:"cni,omitempty"`
@ -335,16 +336,11 @@ func bridgeByName(name string) (*netlink.Bridge, error) {
}
func ensureBridge(brName string, mtu int, promiscMode, vlanFiltering bool) (*netlink.Bridge, error) {
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = brName
linkAttrs.MTU = mtu
br := &netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{
Name: brName,
MTU: mtu,
// Let kernel use default txqueuelen; leaving it unset
// means 0, and a zero-length TX queue messes up FIFO
// traffic shapers which use TX queue length as the
// default packet limit
TxQLen: -1,
},
LinkAttrs: linkAttrs,
}
if vlanFiltering {
br.VlanFiltering = &vlanFiltering
@ -392,7 +388,7 @@ func ensureVlanInterface(br *netlink.Bridge, vlanID int, preserveDefaultVlan boo
return nil, fmt.Errorf("faild to find host namespace: %v", err)
}
_, brGatewayIface, err := setupVeth(hostNS, br, name, br.MTU, false, vlanID, nil, preserveDefaultVlan, "")
_, brGatewayIface, err := setupVeth(hostNS, br, name, br.MTU, false, vlanID, nil, preserveDefaultVlan, "", false)
if err != nil {
return nil, fmt.Errorf("faild to create vlan gateway %q: %v", name, err)
}
@ -411,7 +407,18 @@ func ensureVlanInterface(br *netlink.Bridge, vlanID int, preserveDefaultVlan boo
return brGatewayVeth, nil
}
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool, vlanID int, vlans []int, preserveDefaultVlan bool, mac string) (*current.Interface, *current.Interface, error) {
func setupVeth(
netns ns.NetNS,
br *netlink.Bridge,
ifName string,
mtu int,
hairpinMode bool,
vlanID int,
vlans []int,
preserveDefaultVlan bool,
mac string,
portIsolation bool,
) (*current.Interface, *current.Interface, error) {
contIface := &current.Interface{}
hostIface := &current.Interface{}
@ -448,6 +455,11 @@ func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairp
return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err)
}
// set isolation mode
if err = netlink.LinkSetIsolated(hostVeth, portIsolation); err != nil {
return nil, nil, fmt.Errorf("failed to set isolated on for %v: %v", hostVeth.Attrs().Name, err)
}
if (vlanID != 0 || len(vlans) > 0) && !preserveDefaultVlan {
err = removeDefaultVlan(hostVeth)
if err != nil {
@ -554,7 +566,7 @@ func cmdAdd(args *skel.CmdArgs) error {
}
defer netns.Close()
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode, n.Vlan, n.vlans, n.PreserveDefaultVlan, n.mac)
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode, n.Vlan, n.vlans, n.PreserveDefaultVlan, n.mac, n.PortIsolation)
if err != nil {
return err
}
@ -673,12 +685,12 @@ func cmdAdd(args *skel.CmdArgs) error {
}
if n.IPMasq {
chain := utils.FormatChainName(n.Name, args.ContainerID)
comment := utils.FormatComment(n.Name, args.ContainerID)
ipns := []*net.IPNet{}
for _, ipc := range result.IPs {
if err = ip.SetupIPMasq(&ipc.Address, chain, comment); err != nil {
return err
}
ipns = append(ipns, &ipc.Address)
}
if err = ip.SetupIPMasqForNetworks(n.IPMasqBackend, ipns, n.Name, args.IfName, args.ContainerID); err != nil {
return err
}
}
} else if !n.DisableContainerInterface {
@ -814,12 +826,8 @@ func cmdDel(args *skel.CmdArgs) error {
}
if isLayer3 && n.IPMasq {
chain := utils.FormatChainName(n.Name, args.ContainerID)
comment := utils.FormatComment(n.Name, args.ContainerID)
for _, ipn := range ipnets {
if err := ip.TeardownIPMasq(ipn, chain, comment); err != nil {
return err
}
if err := ip.TeardownIPMasqForNetworks(ipnets, n.Name, args.IfName, args.ContainerID); err != nil {
return err
}
}
@ -827,7 +835,13 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("bridge"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
Status: cmdStatus,
/* FIXME GC */
}, version.All, bv.BuildString("bridge"))
}
type cniBridgeIf struct {
@ -1088,3 +1102,18 @@ func cmdCheck(args *skel.CmdArgs) error {
func uniqueID(containerID, cniIface string) string {
return containerID + "-" + cniIface
}
func cmdStatus(args *skel.CmdArgs) error {
conf := NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("failed to load netconf: %w", err)
}
if conf.IPAM.Type != "" {
if err := ipam.ExecStatus(conf.IPAM.Type, args.StdinData); err != nil {
return err
}
}
return nil
}

View File

@ -15,6 +15,7 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net"
@ -27,6 +28,7 @@ import (
. "github.com/onsi/gomega"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netlink/nl"
"sigs.k8s.io/knftables"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
@ -77,8 +79,10 @@ type testCase struct {
vlanTrunk []*VlanTrunk
removeDefaultVlan bool
ipMasq bool
ipMasqBackend string
macspoofchk bool
disableContIface bool
portIsolation bool
AddErr020 string
DelErr020 string
@ -159,6 +163,9 @@ const (
disableContainerInterface = `,
"disableContainerInterface": true`
portIsolation = `,
"portIsolation": true`
ipamStartStr = `,
"ipam": {
"type": "host-local"`
@ -172,6 +179,9 @@ const (
ipMasqConfStr = `,
"ipMasq": %t`
ipMasqBackendConfStr = `,
"ipMasqBackend": "%s"`
// Single subnet configuration (legacy)
subnetConfStr = `,
"subnet": "%s"`
@ -243,6 +253,9 @@ func (tc testCase) netConfJSON(dataDir string) string {
if tc.ipMasq {
conf += tc.ipMasqConfig()
}
if tc.ipMasqBackend != "" {
conf += tc.ipMasqBackendConfig()
}
if tc.args.cni.mac != "" {
conf += fmt.Sprintf(argsFormat, tc.args.cni.mac)
}
@ -257,6 +270,10 @@ func (tc testCase) netConfJSON(dataDir string) string {
conf += disableContainerInterface
}
if tc.portIsolation {
conf += portIsolation
}
if !tc.isLayer2 {
conf += netDefault
if tc.subnet != "" || tc.ranges != nil {
@ -295,6 +312,11 @@ func (tc testCase) ipMasqConfig() string {
return conf
}
func (tc testCase) ipMasqBackendConfig() string {
conf := fmt.Sprintf(ipMasqBackendConfStr, tc.ipMasqBackend)
return conf
}
func (tc testCase) rangesConfig() string {
conf := rangesStartStr
for i, tcRange := range tc.ranges {
@ -494,6 +516,11 @@ type (
func newTesterByVersion(version string, testNS, targetNS ns.NetNS) cmdAddDelTester {
switch {
case strings.HasPrefix(version, "1.1."):
return &testerV10x{
testNS: testNS,
targetNS: targetNS,
}
case strings.HasPrefix(version, "1.0."):
return &testerV10x{
testNS: testNS,
@ -630,6 +657,10 @@ func (tester *testerV10x) cmdAddTest(tc testCase, dataDir string) (types.Result,
Expect(link).To(BeAssignableToTypeOf(&netlink.Veth{}))
tester.vethName = result.Interfaces[1].Name
protInfo, err := netlink.LinkGetProtinfo(link)
Expect(err).NotTo(HaveOccurred())
Expect(protInfo.Isolated).To(Equal(tc.portIsolation), "link isolation should be on when portIsolation is set")
// check vlan exist on the veth interface
if tc.vlan != 0 {
interfaceMap, err := netlink.BridgeVlanList()
@ -724,7 +755,7 @@ func (tester *testerV10x) cmdAddTest(tc testCase, dataDir string) (types.Result,
continue
}
for _, route := range routes {
*found = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwIP))
*found = (ip.IsIPNetZero(route.Dst) && route.Src == nil && route.Gw.Equal(gwIP))
if *found {
break
}
@ -809,7 +840,7 @@ func (tester *testerV10x) cmdCheckTest(tc testCase, conf *Net, _ string) {
continue
}
for _, route := range routes {
*found = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwIP))
*found = (ip.IsIPNetZero(route.Dst) && route.Src == nil && route.Gw.Equal(gwIP))
if *found {
break
}
@ -1059,7 +1090,7 @@ func (tester *testerV04x) cmdAddTest(tc testCase, dataDir string) (types.Result,
continue
}
for _, route := range routes {
*found = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwIP))
*found = (ip.IsIPNetZero(route.Dst) && route.Src == nil && route.Gw.Equal(gwIP))
if *found {
break
}
@ -1143,7 +1174,7 @@ func (tester *testerV04x) cmdCheckTest(tc testCase, conf *Net, _ string) {
continue
}
for _, route := range routes {
*found = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwIP))
*found = (ip.IsIPNetZero(route.Dst) && route.Src == nil && route.Gw.Equal(gwIP))
if *found {
break
}
@ -1391,7 +1422,7 @@ func (tester *testerV03x) cmdAddTest(tc testCase, dataDir string) (types.Result,
continue
}
for _, route := range routes {
*found = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwIP))
*found = (ip.IsIPNetZero(route.Dst) && route.Src == nil && route.Gw.Equal(gwIP))
if *found {
break
}
@ -1469,6 +1500,14 @@ func (tester *testerV01xOr02x) cmdAddTest(tc testCase, dataDir string) (types.Re
err := tester.testNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
// check that STATUS is
if testutils.SpecVersionHasSTATUS(tc.cniVersion) {
err := testutils.CmdStatus(func() error {
return cmdStatus(&skel.CmdArgs{StdinData: []byte(tc.netConfJSON(dataDir))})
})
Expect(err).NotTo(HaveOccurred())
}
r, raw, err := testutils.CmdAddWithArgs(tester.args, func() error {
return cmdAdd(tester.args)
})
@ -1612,7 +1651,7 @@ func (tester *testerV01xOr02x) cmdAddTest(tc testCase, dataDir string) (types.Re
continue
}
for _, route := range routes {
*found = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwIP))
*found = (ip.IsIPNetZero(route.Dst) && route.Src == nil && route.Gw.Equal(gwIP))
if *found {
break
}
@ -1876,10 +1915,10 @@ var _ = Describe("bridge Operations", func() {
err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = BRNAME
err := netlink.LinkAdd(&netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{
Name: BRNAME,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
link, err := netlink.LinkByName(BRNAME)
@ -2390,41 +2429,82 @@ var _ = Describe("bridge Operations", func() {
})
if testutils.SpecVersionHasChaining(ver) {
It(fmt.Sprintf("[%s] configures a bridge and ipMasq rules", ver), func() {
err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
tc := testCase{
ranges: []rangeInfo{{
subnet: "10.1.2.0/24",
}},
ipMasq: true,
cniVersion: ver,
}
for _, tc := range []testCase{
{
ranges: []rangeInfo{{
subnet: "10.1.2.0/24",
}},
ipMasq: true,
cniVersion: ver,
},
{
ranges: []rangeInfo{{
subnet: "10.1.2.0/24",
}},
ipMasq: true,
ipMasqBackend: "iptables",
cniVersion: ver,
},
{
ranges: []rangeInfo{{
subnet: "10.1.2.0/24",
}},
ipMasq: true,
ipMasqBackend: "nftables",
cniVersion: ver,
},
} {
tc := tc
It(fmt.Sprintf("[%s] configures a bridge and ipMasq rules with ipMasqBackend %q", ver, tc.ipMasqBackend), func() {
err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
args := tc.createCmdArgs(originalNS, dataDir)
r, _, err := testutils.CmdAddWithArgs(args, func() error {
return cmdAdd(args)
args := tc.createCmdArgs(originalNS, dataDir)
r, _, err := testutils.CmdAddWithArgs(args, func() error {
return cmdAdd(args)
})
Expect(err).NotTo(HaveOccurred())
result, err := types100.GetResult(r)
Expect(err).NotTo(HaveOccurred())
Expect(result.IPs).Should(HaveLen(1))
ip := result.IPs[0].Address.IP.String()
// Update this if the default ipmasq backend changes
switch tc.ipMasqBackend {
case "iptables", "":
ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
Expect(err).NotTo(HaveOccurred())
rules, err := ipt.List("nat", "POSTROUTING")
Expect(err).NotTo(HaveOccurred())
Expect(rules).Should(ContainElement(ContainSubstring(ip)))
case "nftables":
nft, err := knftables.New(knftables.InetFamily, "cni_plugins_masquerade")
Expect(err).NotTo(HaveOccurred())
rules, err := nft.ListRules(context.TODO(), "masq_checks")
Expect(err).NotTo(HaveOccurred())
// FIXME: ListRules() doesn't return the actual rule strings,
// and we can't easily compute the ipmasq plugin's comment.
comments := 0
for _, r := range rules {
if r.Comment != nil {
comments++
break
}
}
Expect(comments).To(Equal(1), "expected to find exactly one Rule with a comment")
}
err = testutils.CmdDelWithArgs(args, func() error {
return cmdDel(args)
})
Expect(err).NotTo(HaveOccurred())
return nil
})
Expect(err).NotTo(HaveOccurred())
result, err := types100.GetResult(r)
Expect(err).NotTo(HaveOccurred())
Expect(result.IPs).Should(HaveLen(1))
ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
Expect(err).NotTo(HaveOccurred())
rules, err := ipt.List("nat", "POSTROUTING")
Expect(err).NotTo(HaveOccurred())
Expect(rules).Should(ContainElement(ContainSubstring(result.IPs[0].Address.IP.String())))
err = testutils.CmdDelWithArgs(args, func() error {
return cmdDel(args)
})
Expect(err).NotTo(HaveOccurred())
return nil
})
Expect(err).NotTo(HaveOccurred())
})
}
for i, tc := range []testCase{
{
@ -2520,6 +2600,36 @@ var _ = Describe("bridge Operations", func() {
return nil
})).To(Succeed())
})
It(fmt.Sprintf("[%s] when port-isolation is off, should set the veth peer on node with isolation off", ver), func() {
Expect(originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
tc := testCase{
cniVersion: ver,
portIsolation: false,
isLayer2: true,
AddErr020: "cannot convert: no valid IP addresses",
AddErr010: "cannot convert: no valid IP addresses",
}
cmdAddDelTest(originalNS, targetNS, tc, dataDir)
return nil
})).To(Succeed())
})
It(fmt.Sprintf("[%s] when port-isolation is on, should set the veth peer on node with isolation on", ver), func() {
Expect(originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
tc := testCase{
cniVersion: ver,
portIsolation: true,
isLayer2: true,
AddErr020: "cannot convert: no valid IP addresses",
AddErr010: "cannot convert: no valid IP addresses",
}
cmdAddDelTest(originalNS, targetNS, tc, dataDir)
return nil
})).To(Succeed())
})
}
It("check vlan id when loading net conf", func() {

View File

@ -43,11 +43,12 @@ func parseNetConf(bytes []byte) (*types.NetConf, error) {
func createDummy(ifName string, netns ns.NetNS) (*current.Interface, error) {
dummy := &current.Interface{}
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = ifName
linkAttrs.Namespace = netlink.NsFd(int(netns.Fd()))
dm := &netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: ifName,
Namespace: netlink.NsFd(int(netns.Fd())),
},
LinkAttrs: linkAttrs,
}
if err := netlink.LinkAdd(dm); err != nil {
@ -179,7 +180,13 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("dummy"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
Status: cmdStatus,
/* FIXME GC */
}, version.All, bv.BuildString("dummy"))
}
func cmdCheck(args *skel.CmdArgs) error {
@ -288,3 +295,16 @@ func validateCniContainerInterface(intf current.Interface) error {
return nil
}
func cmdStatus(args *skel.CmdArgs) error {
conf := types.NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("failed to load netconf: %w", err)
}
if err := ipam.ExecStatus(conf.IPAM.Type, args.StdinData); err != nil {
return err
}
return nil
}

View File

@ -106,7 +106,7 @@ type (
func newTesterByVersion(version string) tester {
switch {
case strings.HasPrefix(version, "1.0."):
case strings.HasPrefix(version, "1."):
return &testerV10x{}
case strings.HasPrefix(version, "0.4."):
return &testerV04x{}
@ -180,11 +180,11 @@ var _ = Describe("dummy Operations", func() {
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = MASTER_NAME
// Add master
err = netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: MASTER_NAME,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
m, err := netlink.LinkByName(MASTER_NAME)
@ -261,6 +261,13 @@ var _ = Describe("dummy Operations", func() {
defer GinkgoRecover()
var err error
if testutils.SpecVersionHasSTATUS(ver) {
err = testutils.CmdStatus(func() error {
return cmdStatus(args)
})
Expect(err).NotTo(HaveOccurred())
}
result, _, err = testutils.CmdAddWithArgs(args, func() error {
return cmdAdd(args)
})

View File

@ -230,118 +230,121 @@ func cmdDel(args *skel.CmdArgs) error {
return nil
}
// setTempName sets a temporary name for netdevice, returns updated Link object or error
// if occurred.
func setTempName(dev netlink.Link) (netlink.Link, error) {
tempName := fmt.Sprintf("%s%d", "temp_", dev.Attrs().Index)
// rename to tempName
if err := netlink.LinkSetName(dev, tempName); err != nil {
return nil, fmt.Errorf("failed to rename device %q to %q: %v", dev.Attrs().Name, tempName, err)
}
// Get updated Link obj
tempDev, err := netlink.LinkByName(tempName)
if err != nil {
return nil, fmt.Errorf("failed to find %q after rename to %q: %v", dev.Attrs().Name, tempName, err)
}
return tempDev, nil
}
func moveLinkIn(hostDev netlink.Link, containerNs ns.NetNS, ifName string) (netlink.Link, error) {
origLinkFlags := hostDev.Attrs().Flags
func moveLinkIn(hostDev netlink.Link, containerNs ns.NetNS, containerIfName string) (netlink.Link, error) {
hostDevName := hostDev.Attrs().Name
defaultNs, err := ns.GetCurrentNS()
// With recent kernels we could do all changes in a single netlink call,
// but on failure the device is left in a partially modified state.
// Doing changes one by one allow us to (try to) rollback to the initial state.
// Create a temporary namespace to rename (and modify) the device in.
// We were previously using a temporary name, but rapid rename leads to
// race condition with udev and NetworkManager.
tempNS, err := ns.TempNetNS()
if err != nil {
return nil, fmt.Errorf("failed to get host namespace: %v", err)
return nil, fmt.Errorf("failed to create tempNS: %v", err)
}
defer tempNS.Close()
// Devices can be renamed only when down
if err = netlink.LinkSetDown(hostDev); err != nil {
return nil, fmt.Errorf("failed to set %q down: %v", hostDev.Attrs().Name, err)
}
// restore original link state in case of error
defer func() {
if err != nil {
if origLinkFlags&net.FlagUp == net.FlagUp && hostDev != nil {
_ = netlink.LinkSetUp(hostDev)
// Restore original up state in case of error
// This must be done in the hostNS as moving
// device between namespaces sets the link down
if hostDev.Attrs().Flags&net.FlagUp == net.FlagUp {
defer func() {
if err != nil {
// lookup the device again (index might have changed)
if hostDev, err := netlink.LinkByName(hostDevName); err == nil {
_ = netlink.LinkSetUp(hostDev)
}
}
}
}()
hostDev, err = setTempName(hostDev)
if err != nil {
return nil, fmt.Errorf("failed to rename device %q to temporary name: %v", hostDevName, err)
}()
}
// restore original netdev name in case of error
defer func() {
if err != nil && hostDev != nil {
_ = netlink.LinkSetName(hostDev, hostDevName)
}
}()
if err = netlink.LinkSetNsFd(hostDev, int(containerNs.Fd())); err != nil {
return nil, fmt.Errorf("failed to move %q to container ns: %v", hostDev.Attrs().Name, err)
// Move the host device into tempNS
if err = netlink.LinkSetNsFd(hostDev, int(tempNS.Fd())); err != nil {
return nil, fmt.Errorf("failed to move %q to tempNS: %v", hostDevName, err)
}
var contDev netlink.Link
tempDevName := hostDev.Attrs().Name
if err = containerNs.Do(func(_ ns.NetNS) error {
var err error
contDev, err = netlink.LinkByName(tempDevName)
// In a container in container scenario, hostNS is not the initial net namespace,
// but host / container naming is easier to follow.
if err = tempNS.Do(func(hostNS ns.NetNS) error {
// lookup the device in tempNS (index might have changed)
tempNSDev, err := netlink.LinkByName(hostDevName)
if err != nil {
return fmt.Errorf("failed to find %q: %v", tempDevName, err)
return fmt.Errorf("failed to find %q in tempNS: %v", hostDevName, err)
}
// move netdev back to host namespace in case of error
// detroying a non empty tempNS would move physical devices back to the initial net namespace,
// not the namespace of the "parent" process, and virtual devices would be destroyed,
// so we need to actively move the device back to hostNS on error
defer func() {
if err != nil {
_ = netlink.LinkSetNsFd(contDev, int(defaultNs.Fd()))
// we need to get updated link object as link was moved back to host namepsace
_ = defaultNs.Do(func(_ ns.NetNS) error {
hostDev, _ = netlink.LinkByName(tempDevName)
return nil
})
if err != nil && tempNSDev != nil {
_ = netlink.LinkSetNsFd(tempNSDev, int(hostNS.Fd()))
}
}()
// Rename the device to the wanted name
if err = netlink.LinkSetName(tempNSDev, containerIfName); err != nil {
return fmt.Errorf("failed to rename host device %q to %q: %v", hostDevName, containerIfName, err)
}
// Restore the original device name in case of error
defer func() {
if err != nil && tempNSDev != nil {
_ = netlink.LinkSetName(tempNSDev, hostDevName)
}
}()
// Save host device name into the container device's alias property
if err = netlink.LinkSetAlias(contDev, hostDevName); err != nil {
return fmt.Errorf("failed to set alias to %q: %v", tempDevName, err)
}
// Rename container device to respect args.IfName
if err = netlink.LinkSetName(contDev, ifName); err != nil {
return fmt.Errorf("failed to rename device %q to %q: %v", tempDevName, ifName, err)
if err = netlink.LinkSetAlias(tempNSDev, hostDevName); err != nil {
return fmt.Errorf("failed to set alias to %q: %v", hostDevName, err)
}
// restore tempDevName in case of error
// Remove the alias on error
defer func() {
if err != nil {
_ = netlink.LinkSetName(contDev, tempDevName)
if err != nil && tempNSDev != nil {
_ = netlink.LinkSetAlias(tempNSDev, "")
}
}()
// Bring container device up
if err = netlink.LinkSetUp(contDev); err != nil {
return fmt.Errorf("failed to set %q up: %v", ifName, err)
// Move the device to the containerNS
if err = netlink.LinkSetNsFd(tempNSDev, int(containerNs.Fd())); err != nil {
return fmt.Errorf("failed to move %q (host: %q) to container NS: %v", containerIfName, hostDevName, err)
}
// bring device down in case of error
// Lookup the device again on error, the index might have changed
defer func() {
if err != nil {
_ = netlink.LinkSetDown(contDev)
tempNSDev, _ = netlink.LinkByName(containerIfName)
}
}()
// Retrieve link again to get up-to-date name and attributes
contDev, err = netlink.LinkByName(ifName)
if err != nil {
return fmt.Errorf("failed to find %q: %v", ifName, err)
}
return nil
err = containerNs.Do(func(_ ns.NetNS) error {
var err error
contDev, err = netlink.LinkByName(containerIfName)
if err != nil {
return fmt.Errorf("failed to find %q in container NS: %v", containerIfName, err)
}
// Move the interface back to tempNS on error
defer func() {
if err != nil {
_ = netlink.LinkSetNsFd(contDev, int(tempNS.Fd()))
}
}()
// Bring the device up
// This must be done in the containerNS
if err = netlink.LinkSetUp(contDev); err != nil {
return fmt.Errorf("failed to set %q up: %v", containerIfName, err)
}
return nil
})
return err
}); err != nil {
return nil, err
}
@ -349,78 +352,107 @@ func moveLinkIn(hostDev netlink.Link, containerNs ns.NetNS, ifName string) (netl
return contDev, nil
}
func moveLinkOut(containerNs ns.NetNS, ifName string) error {
defaultNs, err := ns.GetCurrentNS()
func moveLinkOut(containerNs ns.NetNS, containerIfName string) error {
// Create a temporary namespace to rename (and modify) the device in.
// We were previously using a temporary name, but multiple rapid renames
// leads to race condition with udev and NetworkManager.
tempNS, err := ns.TempNetNS()
if err != nil {
return err
return fmt.Errorf("failed to create tempNS: %v", err)
}
defer defaultNs.Close()
defer tempNS.Close()
var tempName string
var origDev netlink.Link
err = containerNs.Do(func(_ ns.NetNS) error {
dev, err := netlink.LinkByName(ifName)
if err != nil {
return fmt.Errorf("failed to find %q: %v", ifName, err)
}
origDev = dev
var contDev netlink.Link
// Devices can be renamed only when down
if err = netlink.LinkSetDown(dev); err != nil {
return fmt.Errorf("failed to set %q down: %v", ifName, err)
}
defer func() {
// If moving the device to the host namespace fails, set its name back to ifName so that this
// function can be retried. Also bring the device back up, unless it was already down before.
if err != nil {
_ = netlink.LinkSetName(dev, ifName)
if dev.Attrs().Flags&net.FlagUp == net.FlagUp {
_ = netlink.LinkSetUp(dev)
}
}
}()
newLink, err := setTempName(dev)
if err != nil {
return fmt.Errorf("failed to rename device %q to temporary name: %v", ifName, err)
}
dev = newLink
tempName = dev.Attrs().Name
if err = netlink.LinkSetNsFd(dev, int(defaultNs.Fd())); err != nil {
return fmt.Errorf("failed to move %q to host netns: %v", tempName, err)
}
return nil
})
if err != nil {
return err
}
// Rename the device to its original name from the host namespace
tempDev, err := netlink.LinkByName(tempName)
if err != nil {
return fmt.Errorf("failed to find %q in host namespace: %v", tempName, err)
}
if err = netlink.LinkSetName(tempDev, tempDev.Attrs().Alias); err != nil {
// move device back to container ns so it may be retired
defer func() {
_ = netlink.LinkSetNsFd(tempDev, int(containerNs.Fd()))
_ = containerNs.Do(func(_ ns.NetNS) error {
lnk, err := netlink.LinkByName(tempName)
if err != nil {
return err
}
_ = netlink.LinkSetName(lnk, ifName)
if origDev.Attrs().Flags&net.FlagUp == net.FlagUp {
_ = netlink.LinkSetUp(lnk)
// Restore original up state in case of error
// This must be done in the containerNS as moving
// device between namespaces sets the link down
defer func() {
if err != nil && contDev != nil && contDev.Attrs().Flags&net.FlagUp == net.FlagUp {
containerNs.Do(func(_ ns.NetNS) error {
// lookup the device again (index might have changed)
if contDev, err := netlink.LinkByName(containerIfName); err == nil {
_ = netlink.LinkSetUp(contDev)
}
return nil
})
}
}()
err = containerNs.Do(func(_ ns.NetNS) error {
var err error
// Lookup the device in the containerNS
contDev, err = netlink.LinkByName(containerIfName)
if err != nil {
return fmt.Errorf("failed to find %q in containerNS: %v", containerIfName, err)
}
// Verify we have the original name
if contDev.Attrs().Alias == "" {
return fmt.Errorf("failed to find original ifname for %q (alias is not set)", containerIfName)
}
// Move the device to the tempNS
if err = netlink.LinkSetNsFd(contDev, int(tempNS.Fd())); err != nil {
return fmt.Errorf("failed to move %q to tempNS: %v", containerIfName, err)
}
return nil
})
if err != nil {
return err
}
err = tempNS.Do(func(hostNS ns.NetNS) error {
// Lookup the device in tempNS (index might have changed)
tempNSDev, err := netlink.LinkByName(containerIfName)
if err != nil {
return fmt.Errorf("failed to find %q in tempNS: %v", containerIfName, err)
}
// Move the device back to containerNS on error
defer func() {
if err != nil {
_ = netlink.LinkSetNsFd(tempNSDev, int(containerNs.Fd()))
}
}()
return fmt.Errorf("failed to restore %q to original name %q: %v", tempName, tempDev.Attrs().Alias, err)
hostDevName := tempNSDev.Attrs().Alias
// Rename container device to hostDevName
if err = netlink.LinkSetName(tempNSDev, hostDevName); err != nil {
return fmt.Errorf("failed to rename device %q to %q: %v", containerIfName, hostDevName, err)
}
// Rename the device back to containerIfName on error
defer func() {
if err != nil {
_ = netlink.LinkSetName(tempNSDev, containerIfName)
}
}()
// Unset device's alias property
if err = netlink.LinkSetAlias(tempNSDev, ""); err != nil {
return fmt.Errorf("failed to unset alias of %q: %v", hostDevName, err)
}
// Set back the device alias to hostDevName on error
defer func() {
if err != nil {
_ = netlink.LinkSetAlias(tempNSDev, hostDevName)
}
}()
// Finally move the device to the hostNS
if err = netlink.LinkSetNsFd(tempNSDev, int(hostNS.Fd())); err != nil {
return fmt.Errorf("failed to move %q to hostNS: %v", hostDevName, err)
}
// As we don't know the previous state, leave the link down
return nil
})
if err != nil {
return err
}
return nil
@ -518,7 +550,13 @@ func getLink(devname, hwaddr, kernelpath, pciaddr string, auxDev string) (netlin
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("host-device"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
Status: cmdStatus,
/* FIXME GC */
}, version.All, bv.BuildString("host-device"))
}
func cmdCheck(args *skel.CmdArgs) error {
@ -625,3 +663,20 @@ func validateCniContainerInterface(intf current.Interface) error {
return nil
}
func cmdStatus(args *skel.CmdArgs) error {
conf := NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("failed to load netconf: %w", err)
}
if conf.IPAM.Type != "" {
if err := ipam.ExecStatus(conf.IPAM.Type, args.StdinData); err != nil {
return err
}
}
// TODO: Check if host device exists.
return nil
}

View File

@ -231,7 +231,7 @@ type (
func newTesterByVersion(version string) tester {
switch {
case strings.HasPrefix(version, "1.0."):
case strings.HasPrefix(version, "1."):
return &testerV10x{}
case strings.HasPrefix(version, "0.4."):
return &testerV04x{}
@ -341,10 +341,10 @@ var _ = Describe("base functionality", func() {
// prepare ifname in original namespace
_ = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = ifname
err := netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: ifname,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
origLink, err = netlink.LinkByName(ifname)
@ -362,6 +362,15 @@ var _ = Describe("base functionality", func() {
"type": "host-device",
"device": %q
}`, ver, ifname)
// if v1.1 or greater, call CmdStatus
if testutils.SpecVersionHasSTATUS(ver) {
err := testutils.CmdStatus(func() error {
return cmdStatus(&skel.CmdArgs{StdinData: []byte(conf)})
})
Expect(err).NotTo(HaveOccurred())
}
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: targetNS.Path(),
@ -422,10 +431,10 @@ var _ = Describe("base functionality", func() {
// prepare host device in original namespace
_ = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = ifname
err := netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: ifname,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
origLink, err = netlink.LinkByName(ifname)
@ -483,10 +492,10 @@ var _ = Describe("base functionality", func() {
// create another conflict host device with same name "dummy0"
_ = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = ifname
err := netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: ifname,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
conflictLink, err = netlink.LinkByName(ifname)
@ -608,10 +617,10 @@ var _ = Describe("base functionality", func() {
// prepare ifname in original namespace
_ = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = ifname
err := netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: ifname,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
origLink, err = netlink.LinkByName(ifname)
@ -720,10 +729,10 @@ var _ = Describe("base functionality", func() {
// prepare ifname in original namespace
_ = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = ifname
err := netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: ifname,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
origLink, err = netlink.LinkByName(ifname)
@ -912,10 +921,10 @@ var _ = Describe("base functionality", func() {
// prepare ifname in original namespace
_ = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = ifname
err := netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: ifname,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
origLink, err = netlink.LinkByName(ifname)
@ -969,10 +978,10 @@ var _ = Describe("base functionality", func() {
// prepare ifname in original namespace
_ = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = ifname
err := netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: ifname,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
origLink, err = netlink.LinkByName(ifname)
@ -1093,10 +1102,10 @@ var _ = Describe("base functionality", func() {
// prepare host device in original namespace
_ = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = ifname
err := netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: ifname,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
origLink, err = netlink.LinkByName(ifname)
@ -1154,10 +1163,10 @@ var _ = Describe("base functionality", func() {
// create another conflict host device with same name "dummy0"
_ = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = ifname
err := netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: ifname,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
conflictLink, err = netlink.LinkByName(ifname)
@ -1227,10 +1236,10 @@ var _ = Describe("base functionality", func() {
// prepare host device in original namespace
_ = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = hostIfname
err := netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: hostIfname,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
origLink, err = netlink.LinkByName(hostIfname)
@ -1243,10 +1252,10 @@ var _ = Describe("base functionality", func() {
// prepare device in container namespace with same name as host device
_ = targetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = containerAdditionalIfname
err := netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: containerAdditionalIfname,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
containerLink, err = netlink.LinkByName(containerAdditionalIfname)

View File

@ -144,14 +144,15 @@ func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interf
return nil, err
}
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.MTU = conf.MTU
linkAttrs.Name = tmpName
linkAttrs.ParentIndex = m.Attrs().Index
linkAttrs.Namespace = netlink.NsFd(int(netns.Fd()))
mv := &netlink.IPVlan{
LinkAttrs: netlink.LinkAttrs{
MTU: conf.MTU,
Name: tmpName,
ParentIndex: m.Attrs().Index,
Namespace: netlink.NsFd(int(netns.Fd())),
},
Mode: mode,
LinkAttrs: linkAttrs,
Mode: mode,
}
if conf.LinkContNs {
@ -195,7 +196,7 @@ func getDefaultRouteInterfaceName() (string, error) {
}
for _, v := range routeToDstIP {
if v.Dst == nil {
if ip.IsIPNetZero(v.Dst) {
l, err := netlink.LinkByIndex(v.LinkIndex)
if err != nil {
return "", err
@ -349,7 +350,13 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("ipvlan"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
Status: cmdStatus,
/* FIXME GC */
}, version.All, bv.BuildString("ipvlan"))
}
func cmdCheck(args *skel.CmdArgs) error {
@ -484,3 +491,19 @@ func validateCniContainerInterface(intf current.Interface, modeExpected string)
return nil
}
func cmdStatus(args *skel.CmdArgs) error {
conf := NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("failed to load netconf: %w", err)
}
if conf.IPAM.Type != "" {
if err := ipam.ExecStatus(conf.IPAM.Type, args.StdinData); err != nil {
return err
}
}
// TODO: Check if master interface exists.
return nil
}

View File

@ -114,6 +114,13 @@ func ipvlanAddCheckDelTest(conf, masterName string, originalNS, targetNS ns.NetN
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
if testutils.SpecVersionHasSTATUS(cniVersion) {
err = testutils.CmdStatus(func() error {
return cmdStatus(args)
})
Expect(err).NotTo(HaveOccurred())
}
result, _, err = testutils.CmdAddWithArgs(args, func() error {
return cmdAdd(args)
})
@ -214,7 +221,7 @@ type (
func newTesterByVersion(version string) tester {
switch {
case strings.HasPrefix(version, "1.0."):
case strings.HasPrefix(version, "1."):
return &testerV10x{}
case strings.HasPrefix(version, "0.4.") || strings.HasPrefix(version, "0.3."):
return &testerV04x{}
@ -281,11 +288,11 @@ var _ = Describe("ipvlan Operations", func() {
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = MASTER_NAME
// Add master
err = netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: MASTER_NAME,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
_, err = netlink.LinkByName(MASTER_NAME)
@ -297,11 +304,11 @@ var _ = Describe("ipvlan Operations", func() {
err = targetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = MASTER_NAME_INCONTAINER
// Add master
err = netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: MASTER_NAME_INCONTAINER,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
_, err = netlink.LinkByName(MASTER_NAME_INCONTAINER)

View File

@ -172,7 +172,13 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("loopback"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
/* FIXME GC */
/* FIXME Status */
}, version.All, bv.BuildString("loopback"))
}
func cmdCheck(args *skel.CmdArgs) error {

View File

@ -74,7 +74,8 @@ var _ = Describe("Loopback", func() {
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())
Eventually(session).Should(gbytes.Say(`{.*}`))
// "(?s)" turns on the "s" flag, making "." match newlines too.
Eventually(session).Should(gbytes.Say(`(?s){.*}`))
Eventually(session).Should(gexec.Exit(0))
var lo *net.Interface

View File

@ -41,6 +41,7 @@ type NetConf struct {
MTU int `json:"mtu"`
Mac string `json:"mac,omitempty"`
LinkContNs bool `json:"linkInContainer,omitempty"`
BcQueueLen uint32 `json:"bcqueuelen,omitempty"`
RuntimeConfig struct {
Mac string `json:"mac,omitempty"`
@ -67,7 +68,7 @@ func getDefaultRouteInterfaceName() (string, error) {
}
for _, v := range routeToDstIP {
if v.Dst == nil {
if ip.IsIPNetZero(v.Dst) {
l, err := netlink.LinkByIndex(v.LinkIndex)
if err != nil {
return "", err
@ -225,12 +226,11 @@ func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Inter
return nil, err
}
linkAttrs := netlink.LinkAttrs{
MTU: conf.MTU,
Name: tmpName,
ParentIndex: m.Attrs().Index,
Namespace: netlink.NsFd(int(netns.Fd())),
}
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.MTU = conf.MTU
linkAttrs.Name = tmpName
linkAttrs.ParentIndex = m.Attrs().Index
linkAttrs.Namespace = netlink.NsFd(int(netns.Fd()))
if conf.Mac != "" {
addr, err := net.ParseMAC(conf.Mac)
@ -245,6 +245,8 @@ func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Inter
Mode: mode,
}
mv.BCQueueLen = conf.BcQueueLen
if conf.LinkContNs {
err = netns.Do(func(_ ns.NetNS) error {
return netlink.LinkAdd(mv)
@ -426,7 +428,13 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("macvlan"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
Status: cmdStatus,
/* FIXME GC */
}, version.All, bv.BuildString("macvlan"))
}
func cmdCheck(args *skel.CmdArgs) error {
@ -562,3 +570,20 @@ func validateCniContainerInterface(intf current.Interface, modeExpected string)
return nil
}
func cmdStatus(args *skel.CmdArgs) error {
conf := NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("failed to load netconf: %w", err)
}
if conf.IPAM.Type != "" {
if err := ipam.ExecStatus(conf.IPAM.Type, args.StdinData); err != nil {
return err
}
}
// TODO: Check if master interface exists.
return nil
}

View File

@ -117,7 +117,7 @@ type (
func newTesterByVersion(version string) tester {
switch {
case strings.HasPrefix(version, "1.0."):
case strings.HasPrefix(version, "1."):
return &testerV10x{}
case strings.HasPrefix(version, "0.4."):
return &testerV04x{}
@ -208,11 +208,11 @@ var _ = Describe("macvlan Operations", func() {
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = MASTER_NAME
// Add master
err = netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: MASTER_NAME,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
_, err = netlink.LinkByName(MASTER_NAME)
@ -224,11 +224,11 @@ var _ = Describe("macvlan Operations", func() {
err = targetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = MASTER_NAME_INCONTAINER
// Add master
err = netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: MASTER_NAME_INCONTAINER,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
_, err = netlink.LinkByName(MASTER_NAME_INCONTAINER)
@ -322,6 +322,13 @@ var _ = Describe("macvlan Operations", func() {
err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
if testutils.SpecVersionHasSTATUS(ver) {
err := testutils.CmdStatus(func() error {
return cmdStatus(args)
})
Expect(err).NotTo(HaveOccurred())
}
result, _, err := testutils.CmdAddWithArgs(args, func() error {
return cmdAdd(args)
})
@ -434,6 +441,13 @@ var _ = Describe("macvlan Operations", func() {
err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
if testutils.SpecVersionHasSTATUS(ver) {
err := testutils.CmdStatus(func() error {
return cmdStatus(args)
})
Expect(err).NotTo(HaveOccurred())
}
result, _, err := testutils.CmdAddWithArgs(args, func() error {
return cmdAdd(args)
})
@ -520,6 +534,13 @@ var _ = Describe("macvlan Operations", func() {
defer GinkgoRecover()
var err error
if testutils.SpecVersionHasSTATUS(ver) {
err := testutils.CmdStatus(func() error {
return cmdStatus(args)
})
Expect(err).NotTo(HaveOccurred())
}
result, _, err = testutils.CmdAddWithArgs(args, func() error {
return cmdAdd(args)
})
@ -660,6 +681,13 @@ var _ = Describe("macvlan Operations", func() {
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
if testutils.SpecVersionHasSTATUS(ver) {
err := testutils.CmdStatus(func() error {
return cmdStatus(args)
})
Expect(err).NotTo(HaveOccurred())
}
result, _, err := testutils.CmdAddWithArgs(args, func() error {
return cmdAdd(args)
})

View File

@ -31,7 +31,6 @@ import (
"github.com/containernetworking/plugins/pkg/ip"
"github.com/containernetworking/plugins/pkg/ipam"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containernetworking/plugins/pkg/utils"
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
)
@ -44,8 +43,9 @@ func init() {
type NetConf struct {
types.NetConf
IPMasq bool `json:"ipMasq"`
MTU int `json:"mtu"`
IPMasq bool `json:"ipMasq"`
IPMasqBackend *string `json:"ipMasqBackend,omitempty"`
MTU int `json:"mtu"`
}
func setupContainerVeth(netns ns.NetNS, ifName string, mtu int, pr *current.Result) (*current.Interface, *current.Interface, error) {
@ -229,12 +229,12 @@ func cmdAdd(args *skel.CmdArgs) error {
}
if conf.IPMasq {
chain := utils.FormatChainName(conf.Name, args.ContainerID)
comment := utils.FormatComment(conf.Name, args.ContainerID)
ipns := []*net.IPNet{}
for _, ipc := range result.IPs {
if err = ip.SetupIPMasq(&ipc.Address, chain, comment); err != nil {
return err
}
ipns = append(ipns, &ipc.Address)
}
if err = ip.SetupIPMasqForNetworks(conf.IPMasqBackend, ipns, conf.Name, args.IfName, args.ContainerID); err != nil {
return err
}
}
@ -293,10 +293,8 @@ func cmdDel(args *skel.CmdArgs) error {
}
if len(ipnets) != 0 && conf.IPMasq {
chain := utils.FormatChainName(conf.Name, args.ContainerID)
comment := utils.FormatComment(conf.Name, args.ContainerID)
for _, ipn := range ipnets {
err = ip.TeardownIPMasq(ipn, chain, comment)
if err := ip.TeardownIPMasqForNetworks(ipnets, conf.Name, args.IfName, args.ContainerID); err != nil {
return err
}
}
@ -304,7 +302,13 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("ptp"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
Status: cmdStatus,
/* FIXME GC */
}, version.All, bv.BuildString("ptp"))
}
func cmdCheck(args *skel.CmdArgs) error {
@ -407,3 +411,16 @@ func validateCniContainerInterface(intf current.Interface) error {
return nil
}
func cmdStatus(args *skel.CmdArgs) error {
conf := NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("failed to load netconf: %w", err)
}
if err := ipam.ExecStatus(conf.IPAM.Type, args.StdinData); err != nil {
return err
}
return nil
}

View File

@ -39,6 +39,7 @@ type Net struct {
CNIVersion string `json:"cniVersion"`
Type string `json:"type,omitempty"`
IPMasq bool `json:"ipMasq"`
IPMasqBackend *string `json:"ipMasqBackend,omitempty"`
MTU int `json:"mtu"`
IPAM *allocator.IPAMConfig `json:"ipam"`
DNS types.DNS `json:"dns"`
@ -104,7 +105,7 @@ type (
func newTesterByVersion(version string) tester {
switch {
case strings.HasPrefix(version, "1.0."):
case strings.HasPrefix(version, "1."):
return &testerV10x{}
case strings.HasPrefix(version, "0.4."):
return &testerV04x{}
@ -249,6 +250,14 @@ var _ = Describe("ptp Operations", func() {
defer GinkgoRecover()
var err error
if testutils.SpecVersionHasSTATUS(cniVersion) {
By("Doing a cni STATUS")
err = testutils.CmdStatus(func() error {
return cmdStatus(args)
})
Expect(err).NotTo(HaveOccurred())
}
result, _, err = testutils.CmdAddWithArgs(args, func() error {
return cmdAdd(args)
})
@ -368,6 +377,62 @@ var _ = Describe("ptp Operations", func() {
doTest(conf, ver, 1, dnsConf, targetNS)
})
It(fmt.Sprintf("[%s] configures and deconfigures a ptp link when specifying ipMasqBackend: iptables", ver), func() {
dnsConf := types.DNS{
Nameservers: []string{"10.1.2.123"},
Domain: "some.domain.test",
Search: []string{"search.test"},
Options: []string{"option1:foo"},
}
dnsConfBytes, err := json.Marshal(dnsConf)
Expect(err).NotTo(HaveOccurred())
conf := fmt.Sprintf(`{
"cniVersion": "%s",
"name": "mynet",
"type": "ptp",
"ipMasq": true,
"ipMasqBackend": "iptables",
"mtu": 5000,
"ipam": {
"type": "host-local",
"subnet": "10.1.2.0/24",
"dataDir": "%s"
},
"dns": %s
}`, ver, dataDir, string(dnsConfBytes))
doTest(conf, ver, 1, dnsConf, targetNS)
})
It(fmt.Sprintf("[%s] configures and deconfigures a ptp link when specifying ipMasqBackend: nftables", ver), func() {
dnsConf := types.DNS{
Nameservers: []string{"10.1.2.123"},
Domain: "some.domain.test",
Search: []string{"search.test"},
Options: []string{"option1:foo"},
}
dnsConfBytes, err := json.Marshal(dnsConf)
Expect(err).NotTo(HaveOccurred())
conf := fmt.Sprintf(`{
"cniVersion": "%s",
"name": "mynet",
"type": "ptp",
"ipMasq": true,
"ipMasqBackend": "nftables",
"mtu": 5000,
"ipam": {
"type": "host-local",
"subnet": "10.1.2.0/24",
"dataDir": "%s"
},
"dns": %s
}`, ver, dataDir, string(dnsConfBytes))
doTest(conf, ver, 1, dnsConf, targetNS)
})
It(fmt.Sprintf("[%s] configures and deconfigures a dual-stack ptp link + routes with ADD/DEL", ver), func() {
conf := fmt.Sprintf(`{
"cniVersion": "%s",

View File

@ -137,10 +137,9 @@ func createTapWithIptool(tmpName string, mtu int, multiqueue bool, mac string, o
}
func createLinkWithNetlink(tmpName string, mtu int, nsFd int, multiqueue bool, mac string, owner *uint32, group *uint32) error {
linkAttrs := netlink.LinkAttrs{
Name: tmpName,
Namespace: netlink.NsFd(nsFd),
}
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = tmpName
linkAttrs.Namespace = netlink.NsFd(nsFd)
if mtu != 0 {
linkAttrs.MTU = mtu
}
@ -386,7 +385,13 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("tap"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
Status: cmdStatus,
/* FIXME GC */
}, version.All, bv.BuildString("tap"))
}
func cmdCheck(args *skel.CmdArgs) error {
@ -455,3 +460,18 @@ func cmdCheck(args *skel.CmdArgs) error {
return nil
})
}
func cmdStatus(args *skel.CmdArgs) error {
conf := NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("failed to load netconf: %w", err)
}
if conf.IPAM.Type != "" {
if err := ipam.ExecStatus(conf.IPAM.Type, args.StdinData); err != nil {
return err
}
}
return nil
}

View File

@ -108,7 +108,7 @@ type (
func newTesterByVersion(version string) tester {
switch {
case strings.HasPrefix(version, "1.0."):
case strings.HasPrefix(version, "1."):
return &testerV10x{}
case strings.HasPrefix(version, "0.4."):
return &testerV04x{}
@ -223,6 +223,13 @@ var _ = Describe("Add, check, remove tap plugin", func() {
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
if testutils.SpecVersionHasSTATUS(ver) {
err := testutils.CmdStatus(func() error {
return cmdStatus(args)
})
Expect(err).NotTo(HaveOccurred())
}
result, _, err = testutils.CmdAddWithArgs(args, func() error {
return cmdAdd(args)
})
@ -346,10 +353,10 @@ var _ = Describe("Add, check, remove tap plugin", func() {
Expect(
targetNS.Do(func(ns.NetNS) error {
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = bridgeName
if err := netlink.LinkAdd(&netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{
Name: bridgeName,
},
LinkAttrs: linkAttrs,
}); err != nil {
return err
}
@ -364,6 +371,13 @@ var _ = Describe("Add, check, remove tap plugin", func() {
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
if testutils.SpecVersionHasSTATUS(ver) {
err := testutils.CmdStatus(func() error {
return cmdStatus(args)
})
Expect(err).NotTo(HaveOccurred())
}
result, _, err = testutils.CmdAddWithArgs(args, func() error {
return cmdAdd(args)
})

View File

@ -119,14 +119,15 @@ func createVlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interfac
return nil, err
}
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.MTU = conf.MTU
linkAttrs.Name = tmpName
linkAttrs.ParentIndex = m.Attrs().Index
linkAttrs.Namespace = netlink.NsFd(int(netns.Fd()))
v := &netlink.Vlan{
LinkAttrs: netlink.LinkAttrs{
MTU: conf.MTU,
Name: tmpName,
ParentIndex: m.Attrs().Index,
Namespace: netlink.NsFd(int(netns.Fd())),
},
VlanId: conf.VlanID,
LinkAttrs: linkAttrs,
VlanId: conf.VlanID,
}
if conf.LinkContNs {
@ -259,7 +260,13 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("vlan"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
Status: cmdStatus,
/* FIXME GC */
}, version.All, bv.BuildString("vlan"))
}
func cmdCheck(args *skel.CmdArgs) error {
@ -392,3 +399,18 @@ func validateCniContainerInterface(intf current.Interface, vlanID int, mtu int)
return nil
}
func cmdStatus(args *skel.CmdArgs) error {
conf := NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("failed to load netconf: %w", err)
}
if err := ipam.ExecStatus(conf.IPAM.Type, args.StdinData); err != nil {
return err
}
// TODO: Check if master interface exists.
return nil
}

View File

@ -113,7 +113,7 @@ type (
func newTesterByVersion(version string) tester {
switch {
case strings.HasPrefix(version, "1.0."):
case strings.HasPrefix(version, "1."):
return &testerV10x{}
case strings.HasPrefix(version, "0.4."):
return &testerV04x{}
@ -187,11 +187,11 @@ var _ = Describe("vlan Operations", func() {
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = MASTER_NAME
// Add master
err = netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: MASTER_NAME,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
m, err := netlink.LinkByName(MASTER_NAME)
@ -205,11 +205,11 @@ var _ = Describe("vlan Operations", func() {
err = targetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = MASTER_NAME_INCONTAINER
// Add master
err = netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: MASTER_NAME_INCONTAINER,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
m, err := netlink.LinkByName(MASTER_NAME_INCONTAINER)
@ -354,6 +354,13 @@ var _ = Describe("vlan Operations", func() {
defer GinkgoRecover()
var err error
if testutils.SpecVersionHasSTATUS(ver) {
err := testutils.CmdStatus(func() error {
return cmdStatus(args)
})
Expect(err).NotTo(HaveOccurred())
}
result, _, err = testutils.CmdAddWithArgs(args, func() error {
return cmdAdd(args)
})

View File

@ -215,5 +215,26 @@ func cmdCheck(_ *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("win-bridge"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
Status: cmdStatus,
/* FIXME GC */
}, version.All, bv.BuildString("win-bridge"))
}
func cmdStatus(args *skel.CmdArgs) error {
conf := NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("failed to load netconf: %w", err)
}
if conf.IPAM.Type != "" {
if err := ipam.ExecStatus(conf.IPAM.Type, args.StdinData); err != nil {
return err
}
}
return nil
}

View File

@ -106,13 +106,13 @@ func cmdHcnAdd(args *skel.CmdArgs, n *NetConf) (*current.Result, error) {
return nil, errors.Annotatef(err, "error while hcn.GetNetworkByName(%s)", networkName)
}
if hcnNetwork == nil {
return nil, fmt.Errorf("network %v is not found", networkName)
return nil, fmt.Errorf("network %v is not found", networkName)
}
if hnsNetwork == nil {
return nil, fmt.Errorf("network %v not found", networkName)
}
if !strings.EqualFold(string (hcnNetwork.Type), "Overlay") {
if !strings.EqualFold(string(hcnNetwork.Type), "Overlay") {
return nil, fmt.Errorf("network %v is of an unexpected type: %v", networkName, hcnNetwork.Type)
}
@ -131,7 +131,6 @@ func cmdHcnAdd(args *skel.CmdArgs, n *NetConf) (*current.Result, error) {
n.ApplyOutboundNatPolicy(hnsNetwork.Subnets[0].AddressPrefix)
}
hcnEndpoint, err := hns.GenerateHcnEndpoint(epInfo, &n.NetConf)
if err != nil {
return nil, errors.Annotate(err, "error while generating HostComputeEndpoint")
}
@ -142,15 +141,14 @@ func cmdHcnAdd(args *skel.CmdArgs, n *NetConf) (*current.Result, error) {
}
result, err := hns.ConstructHcnResult(hcnNetwork, hcnEndpoint)
if err != nil {
ipam.ExecDel(n.IPAM.Type, args.StdinData)
return nil, errors.Annotate(err, "error while constructing HostComputeEndpoint addition result")
}
return result, nil
}
func cmdHnsAdd(args *skel.CmdArgs, n *NetConf) (*current.Result, error) {
success := false
@ -243,6 +241,7 @@ func cmdHnsAdd(args *skel.CmdArgs, n *NetConf) (*current.Result, error) {
success = true
return result, nil
}
func cmdAdd(args *skel.CmdArgs) error {
n, cniVersion, err := loadNetConf(args.StdinData)
if err != nil {
@ -288,5 +287,24 @@ func cmdCheck(_ *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("win-overlay"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
Status: cmdStatus,
/* FIXME GC */
}, version.All, bv.BuildString("win-overlay"))
}
func cmdStatus(args *skel.CmdArgs) error {
conf := NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("failed to load netconf: %w", err)
}
if err := ipam.ExecStatus(conf.IPAM.Type, args.StdinData); err != nil {
return err
}
return nil
}

View File

@ -1,563 +0,0 @@
// Copyright 2023 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"
"net"
"syscall"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/vishvananda/netlink"
"github.com/containernetworking/cni/pkg/skel"
types100 "github.com/containernetworking/cni/pkg/types/100"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containernetworking/plugins/pkg/testutils"
)
var _ = Describe("bandwidth config test", func() {
var (
hostNs ns.NetNS
containerNs ns.NetNS
ifbDeviceName string
hostIfname string
containerIfname string
hostIP net.IP
containerIP net.IP
hostIfaceMTU int
)
BeforeEach(func() {
var err error
hostIfname = "host-veth"
containerIfname = "container-veth"
hostNs, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
containerNs, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
hostIP = net.IP{169, 254, 0, 1}
containerIP = net.IP{10, 254, 0, 1}
hostIfaceMTU = 1024
ifbDeviceName = "bwpa8eda89404b7"
createVeth(hostNs, hostIfname, containerNs, containerIfname, hostIP, containerIP, hostIfaceMTU)
})
AfterEach(func() {
Expect(containerNs.Close()).To(Succeed())
Expect(testutils.UnmountNS(containerNs)).To(Succeed())
Expect(hostNs.Close()).To(Succeed())
Expect(testutils.UnmountNS(hostNs)).To(Succeed())
})
// Bandwidth requires host-side interface info, and thus only
// supports 0.3.0 and later CNI versions
for _, ver := range []string{"0.3.0", "0.3.1", "0.4.0", "1.0.0"} {
// Redefine ver inside for scope so real value is picked up by each dynamically defined It()
// See Gingkgo's "Patterns for dynamically generating tests" documentation.
ver := ver
Describe("cmdADD", func() {
It(fmt.Sprintf("[%s] fails with invalid UnshapedSubnets", ver), func() {
conf := fmt.Sprintf(`{
"cniVersion": "%s",
"name": "cni-plugin-bandwidth-test",
"type": "bandwidth",
"ingressRate": 123,
"ingressBurst": 123,
"egressRate": 123,
"egressBurst": 123,
"unshapedSubnets": ["10.0.0.0/8", "hello"],
"prevResult": {
"interfaces": [
{
"name": "%s",
"sandbox": ""
},
{
"name": "%s",
"sandbox": "%s"
}
],
"ips": [
{
"version": "4",
"address": "%s/24",
"gateway": "10.0.0.1",
"interface": 1
}
],
"routes": []
}
}`, ver, hostIfname, containerIfname, containerNs.Path(), containerIP.String())
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: containerNs.Path(),
IfName: "eth0",
StdinData: []byte(conf),
}
Expect(hostNs.Do(func(netNS ns.NetNS) error {
defer GinkgoRecover()
_, _, err := testutils.CmdAdd(containerNs.Path(), args.ContainerID, "", []byte(conf), func() error { return cmdAdd(args) })
Expect(err).To(MatchError("bad subnet \"hello\" provided, details invalid CIDR address: hello"))
return nil
})).To(Succeed())
})
It(fmt.Sprintf("[%s] fails with invalid ShapedSubnets", ver), func() {
conf := fmt.Sprintf(`{
"cniVersion": "%s",
"name": "cni-plugin-bandwidth-test",
"type": "bandwidth",
"ingressRate": 123,
"ingressBurst": 123,
"egressRate": 123,
"egressBurst": 123,
"ShapedSubnets": ["10.0.0.0/8", "hello"],
"prevResult": {
"interfaces": [
{
"name": "%s",
"sandbox": ""
},
{
"name": "%s",
"sandbox": "%s"
}
],
"ips": [
{
"version": "4",
"address": "%s/24",
"gateway": "10.0.0.1",
"interface": 1
}
],
"routes": []
}
}`, ver, hostIfname, containerIfname, containerNs.Path(), containerIP.String())
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: containerNs.Path(),
IfName: "eth0",
StdinData: []byte(conf),
}
Expect(hostNs.Do(func(netNS ns.NetNS) error {
defer GinkgoRecover()
_, _, err := testutils.CmdAdd(containerNs.Path(), args.ContainerID, "", []byte(conf), func() error { return cmdAdd(args) })
Expect(err).To(MatchError("bad subnet \"hello\" provided, details invalid CIDR address: hello"))
return nil
})).To(Succeed())
})
It(fmt.Sprintf("[%s] fails with both ShapedSubnets and UnshapedSubnets specified", ver), func() {
conf := fmt.Sprintf(`{
"cniVersion": "%s",
"name": "cni-plugin-bandwidth-test",
"type": "bandwidth",
"ingressRate": 123,
"ingressBurst": 123,
"egressRate": 123,
"egressBurst": 123,
"shapedSubnets": ["10.0.0.0/8"],
"unshapedSubnets": ["192.168.0.0/16"],
"prevResult": {
"interfaces": [
{
"name": "%s",
"sandbox": ""
},
{
"name": "%s",
"sandbox": "%s"
}
],
"ips": [
{
"version": "4",
"address": "%s/24",
"gateway": "10.0.0.1",
"interface": 1
}
],
"routes": []
}
}`, ver, hostIfname, containerIfname, containerNs.Path(), containerIP.String())
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: containerNs.Path(),
IfName: "eth0",
StdinData: []byte(conf),
}
Expect(hostNs.Do(func(netNS ns.NetNS) error {
defer GinkgoRecover()
_, _, err := testutils.CmdAdd(containerNs.Path(), args.ContainerID, "", []byte(conf), func() error { return cmdAdd(args) })
Expect(err).To(MatchError("unshapedSubnets and shapedSubnets cannot be both specified, one of them should be discarded"))
return nil
})).To(Succeed())
})
It(fmt.Sprintf("[%s] fails an invalid ingress config", ver), func() {
conf := fmt.Sprintf(`{
"cniVersion": "%s",
"name": "cni-plugin-bandwidth-test",
"type": "bandwidth",
"ingressRate": 0,
"ingressBurst": 123,
"egressRate": 123,
"egressBurst": 123,
"prevResult": {
"interfaces": [
{
"name": "%s",
"sandbox": ""
},
{
"name": "%s",
"sandbox": "%s"
}
],
"ips": [
{
"version": "4",
"address": "%s/24",
"gateway": "10.0.0.1",
"interface": 1
}
],
"routes": []
}
}`, ver, hostIfname, containerIfname, containerNs.Path(), containerIP.String())
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: containerNs.Path(),
IfName: "eth0",
StdinData: []byte(conf),
}
Expect(hostNs.Do(func(netNS ns.NetNS) error {
defer GinkgoRecover()
_, _, err := testutils.CmdAdd(containerNs.Path(), args.ContainerID, "", []byte(conf), func() error { return cmdAdd(args) })
Expect(err).To(MatchError("if burst is set, rate must also be set"))
return nil
})).To(Succeed())
})
It(fmt.Sprintf("[%s] fails an invalid egress config", ver), func() {
conf := fmt.Sprintf(`{
"cniVersion": "%s",
"name": "cni-plugin-bandwidth-test",
"type": "bandwidth",
"ingressRate": 123,
"ingressBurst": 123,
"egressRate": 0,
"egressBurst": 123,
"prevResult": {
"interfaces": [
{
"name": "%s",
"sandbox": ""
},
{
"name": "%s",
"sandbox": "%s"
}
],
"ips": [
{
"version": "4",
"address": "%s/24",
"gateway": "10.0.0.1",
"interface": 1
}
],
"routes": []
}
}`, ver, hostIfname, containerIfname, containerNs.Path(), containerIP.String())
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: containerNs.Path(),
IfName: "eth0",
StdinData: []byte(conf),
}
Expect(hostNs.Do(func(netNS ns.NetNS) error {
defer GinkgoRecover()
_, _, err := testutils.CmdAdd(containerNs.Path(), args.ContainerID, "", []byte(conf), func() error { return cmdAdd(args) })
Expect(err).To(MatchError("if burst is set, rate must also be set"))
return nil
})).To(Succeed())
})
// Runtime config parameters are expected to be preempted by the global config ones whenever specified
It(fmt.Sprintf("[%s] should apply static config when both static config and runtime config exist", ver), func() {
conf := fmt.Sprintf(`{
"cniVersion": "%s",
"name": "cni-plugin-bandwidth-test",
"type": "bandwidth",
"ingressRate": 0,
"ingressBurst": 0,
"egressRate": 123,
"egressBurst": 123,
"unshapedSubnets": ["192.168.0.0/24"],
"runtimeConfig": {
"bandWidth": {
"ingressRate": 8,
"ingressBurst": 8,
"egressRate": 16,
"egressBurst": 9,
"unshapedSubnets": ["10.0.0.0/8", "fd00:db8:abcd:1234:e000::/68"]
}
},
"prevResult": {
"interfaces": [
{
"name": "%s",
"sandbox": ""
},
{
"name": "%s",
"sandbox": "%s"
}
],
"ips": [
{
"version": "4",
"address": "%s/24",
"gateway": "10.0.0.1",
"interface": 1
}
],
"routes": []
}
}`, ver, hostIfname, containerIfname, containerNs.Path(), containerIP.String())
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: containerNs.Path(),
IfName: containerIfname,
StdinData: []byte(conf),
}
Expect(hostNs.Do(func(netNS ns.NetNS) error {
defer GinkgoRecover()
r, out, err := testutils.CmdAdd(containerNs.Path(), args.ContainerID, "", []byte(conf), func() error { return cmdAdd(args) })
Expect(err).NotTo(HaveOccurred(), string(out))
result, err := types100.GetResult(r)
Expect(err).NotTo(HaveOccurred())
Expect(result.Interfaces).To(HaveLen(3))
Expect(result.Interfaces[2].Name).To(Equal(ifbDeviceName))
Expect(result.Interfaces[2].Sandbox).To(Equal(""))
ifbLink, err := netlink.LinkByName(ifbDeviceName)
Expect(err).NotTo(HaveOccurred())
Expect(ifbLink.Attrs().MTU).To(Equal(hostIfaceMTU))
qdiscs, err := netlink.QdiscList(ifbLink)
Expect(err).NotTo(HaveOccurred())
Expect(qdiscs).To(HaveLen(1))
Expect(qdiscs[0].Attrs().LinkIndex).To(Equal(ifbLink.Attrs().Index))
Expect(qdiscs[0]).To(BeAssignableToTypeOf(&netlink.Htb{}))
Expect(qdiscs[0].(*netlink.Htb).Defcls).To(Equal(uint32(ShapedClassMinorID)))
classes, err := netlink.ClassList(ifbLink, qdiscs[0].Attrs().Handle)
Expect(err).NotTo(HaveOccurred())
Expect(classes).To(HaveLen(2))
// Uncapped class
Expect(classes[0]).To(BeAssignableToTypeOf(&netlink.HtbClass{}))
Expect(classes[0].(*netlink.HtbClass).Handle).To(Equal(netlink.MakeHandle(1, 1)))
Expect(classes[0].(*netlink.HtbClass).Rate).To(Equal(UncappedRate))
Expect(classes[0].(*netlink.HtbClass).Buffer).To(Equal(uint32(0)))
Expect(classes[0].(*netlink.HtbClass).Ceil).To(Equal(UncappedRate))
Expect(classes[0].(*netlink.HtbClass).Cbuffer).To(Equal(uint32(0)))
// Class with traffic shapping settings
Expect(classes[1]).To(BeAssignableToTypeOf(&netlink.HtbClass{}))
Expect(classes[1].(*netlink.HtbClass).Handle).To(Equal(netlink.MakeHandle(1, uint16(qdiscs[0].(*netlink.Htb).Defcls))))
Expect(classes[1].(*netlink.HtbClass).Rate).To(Equal(uint64(15)))
// Expect(classes[1].(*netlink.HtbClass).Buffer).To(Equal(uint32(7812500)))
Expect(classes[1].(*netlink.HtbClass).Ceil).To(Equal(uint64(30)))
// Expect(classes[1].(*netlink.HtbClass).Cbuffer).To(Equal(uint32(0)))
filters, err := netlink.FilterList(ifbLink, qdiscs[0].Attrs().Handle)
Expect(err).NotTo(HaveOccurred())
Expect(filters).To(HaveLen(1))
// traffic to 192.168.0.0/24 redirected to uncapped class
Expect(filters[0]).To(BeAssignableToTypeOf(&netlink.U32{}))
Expect(filters[0].(*netlink.U32).Actions).To(BeEmpty())
Expect(filters[0].Attrs().Protocol).To(Equal(uint16(syscall.ETH_P_IP)))
Expect(filters[0].Attrs().LinkIndex).To(Equal(ifbLink.Attrs().Index))
Expect(filters[0].Attrs().Priority).To(Equal(uint16(16)))
Expect(filters[0].Attrs().Parent).To(Equal(qdiscs[0].Attrs().Handle))
Expect(filters[0].(*netlink.U32).ClassId).To(Equal(netlink.MakeHandle(1, 1)))
filterSel := filters[0].(*netlink.U32).Sel
Expect(filterSel).To(BeAssignableToTypeOf(&netlink.TcU32Sel{}))
Expect(filterSel.Flags).To(Equal(uint8(netlink.TC_U32_TERMINAL)))
Expect(filterSel.Keys).To(HaveLen(1))
Expect(filterSel.Nkeys).To(Equal(uint8(1)))
// The filter should match to 192.168.0.0/24 dst address in other words it should be:
// match c0a80000/ffffff00 at 16
selKey := filterSel.Keys[0]
Expect(selKey.Val).To(Equal(uint32(192*math.Pow(256, 3) + 168*math.Pow(256, 2))))
Expect(selKey.Off).To(Equal(int32(16)))
Expect(selKey.Mask).To(Equal(uint32(255*math.Pow(256, 3) + 255*math.Pow(256, 2) + 255*256)))
hostVethLink, err := netlink.LinkByName(hostIfname)
Expect(err).NotTo(HaveOccurred())
qdiscFilters, err := netlink.FilterList(hostVethLink, netlink.MakeHandle(0xffff, 0))
Expect(err).NotTo(HaveOccurred())
Expect(qdiscFilters).To(HaveLen(1))
Expect(qdiscFilters[0].(*netlink.U32).Actions[0].(*netlink.MirredAction).Ifindex).To(Equal(ifbLink.Attrs().Index))
return nil
})).To(Succeed())
// Container ingress (host egress)
Expect(hostNs.Do(func(n ns.NetNS) error {
defer GinkgoRecover()
vethLink, err := netlink.LinkByName(hostIfname)
Expect(err).NotTo(HaveOccurred())
qdiscs, err := netlink.QdiscList(vethLink)
Expect(err).NotTo(HaveOccurred())
// No ingress QoS just mirroring
Expect(qdiscs).To(HaveLen(2))
Expect(qdiscs[0].Attrs().LinkIndex).To(Equal(vethLink.Attrs().Index))
Expect(qdiscs[0]).NotTo(BeAssignableToTypeOf(&netlink.Htb{}))
Expect(qdiscs[1]).NotTo(BeAssignableToTypeOf(&netlink.Htb{}))
return nil
})).To(Succeed())
})
It(fmt.Sprintf("[%s] should apply static config when both static config and runtime config exist (bad config example)", ver), func() {
conf := fmt.Sprintf(`{
"cniVersion": "%s",
"name": "cni-plugin-bandwidth-test",
"type": "bandwidth",
"ingressRate": 0,
"ingressBurst": 123,
"egressRate": 123,
"egressBurst": 123,
"runtimeConfig": {
"bandWidth": {
"ingressRate": 8,
"ingressBurst": 8,
"egressRate": 16,
"egressBurst": 9
}
},
"prevResult": {
"interfaces": [
{
"name": "%s",
"sandbox": ""
},
{
"name": "%s",
"sandbox": "%s"
}
],
"ips": [
{
"version": "4",
"address": "%s/24",
"gateway": "10.0.0.1",
"interface": 1
}
],
"routes": []
}
}`, ver, hostIfname, containerIfname, containerNs.Path(), containerIP.String())
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: containerNs.Path(),
IfName: "eth0",
StdinData: []byte(conf),
}
Expect(hostNs.Do(func(netNS ns.NetNS) error {
defer GinkgoRecover()
_, _, err := testutils.CmdAdd(containerNs.Path(), args.ContainerID, "", []byte(conf), func() error { return cmdAdd(args) })
Expect(err).To(MatchError("if burst is set, rate must also be set"))
return nil
})).To(Succeed())
})
})
}
Describe("Validating input", func() {
It("Should allow only 4GB burst rate", func() {
err := validateRateAndBurst(5000, 4*1024*1024*1024*8-16) // 2 bytes less than the max should pass
Expect(err).NotTo(HaveOccurred())
err = validateRateAndBurst(5000, 4*1024*1024*1024*8) // we're 1 bit above MaxUint32
Expect(err).To(HaveOccurred())
err = validateRateAndBurst(0, 1)
Expect(err).To(HaveOccurred())
err = validateRateAndBurst(1, 0)
Expect(err).To(HaveOccurred())
err = validateRateAndBurst(0, 0)
Expect(err).NotTo(HaveOccurred())
})
It("Should fail if both ShapedSubnets and UnshapedSubnets are specified", func() {
err := validateSubnets([]string{"10.0.0.0/8"}, []string{"192.168.0.0/24"})
Expect(err).To(HaveOccurred())
})
It("Should fail if specified UnshapedSubnets are not valid CIDRs", func() {
err := validateSubnets([]string{"10.0.0.0/8", "hello"}, []string{})
Expect(err).To(HaveOccurred())
})
It("Should fail if specified ShapedSubnets are not valid CIDRs", func() {
err := validateSubnets([]string{}, []string{"10.0.0.0/8", "hello"})
Expect(err).To(HaveOccurred())
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -1,824 +0,0 @@
// Copyright 2023 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 (
"context"
"encoding/json"
"fmt"
"log"
"net"
"os"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec"
"github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
types100 "github.com/containernetworking/cni/pkg/types/100"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containernetworking/plugins/pkg/testutils"
)
var _ = Describe("bandwidth measure test", func() {
var (
hostNs ns.NetNS
containerNs ns.NetNS
hostIfname string
containerIfname string
hostIP net.IP
containerIP net.IP
hostIfaceMTU int
)
BeforeEach(func() {
var err error
hostIfname = "host-veth"
containerIfname = "container-veth"
hostNs, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
containerNs, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
hostIP = net.IP{169, 254, 0, 1}
containerIP = net.IP{10, 254, 0, 1}
hostIfaceMTU = 1024
createVeth(hostNs, hostIfname, containerNs, containerIfname, hostIP, containerIP, hostIfaceMTU)
})
AfterEach(func() {
Expect(containerNs.Close()).To(Succeed())
Expect(testutils.UnmountNS(containerNs)).To(Succeed())
Expect(hostNs.Close()).To(Succeed())
Expect(testutils.UnmountNS(hostNs)).To(Succeed())
})
// Bandwidth requires host-side interface info, and thus only
// supports 0.3.0 and later CNI versions
for _, ver := range []string{"0.3.0", "0.3.1", "0.4.0", "1.0.0"} {
Describe(fmt.Sprintf("[%s] QoS effective", ver), func() {
Context(fmt.Sprintf("[%s] when chaining bandwidth plugin with PTP", ver), func() {
var ptpConf string
var rateInBits uint64
var burstInBits uint64
var packetInBytes int
var containerWithoutQoSNS ns.NetNS
var containerWithQoSNS ns.NetNS
var portServerWithQoS int
var portServerWithoutQoS int
var containerWithQoSRes types.Result
var containerWithoutQoSRes types.Result
var echoServerWithQoS *gexec.Session
var echoServerWithoutQoS *gexec.Session
var dataDir string
BeforeEach(func() {
rateInBytes := 1000
rateInBits = uint64(rateInBytes * 8)
burstInBits = rateInBits * 2
// NOTE: Traffic shapping is not that precise at low rates, would be better to use higher rates + simple time+netcat for data transfer, rather than the provided
// client/server bin (limited to small amount of data)
packetInBytes = rateInBytes * 3
var err error
dataDir, err = os.MkdirTemp("", "bandwidth_linux_test")
Expect(err).NotTo(HaveOccurred())
ptpConf = fmt.Sprintf(`{
"cniVersion": "%s",
"name": "myBWnet",
"type": "ptp",
"ipMasq": true,
"mtu": 512,
"ipam": {
"type": "host-local",
"subnet": "10.1.2.0/24",
"dataDir": "%s"
}
}`, ver, dataDir)
const (
containerWithQoSIFName = "ptp0"
containerWithoutQoSIFName = "ptp1"
)
containerWithQoSNS, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
containerWithoutQoSNS, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
By("create two containers, and use the bandwidth plugin on one of them")
Expect(hostNs.Do(func(ns.NetNS) error {
defer GinkgoRecover()
containerWithQoSRes, _, err = testutils.CmdAdd(containerWithQoSNS.Path(), "dummy", containerWithQoSIFName, []byte(ptpConf), func() error {
r, err := invoke.DelegateAdd(context.TODO(), "ptp", []byte(ptpConf), nil)
Expect(err).NotTo(HaveOccurred())
Expect(r.Print()).To(Succeed())
return err
})
Expect(err).NotTo(HaveOccurred())
containerWithoutQoSRes, _, err = testutils.CmdAdd(containerWithoutQoSNS.Path(), "dummy2", containerWithoutQoSIFName, []byte(ptpConf), func() error {
r, err := invoke.DelegateAdd(context.TODO(), "ptp", []byte(ptpConf), nil)
Expect(err).NotTo(HaveOccurred())
Expect(r.Print()).To(Succeed())
return err
})
Expect(err).NotTo(HaveOccurred())
containerWithQoSResult, err := types100.GetResult(containerWithQoSRes)
Expect(err).NotTo(HaveOccurred())
bandwidthPluginConf := &PluginConf{}
err = json.Unmarshal([]byte(ptpConf), &bandwidthPluginConf)
Expect(err).NotTo(HaveOccurred())
bandwidthPluginConf.RuntimeConfig.Bandwidth = &BandwidthEntry{
IngressBurst: burstInBits,
IngressRate: rateInBits,
EgressBurst: burstInBits,
EgressRate: rateInBits,
}
bandwidthPluginConf.Type = "bandwidth"
newConfBytes, err := buildOneConfig(ver, bandwidthPluginConf, containerWithQoSResult)
Expect(err).NotTo(HaveOccurred())
args := &skel.CmdArgs{
ContainerID: "dummy3",
Netns: containerWithQoSNS.Path(),
IfName: containerWithQoSIFName,
StdinData: newConfBytes,
}
result, out, err := testutils.CmdAdd(containerWithQoSNS.Path(), args.ContainerID, "", newConfBytes, func() error { return cmdAdd(args) })
Expect(err).NotTo(HaveOccurred(), string(out))
if testutils.SpecVersionHasCHECK(ver) {
// Do CNI Check
checkConf := &PluginConf{}
err = json.Unmarshal([]byte(ptpConf), &checkConf)
Expect(err).NotTo(HaveOccurred())
checkConf.RuntimeConfig.Bandwidth = &BandwidthEntry{
IngressBurst: burstInBits,
IngressRate: rateInBits,
EgressBurst: burstInBits,
EgressRate: rateInBits,
}
checkConf.Type = "bandwidth"
newCheckBytes, err := buildOneConfig(ver, checkConf, result)
Expect(err).NotTo(HaveOccurred())
args = &skel.CmdArgs{
ContainerID: "dummy3",
Netns: containerWithQoSNS.Path(),
IfName: containerWithQoSIFName,
StdinData: newCheckBytes,
}
err = testutils.CmdCheck(containerWithQoSNS.Path(), args.ContainerID, "", func() error { return cmdCheck(args) })
Expect(err).NotTo(HaveOccurred())
}
return nil
})).To(Succeed())
By("starting a tcp server on both containers")
portServerWithQoS, echoServerWithQoS = startEchoServerInNamespace(containerWithQoSNS)
portServerWithoutQoS, echoServerWithoutQoS = startEchoServerInNamespace(containerWithoutQoSNS)
})
AfterEach(func() {
Expect(os.RemoveAll(dataDir)).To(Succeed())
Expect(containerWithQoSNS.Close()).To(Succeed())
Expect(testutils.UnmountNS(containerWithQoSNS)).To(Succeed())
Expect(containerWithoutQoSNS.Close()).To(Succeed())
Expect(testutils.UnmountNS(containerWithoutQoSNS)).To(Succeed())
if echoServerWithoutQoS != nil {
echoServerWithoutQoS.Kill()
}
if echoServerWithQoS != nil {
echoServerWithQoS.Kill()
}
})
It("limits ingress traffic on veth device", func() {
var runtimeWithLimit time.Duration
var runtimeWithoutLimit time.Duration
By("gather timing statistics about both containers")
By("sending tcp traffic to the container that has traffic shaped", func() {
start := time.Now()
result, err := types100.GetResult(containerWithQoSRes)
Expect(err).NotTo(HaveOccurred())
makeTCPClientInNS(hostNs.Path(), result.IPs[0].Address.IP.String(), portServerWithQoS, packetInBytes)
end := time.Now()
runtimeWithLimit = end.Sub(start)
log.Printf("Elapsed with qos %.2f", runtimeWithLimit.Seconds())
})
By("sending tcp traffic to the container that does not have traffic shaped", func() {
start := time.Now()
result, err := types100.GetResult(containerWithoutQoSRes)
Expect(err).NotTo(HaveOccurred())
makeTCPClientInNS(hostNs.Path(), result.IPs[0].Address.IP.String(), portServerWithoutQoS, packetInBytes)
end := time.Now()
runtimeWithoutLimit = end.Sub(start)
log.Printf("Elapsed without qos %.2f", runtimeWithoutLimit.Seconds())
})
Expect(runtimeWithLimit).To(BeNumerically(">", runtimeWithoutLimit+1000*time.Millisecond))
})
})
})
Context(fmt.Sprintf("[%s] when chaining bandwidth plugin with PTP and excluding specific subnets from traffic", ver), func() {
var ptpConf string
var rateInBits uint64
var burstInBits uint64
var packetInBytes int
var containerWithoutQoSNS ns.NetNS
var containerWithQoSNS ns.NetNS
var portServerWithQoS int
var portServerWithoutQoS int
var containerWithQoSRes types.Result
var containerWithoutQoSRes types.Result
var echoServerWithQoS *gexec.Session
var echoServerWithoutQoS *gexec.Session
var dataDir string
BeforeEach(func() {
rateInBytes := 1000
rateInBits = uint64(rateInBytes * 8)
burstInBits = rateInBits * 2
unshapedSubnets := []string{"10.1.2.0/24"}
// NOTE: Traffic shapping is not that precise at low rates, would be better to use higher rates + simple time+netcat for data transfer, rather than the provided
// client/server bin (limited to small amount of data)
packetInBytes = rateInBytes * 3
var err error
dataDir, err = os.MkdirTemp("", "bandwidth_linux_test")
Expect(err).NotTo(HaveOccurred())
ptpConf = fmt.Sprintf(`{
"cniVersion": "%s",
"name": "myBWnet",
"type": "ptp",
"ipMasq": true,
"mtu": 512,
"ipam": {
"type": "host-local",
"subnet": "10.1.2.0/24",
"dataDir": "%s"
}
}`, ver, dataDir)
const (
containerWithQoSIFName = "ptp0"
containerWithoutQoSIFName = "ptp1"
)
containerWithQoSNS, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
containerWithoutQoSNS, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
By("create two containers, and use the bandwidth plugin on one of them")
Expect(hostNs.Do(func(ns.NetNS) error {
defer GinkgoRecover()
containerWithQoSRes, _, err = testutils.CmdAdd(containerWithQoSNS.Path(), "dummy", containerWithQoSIFName, []byte(ptpConf), func() error {
r, err := invoke.DelegateAdd(context.TODO(), "ptp", []byte(ptpConf), nil)
Expect(err).NotTo(HaveOccurred())
Expect(r.Print()).To(Succeed())
return err
})
Expect(err).NotTo(HaveOccurred())
containerWithoutQoSRes, _, err = testutils.CmdAdd(containerWithoutQoSNS.Path(), "dummy2", containerWithoutQoSIFName, []byte(ptpConf), func() error {
r, err := invoke.DelegateAdd(context.TODO(), "ptp", []byte(ptpConf), nil)
Expect(err).NotTo(HaveOccurred())
Expect(r.Print()).To(Succeed())
return err
})
Expect(err).NotTo(HaveOccurred())
containerWithQoSResult, err := types100.GetResult(containerWithQoSRes)
Expect(err).NotTo(HaveOccurred())
bandwidthPluginConf := &PluginConf{}
err = json.Unmarshal([]byte(ptpConf), &bandwidthPluginConf)
Expect(err).NotTo(HaveOccurred())
bandwidthPluginConf.RuntimeConfig.Bandwidth = &BandwidthEntry{
IngressBurst: burstInBits,
IngressRate: rateInBits,
EgressBurst: burstInBits,
EgressRate: rateInBits,
UnshapedSubnets: unshapedSubnets,
}
bandwidthPluginConf.Type = "bandwidth"
newConfBytes, err := buildOneConfig(ver, bandwidthPluginConf, containerWithQoSResult)
Expect(err).NotTo(HaveOccurred())
args := &skel.CmdArgs{
ContainerID: "dummy3",
Netns: containerWithQoSNS.Path(),
IfName: containerWithQoSIFName,
StdinData: newConfBytes,
}
result, out, err := testutils.CmdAdd(containerWithQoSNS.Path(), args.ContainerID, "", newConfBytes, func() error { return cmdAdd(args) })
Expect(err).NotTo(HaveOccurred(), string(out))
if testutils.SpecVersionHasCHECK(ver) {
// Do CNI Check
checkConf := &PluginConf{}
err = json.Unmarshal([]byte(ptpConf), &checkConf)
Expect(err).NotTo(HaveOccurred())
checkConf.RuntimeConfig.Bandwidth = &BandwidthEntry{
IngressBurst: burstInBits,
IngressRate: rateInBits,
EgressBurst: burstInBits,
EgressRate: rateInBits,
UnshapedSubnets: unshapedSubnets,
}
checkConf.Type = "bandwidth"
newCheckBytes, err := buildOneConfig(ver, checkConf, result)
Expect(err).NotTo(HaveOccurred())
args = &skel.CmdArgs{
ContainerID: "dummy3",
Netns: containerWithQoSNS.Path(),
IfName: containerWithQoSIFName,
StdinData: newCheckBytes,
}
err = testutils.CmdCheck(containerWithQoSNS.Path(), args.ContainerID, "", func() error { return cmdCheck(args) })
Expect(err).NotTo(HaveOccurred())
}
return nil
})).To(Succeed())
By("starting a tcp server on both containers")
portServerWithQoS, echoServerWithQoS = startEchoServerInNamespace(containerWithQoSNS)
portServerWithoutQoS, echoServerWithoutQoS = startEchoServerInNamespace(containerWithoutQoSNS)
})
AfterEach(func() {
Expect(os.RemoveAll(dataDir)).To(Succeed())
Expect(containerWithQoSNS.Close()).To(Succeed())
Expect(testutils.UnmountNS(containerWithQoSNS)).To(Succeed())
Expect(containerWithoutQoSNS.Close()).To(Succeed())
Expect(testutils.UnmountNS(containerWithoutQoSNS)).To(Succeed())
if echoServerWithoutQoS != nil {
echoServerWithoutQoS.Kill()
}
if echoServerWithQoS != nil {
echoServerWithQoS.Kill()
}
})
It("does not limits ingress traffic on veth device coming from 10.1.2.0/24", func() {
var runtimeWithLimit time.Duration
var runtimeWithoutLimit time.Duration
By("gather timing statistics about both containers")
By("sending tcp traffic to the container that has traffic shaped", func() {
start := time.Now()
result, err := types100.GetResult(containerWithQoSRes)
Expect(err).NotTo(HaveOccurred())
makeTCPClientInNS(hostNs.Path(), result.IPs[0].Address.IP.String(), portServerWithQoS, packetInBytes)
end := time.Now()
runtimeWithLimit = end.Sub(start)
log.Printf("Elapsed with qos %.2f", runtimeWithLimit.Seconds())
})
By("sending tcp traffic to the container that does not have traffic shaped", func() {
start := time.Now()
result, err := types100.GetResult(containerWithoutQoSRes)
Expect(err).NotTo(HaveOccurred())
makeTCPClientInNS(hostNs.Path(), result.IPs[0].Address.IP.String(), portServerWithoutQoS, packetInBytes)
end := time.Now()
runtimeWithoutLimit = end.Sub(start)
log.Printf("Elapsed without qos %.2f", runtimeWithoutLimit.Seconds())
})
Expect(runtimeWithLimit - runtimeWithoutLimit).To(BeNumerically("<", 100*time.Millisecond))
})
})
Context(fmt.Sprintf("[%s] when chaining bandwidth plugin with PTP and only including specific subnets in traffic shapping (not including the main ns one)", ver), func() {
var ptpConf string
var rateInBits uint64
var burstInBits uint64
var packetInBytes int
var containerWithoutQoSNS ns.NetNS
var containerWithQoSNS ns.NetNS
var portServerWithQoS int
var portServerWithoutQoS int
var containerWithQoSRes types.Result
var containerWithoutQoSRes types.Result
var echoServerWithQoS *gexec.Session
var echoServerWithoutQoS *gexec.Session
var dataDir string
BeforeEach(func() {
rateInBytes := 1000
rateInBits = uint64(rateInBytes * 8)
burstInBits = rateInBits * 2
shapedSubnets := []string{"10.2.2.0/24"}
// NOTE: Traffic shapping is not that precise at low rates, would be better to use higher rates + simple time+netcat for data transfer, rather than the provided
// client/server bin (limited to small amount of data)
packetInBytes = rateInBytes * 3
var err error
dataDir, err = os.MkdirTemp("", "bandwidth_linux_test")
Expect(err).NotTo(HaveOccurred())
ptpConf = fmt.Sprintf(`{
"cniVersion": "%s",
"name": "myBWnet",
"type": "ptp",
"ipMasq": true,
"mtu": 512,
"ipam": {
"type": "host-local",
"subnet": "10.1.2.0/24",
"dataDir": "%s"
}
}`, ver, dataDir)
const (
containerWithQoSIFName = "ptp0"
containerWithoutQoSIFName = "ptp1"
)
containerWithQoSNS, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
containerWithoutQoSNS, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
By("create two containers, and use the bandwidth plugin on one of them")
Expect(hostNs.Do(func(ns.NetNS) error {
defer GinkgoRecover()
containerWithQoSRes, _, err = testutils.CmdAdd(containerWithQoSNS.Path(), "dummy", containerWithQoSIFName, []byte(ptpConf), func() error {
r, err := invoke.DelegateAdd(context.TODO(), "ptp", []byte(ptpConf), nil)
Expect(err).NotTo(HaveOccurred())
Expect(r.Print()).To(Succeed())
return err
})
Expect(err).NotTo(HaveOccurred())
containerWithoutQoSRes, _, err = testutils.CmdAdd(containerWithoutQoSNS.Path(), "dummy2", containerWithoutQoSIFName, []byte(ptpConf), func() error {
r, err := invoke.DelegateAdd(context.TODO(), "ptp", []byte(ptpConf), nil)
Expect(err).NotTo(HaveOccurred())
Expect(r.Print()).To(Succeed())
return err
})
Expect(err).NotTo(HaveOccurred())
containerWithQoSResult, err := types100.GetResult(containerWithQoSRes)
Expect(err).NotTo(HaveOccurred())
bandwidthPluginConf := &PluginConf{}
err = json.Unmarshal([]byte(ptpConf), &bandwidthPluginConf)
Expect(err).NotTo(HaveOccurred())
bandwidthPluginConf.RuntimeConfig.Bandwidth = &BandwidthEntry{
IngressBurst: burstInBits,
IngressRate: rateInBits,
EgressBurst: burstInBits,
EgressRate: rateInBits,
ShapedSubnets: shapedSubnets,
}
bandwidthPluginConf.Type = "bandwidth"
newConfBytes, err := buildOneConfig(ver, bandwidthPluginConf, containerWithQoSResult)
Expect(err).NotTo(HaveOccurred())
args := &skel.CmdArgs{
ContainerID: "dummy3",
Netns: containerWithQoSNS.Path(),
IfName: containerWithQoSIFName,
StdinData: newConfBytes,
}
result, out, err := testutils.CmdAdd(containerWithQoSNS.Path(), args.ContainerID, "", newConfBytes, func() error { return cmdAdd(args) })
Expect(err).NotTo(HaveOccurred(), string(out))
if testutils.SpecVersionHasCHECK(ver) {
// Do CNI Check
checkConf := &PluginConf{}
err = json.Unmarshal([]byte(ptpConf), &checkConf)
Expect(err).NotTo(HaveOccurred())
checkConf.RuntimeConfig.Bandwidth = &BandwidthEntry{
IngressBurst: burstInBits,
IngressRate: rateInBits,
EgressBurst: burstInBits,
EgressRate: rateInBits,
ShapedSubnets: shapedSubnets,
}
checkConf.Type = "bandwidth"
newCheckBytes, err := buildOneConfig(ver, checkConf, result)
Expect(err).NotTo(HaveOccurred())
args = &skel.CmdArgs{
ContainerID: "dummy3",
Netns: containerWithQoSNS.Path(),
IfName: containerWithQoSIFName,
StdinData: newCheckBytes,
}
err = testutils.CmdCheck(containerWithQoSNS.Path(), args.ContainerID, "", func() error { return cmdCheck(args) })
Expect(err).NotTo(HaveOccurred())
}
return nil
})).To(Succeed())
By("starting a tcp server on both containers")
portServerWithQoS, echoServerWithQoS = startEchoServerInNamespace(containerWithQoSNS)
portServerWithoutQoS, echoServerWithoutQoS = startEchoServerInNamespace(containerWithoutQoSNS)
})
AfterEach(func() {
Expect(os.RemoveAll(dataDir)).To(Succeed())
Expect(containerWithQoSNS.Close()).To(Succeed())
Expect(testutils.UnmountNS(containerWithQoSNS)).To(Succeed())
Expect(containerWithoutQoSNS.Close()).To(Succeed())
Expect(testutils.UnmountNS(containerWithoutQoSNS)).To(Succeed())
if echoServerWithoutQoS != nil {
echoServerWithoutQoS.Kill()
}
if echoServerWithQoS != nil {
echoServerWithQoS.Kill()
}
})
It("does not limit ingress traffic on veth device coming from non included subnets", func() {
var runtimeWithLimit time.Duration
var runtimeWithoutLimit time.Duration
By("gather timing statistics about both containers")
By("sending tcp traffic to the container that has traffic shaped", func() {
start := time.Now()
result, err := types100.GetResult(containerWithQoSRes)
Expect(err).NotTo(HaveOccurred())
makeTCPClientInNS(hostNs.Path(), result.IPs[0].Address.IP.String(), portServerWithQoS, packetInBytes)
end := time.Now()
runtimeWithLimit = end.Sub(start)
log.Printf("Elapsed with qos %.2f", runtimeWithLimit.Seconds())
})
By("sending tcp traffic to the container that does not have traffic shaped", func() {
start := time.Now()
result, err := types100.GetResult(containerWithoutQoSRes)
Expect(err).NotTo(HaveOccurred())
makeTCPClientInNS(hostNs.Path(), result.IPs[0].Address.IP.String(), portServerWithoutQoS, packetInBytes)
end := time.Now()
runtimeWithoutLimit = end.Sub(start)
log.Printf("Elapsed without qos %.2f", runtimeWithoutLimit.Seconds())
})
Expect(runtimeWithLimit - runtimeWithoutLimit).To(BeNumerically("<", 100*time.Millisecond))
})
})
Context(fmt.Sprintf("[%s] when chaining bandwidth plugin with PTP and only including specific subnets in traffic shapping (including the main ns one)", ver), func() {
var ptpConf string
var rateInBits uint64
var burstInBits uint64
var packetInBytes int
var containerWithoutQoSNS ns.NetNS
var containerWithQoSNS ns.NetNS
var portServerWithQoS int
var portServerWithoutQoS int
var containerWithQoSRes types.Result
var containerWithoutQoSRes types.Result
var echoServerWithQoS *gexec.Session
var echoServerWithoutQoS *gexec.Session
var dataDir string
BeforeEach(func() {
rateInBytes := 1000
rateInBits = uint64(rateInBytes * 8)
burstInBits = rateInBits * 2
shapedSubnets := []string{"10.1.2.1/32"}
// NOTE: Traffic shapping is not that precise at low rates, would be better to use higher rates + simple time+netcat for data transfer, rather than the provided
// client/server bin (limited to small amount of data)
packetInBytes = rateInBytes * 3
var err error
dataDir, err = os.MkdirTemp("", "bandwidth_linux_test")
Expect(err).NotTo(HaveOccurred())
ptpConf = fmt.Sprintf(`{
"cniVersion": "%s",
"name": "myBWnet",
"type": "ptp",
"ipMasq": true,
"mtu": 512,
"ipam": {
"type": "host-local",
"subnet": "10.1.2.0/24",
"dataDir": "%s"
}
}`, ver, dataDir)
const (
containerWithQoSIFName = "ptp0"
containerWithoutQoSIFName = "ptp1"
)
containerWithQoSNS, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
containerWithoutQoSNS, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
By("create two containers, and use the bandwidth plugin on one of them")
Expect(hostNs.Do(func(ns.NetNS) error {
defer GinkgoRecover()
containerWithQoSRes, _, err = testutils.CmdAdd(containerWithQoSNS.Path(), "dummy", containerWithQoSIFName, []byte(ptpConf), func() error {
r, err := invoke.DelegateAdd(context.TODO(), "ptp", []byte(ptpConf), nil)
Expect(err).NotTo(HaveOccurred())
Expect(r.Print()).To(Succeed())
return err
})
Expect(err).NotTo(HaveOccurred())
containerWithoutQoSRes, _, err = testutils.CmdAdd(containerWithoutQoSNS.Path(), "dummy2", containerWithoutQoSIFName, []byte(ptpConf), func() error {
r, err := invoke.DelegateAdd(context.TODO(), "ptp", []byte(ptpConf), nil)
Expect(err).NotTo(HaveOccurred())
Expect(r.Print()).To(Succeed())
return err
})
Expect(err).NotTo(HaveOccurred())
containerWithQoSResult, err := types100.GetResult(containerWithQoSRes)
Expect(err).NotTo(HaveOccurred())
bandwidthPluginConf := &PluginConf{}
err = json.Unmarshal([]byte(ptpConf), &bandwidthPluginConf)
Expect(err).NotTo(HaveOccurred())
bandwidthPluginConf.RuntimeConfig.Bandwidth = &BandwidthEntry{
IngressBurst: burstInBits,
IngressRate: rateInBits,
EgressBurst: burstInBits,
EgressRate: rateInBits,
ShapedSubnets: shapedSubnets,
}
bandwidthPluginConf.Type = "bandwidth"
newConfBytes, err := buildOneConfig(ver, bandwidthPluginConf, containerWithQoSResult)
Expect(err).NotTo(HaveOccurred())
args := &skel.CmdArgs{
ContainerID: "dummy3",
Netns: containerWithQoSNS.Path(),
IfName: containerWithQoSIFName,
StdinData: newConfBytes,
}
result, out, err := testutils.CmdAdd(containerWithQoSNS.Path(), args.ContainerID, "", newConfBytes, func() error { return cmdAdd(args) })
Expect(err).NotTo(HaveOccurred(), string(out))
if testutils.SpecVersionHasCHECK(ver) {
// Do CNI Check
checkConf := &PluginConf{}
err = json.Unmarshal([]byte(ptpConf), &checkConf)
Expect(err).NotTo(HaveOccurred())
checkConf.RuntimeConfig.Bandwidth = &BandwidthEntry{
IngressBurst: burstInBits,
IngressRate: rateInBits,
EgressBurst: burstInBits,
EgressRate: rateInBits,
ShapedSubnets: shapedSubnets,
}
checkConf.Type = "bandwidth"
newCheckBytes, err := buildOneConfig(ver, checkConf, result)
Expect(err).NotTo(HaveOccurred())
args = &skel.CmdArgs{
ContainerID: "dummy3",
Netns: containerWithQoSNS.Path(),
IfName: containerWithQoSIFName,
StdinData: newCheckBytes,
}
err = testutils.CmdCheck(containerWithQoSNS.Path(), args.ContainerID, "", func() error { return cmdCheck(args) })
Expect(err).NotTo(HaveOccurred())
}
return nil
})).To(Succeed())
By("starting a tcp server on both containers")
portServerWithQoS, echoServerWithQoS = startEchoServerInNamespace(containerWithQoSNS)
portServerWithoutQoS, echoServerWithoutQoS = startEchoServerInNamespace(containerWithoutQoSNS)
})
AfterEach(func() {
Expect(os.RemoveAll(dataDir)).To(Succeed())
Expect(containerWithQoSNS.Close()).To(Succeed())
Expect(testutils.UnmountNS(containerWithQoSNS)).To(Succeed())
Expect(containerWithoutQoSNS.Close()).To(Succeed())
Expect(testutils.UnmountNS(containerWithoutQoSNS)).To(Succeed())
if echoServerWithoutQoS != nil {
echoServerWithoutQoS.Kill()
}
if echoServerWithQoS != nil {
echoServerWithQoS.Kill()
}
})
It("limits ingress traffic on veth device coming from included subnets", func() {
var runtimeWithLimit time.Duration
var runtimeWithoutLimit time.Duration
By("gather timing statistics about both containers")
By("sending tcp traffic to the container that has traffic shaped", func() {
start := time.Now()
result, err := types100.GetResult(containerWithQoSRes)
Expect(err).NotTo(HaveOccurred())
makeTCPClientInNS(hostNs.Path(), result.IPs[0].Address.IP.String(), portServerWithQoS, packetInBytes)
end := time.Now()
runtimeWithLimit = end.Sub(start)
log.Printf("Elapsed with qos %.2f", runtimeWithLimit.Seconds())
})
By("sending tcp traffic to the container that does not have traffic shaped", func() {
start := time.Now()
result, err := types100.GetResult(containerWithoutQoSRes)
Expect(err).NotTo(HaveOccurred())
makeTCPClientInNS(hostNs.Path(), result.IPs[0].Address.IP.String(), portServerWithoutQoS, packetInBytes)
end := time.Now()
runtimeWithoutLimit = end.Sub(start)
log.Printf("Elapsed without qos %.2f", runtimeWithoutLimit.Seconds())
})
Expect(runtimeWithLimit).To(BeNumerically(">", runtimeWithoutLimit+1000*time.Millisecond))
})
})
}
})

View File

@ -15,7 +15,6 @@ package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
@ -31,11 +30,10 @@ import (
"github.com/onsi/gomega/gexec"
"github.com/vishvananda/netlink"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/plugins/pkg/ns"
)
func TestHTB(t *testing.T) {
func TestTBF(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "plugins/meta/bandwidth")
}
@ -108,13 +106,13 @@ func makeTCPClientInNS(netns string, address string, port int, numBytes int) {
}
func createVeth(hostNs ns.NetNS, hostVethIfName string, containerNs ns.NetNS, containerVethIfName string, hostIP []byte, containerIP []byte, hostIfaceMTU int) {
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = hostVethIfName
linkAttrs.Flags = net.FlagUp
linkAttrs.MTU = hostIfaceMTU
vethDeviceRequest := &netlink.Veth{
LinkAttrs: netlink.LinkAttrs{
Name: hostVethIfName,
Flags: net.FlagUp,
MTU: hostIfaceMTU,
},
PeerName: containerVethIfName,
LinkAttrs: linkAttrs,
PeerName: containerVethIfName,
}
err := hostNs.Do(func(_ ns.NetNS) error {
@ -195,12 +193,12 @@ func createVeth(hostNs ns.NetNS, hostVethIfName string, containerNs ns.NetNS, co
}
func createVethInOneNs(netNS ns.NetNS, vethName, peerName string) {
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = vethName
linkAttrs.Flags = net.FlagUp
vethDeviceRequest := &netlink.Veth{
LinkAttrs: netlink.LinkAttrs{
Name: vethName,
Flags: net.FlagUp,
},
PeerName: peerName,
LinkAttrs: linkAttrs,
PeerName: peerName,
}
err := netNS.Do(func(_ ns.NetNS) error {
@ -224,13 +222,13 @@ func createMacvlan(netNS ns.NetNS, master, macvlanName string) {
return fmt.Errorf("failed to lookup master %q: %v", master, err)
}
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.MTU = m.Attrs().MTU
linkAttrs.Name = macvlanName
linkAttrs.ParentIndex = m.Attrs().Index
macvlanDeviceRequest := &netlink.Macvlan{
LinkAttrs: netlink.LinkAttrs{
MTU: m.Attrs().MTU,
Name: macvlanName,
ParentIndex: m.Attrs().Index,
},
Mode: netlink.MACVLAN_MODE_BRIDGE,
LinkAttrs: linkAttrs,
Mode: netlink.MACVLAN_MODE_BRIDGE,
}
if err = netlink.LinkAdd(macvlanDeviceRequest); err != nil {
@ -245,47 +243,3 @@ func createMacvlan(netNS ns.NetNS, master, macvlanName string) {
})
Expect(err).NotTo(HaveOccurred())
}
func buildOneConfig(cniVersion string, orig *PluginConf, prevResult types.Result) ([]byte, error) {
var err error
inject := map[string]interface{}{
"name": "myBWnet",
"cniVersion": cniVersion,
}
// Add previous plugin result
if prevResult != nil {
r, err := prevResult.GetAsVersion(cniVersion)
Expect(err).NotTo(HaveOccurred())
inject["prevResult"] = r
}
// Ensure every config uses the same name and version
config := make(map[string]interface{})
confBytes, err := json.Marshal(orig)
if err != nil {
return nil, err
}
err = json.Unmarshal(confBytes, &config)
if err != nil {
return nil, fmt.Errorf("unmarshal existing network bytes: %s", err)
}
for key, value := range inject {
config[key] = value
}
newBytes, err := json.Marshal(config)
if err != nil {
return nil, err
}
conf := &PluginConf{}
if err := json.Unmarshal(newBytes, &conf); err != nil {
return nil, fmt.Errorf("error parsing configuration: %s", err)
}
return newBytes, nil
}

View File

@ -15,8 +15,6 @@
package main
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"syscall"
@ -26,24 +24,17 @@ import (
"github.com/containernetworking/plugins/pkg/ip"
)
const (
latencyInMillis = 25
UncappedRate uint64 = 100_000_000_000
ShapedClassMinorID uint16 = 48
UnShapedClassMinorID uint16 = 1
)
func CreateIfb(ifbDeviceName string, mtu int, qlen int) error {
if qlen < 1000 {
qlen = 1000
}
const latencyInMillis = 25
func CreateIfb(ifbDeviceName string, mtu int) error {
// do not set TxQLen > 0 nor TxQLen == -1 until issues have been fixed with numrxqueues / numtxqueues across interfaces
// which needs to get set on IFB devices via upstream library: see hint https://github.com/containernetworking/plugins/pull/1097
err := netlink.LinkAdd(&netlink.Ifb{
LinkAttrs: netlink.LinkAttrs{
Name: ifbDeviceName,
Flags: net.FlagUp,
MTU: mtu,
TxQLen: qlen,
TxQLen: 0,
},
})
if err != nil {
@ -61,24 +52,15 @@ func TeardownIfb(deviceName string) error {
return err
}
func CreateIngressQdisc(rateInBits, burstInBits uint64, excludeSubnets []string, includeSubnets []string, hostDeviceName string) error {
func CreateIngressQdisc(rateInBits, burstInBits uint64, hostDeviceName string) error {
hostDevice, err := netlink.LinkByName(hostDeviceName)
if err != nil {
return fmt.Errorf("get host device: %s", err)
}
subnets := includeSubnets
exclude := false
if len(excludeSubnets) > 0 {
subnets = excludeSubnets
exclude = true
}
return createHTB(rateInBits, burstInBits, hostDevice.Attrs().Index, subnets, exclude)
return createTBF(rateInBits, burstInBits, hostDevice.Attrs().Index)
}
func CreateEgressQdisc(rateInBits, burstInBits uint64, excludeSubnets []string, includeSubnets []string, hostDeviceName string, ifbDeviceName string) error {
func CreateEgressQdisc(rateInBits, burstInBits uint64, hostDeviceName string, ifbDeviceName string) error {
ifbDevice, err := netlink.LinkByName(ifbDeviceName)
if err != nil {
return fmt.Errorf("get ifb device: %s", err)
@ -125,216 +107,44 @@ func CreateEgressQdisc(rateInBits, burstInBits uint64, excludeSubnets []string,
return fmt.Errorf("add filter: %s", err)
}
subnets := excludeSubnets
exclude := true
if len(includeSubnets) > 0 {
subnets = includeSubnets
exclude = false
}
// throttle traffic on ifb device
err = createHTB(rateInBits, burstInBits, ifbDevice.Attrs().Index, subnets, exclude)
err = createTBF(rateInBits, burstInBits, ifbDevice.Attrs().Index)
if err != nil {
// egress from the container/netns pov = ingress from the main netns/host pov
return fmt.Errorf("create htb container egress qos rules: %s", err)
return fmt.Errorf("create ifb qdisc: %s", err)
}
return nil
}
func createHTB(rateInBits, burstInBits uint64, linkIndex int, subnets []string, excludeSubnets bool) error {
// Netlink struct fields are not clear, let's use shell
defaultClassID := UnShapedClassMinorID
// If no subnets are specified, then shaping should apply to everything
if len(subnets) == 0 || excludeSubnets {
defaultClassID = ShapedClassMinorID
func createTBF(rateInBits, burstInBits uint64, linkIndex int) error {
// Equivalent to
// tc qdisc add dev link root tbf
// rate netConf.BandwidthLimits.Rate
// burst netConf.BandwidthLimits.Burst
if rateInBits <= 0 {
return fmt.Errorf("invalid rate: %d", rateInBits)
}
if burstInBits <= 0 {
return fmt.Errorf("invalid burst: %d", burstInBits)
}
rateInBytes := rateInBits / 8
burstInBytes := burstInBits / 8
bufferInBytes := buffer(rateInBytes, uint32(burstInBytes))
latency := latencyInUsec(latencyInMillis)
limitInBytes := limit(rateInBytes, latency, uint32(burstInBytes))
// Step 1 qdisc
// cmd := exec.Command("/usr/sbin/tc", "qdisc", "add", "dev", interfaceName, "root", "handle", "1:", "htb", "default", "30")
qdisc := &netlink.Htb{
qdisc := &netlink.Tbf{
QdiscAttrs: netlink.QdiscAttrs{
LinkIndex: linkIndex,
Handle: netlink.MakeHandle(1, 0),
Parent: netlink.HANDLE_ROOT,
},
Defcls: uint32(defaultClassID),
// No idea what these are so let's keep the default values from source code...
Version: 3,
Rate2Quantum: 10,
Limit: limitInBytes,
Rate: rateInBytes,
Buffer: bufferInBytes,
}
err := netlink.QdiscAdd(qdisc)
if err != nil {
return fmt.Errorf("error while creating qdisc: %s", err)
}
// Step 2 classes
rateInBytes := rateInBits / 8
burstInBytes := burstInBits / 8
bufferInBytes := buffer(rateInBytes, uint32(burstInBytes))
// The capped class for shaped traffic (included subnets or all but excluded subnets)
// cmd = exec.Command("/usr/sbin/tc", "class", "add", "dev", interfaceName, "parent", "1:", "classid", "1:30", "htb", "rate",
// fmt.Sprintf("%d", rateInBits), "burst", fmt.Sprintf("%d", burstInBits))
shapedClass := &netlink.HtbClass{
ClassAttrs: netlink.ClassAttrs{
LinkIndex: linkIndex,
Handle: netlink.MakeHandle(1, ShapedClassMinorID),
Parent: netlink.MakeHandle(1, 0),
},
Rate: rateInBytes,
Buffer: bufferInBytes,
// Let's set up the "burst" rate to twice the specified rate
Ceil: 2 * rateInBytes,
Cbuffer: bufferInBytes,
}
err = netlink.ClassAdd(shapedClass)
if err != nil {
return fmt.Errorf("error while creating htb default class: %s", err)
}
// The uncapped class for non shaped traffic (either all but included subnets or excluded subnets only)
// cmd = exec.Command("/usr/sbin/tc", "class", "add", "dev", interfaceName, "parent", "1:", "classid", "1:1", "htb",
// "rate", "100000000000")
bigRate := UncappedRate
unshapedClass := &netlink.HtbClass{
ClassAttrs: netlink.ClassAttrs{
LinkIndex: linkIndex,
Handle: netlink.MakeHandle(1, UnShapedClassMinorID),
Parent: qdisc.Handle,
},
Rate: bigRate,
Ceil: bigRate,
// No need for any burst, the minimum buffer size in q_htb.c should be enough to handle the rate which
// is already more than enough
}
err = netlink.ClassAdd(unshapedClass)
if err != nil {
return fmt.Errorf("error while creating htb uncapped class: %s", err)
}
// Now add filters to redirect subnets to the class 1 if excluded instead of the default one (30)
for _, subnet := range subnets {
// cmd = exec.Command("/usr/sbin/tc", "filter", "add", "dev", interfaceName, "parent", "1:", "protocol", protocol,
// "prio", "16", "u32", "match", "ip", "dst", subnet, "flowid", "1:1")
_, nw, err := net.ParseCIDR(subnet)
if err != nil {
return fmt.Errorf("bad subnet %s: %s", subnet, err)
}
var maskBytes []byte = nw.Mask
var subnetBytes []byte = nw.IP
if len(maskBytes) != len(subnetBytes) {
return fmt.Errorf("error using net lib for subnet %s len(maskBytes) != len(subnetBytes) "+
"(%d != %d) should not happen", subnet, len(maskBytes), len(subnetBytes))
}
isIpv4 := nw.IP.To4() != nil
protocol := syscall.ETH_P_IPV6
var prio uint16 = 15
var offset int32 = 24
keepBytes := 16
if isIpv4 {
protocol = syscall.ETH_P_IP
offset = 16
keepBytes = 4
// prio/pref needs to be changed if we change the protocol, looks like we cannot mix protocols with the same pref
prio = 16
}
if len(maskBytes) < keepBytes {
return fmt.Errorf("error with net lib, unexpected count of bytes for ipv4 mask (%d < %d)",
len(maskBytes), keepBytes)
}
if len(subnetBytes) < keepBytes {
return fmt.Errorf("error with net lib, unexpected count of bytes for ipv4 subnet (%d < %d)",
len(subnetBytes), keepBytes)
}
maskBytes = maskBytes[len(maskBytes)-keepBytes:]
subnetBytes = subnetBytes[len(subnetBytes)-keepBytes:]
// For ipv4 we should have at most 1 key, for ipv6 at most 4
keys := make([]netlink.TcU32Key, 0, 4)
for i := 0; i < len(maskBytes); i += 4 {
var mask, subnetI uint32
buf := bytes.NewReader(maskBytes[i : i+4])
err = binary.Read(buf, binary.BigEndian, &mask)
if err != nil {
return fmt.Errorf("error, htb filter, unable to build mask match filter, iter %d for subnet %s",
i, subnet)
}
if mask != 0 {
// If mask == 0, any value on this section will be a match and we do not need a filter for this
buf = bytes.NewReader(subnetBytes[i : i+4])
err = binary.Read(buf, binary.BigEndian, &subnetI)
if err != nil {
return fmt.Errorf("error, htb filter, unable to build subnet match filter, iter %d for subnet %s",
i, subnet)
}
keys = append(keys, netlink.TcU32Key{
Mask: mask,
Val: subnetI,
Off: offset,
OffMask: 0,
})
}
offset += 4
}
if len(keys) != cap(keys) {
shrinkedKeys := make([]netlink.TcU32Key, len(keys))
copied := copy(shrinkedKeys, keys)
if copied != len(keys) {
return fmt.Errorf("copy tc u32 keys error, for subnet %s copied %d != keys %d", subnet, copied, len(keys))
}
keys = shrinkedKeys
}
if isIpv4 && len(keys) > 1 {
return fmt.Errorf("error, htb ipv4 filter, unexpected rule length (%d > 1), for subnet %s",
len(keys), subnet)
} else if len(keys) > 4 {
return fmt.Errorf("error, htb ipv6 filter, unexpected rule length (%d > 4), for subnet %s",
len(keys), subnet)
}
// If len(keys) == 0, it means that we want to wildcard all traffic on the non default/uncapped class
var selector *netlink.TcU32Sel
if len(keys) > 0 {
selector = &netlink.TcU32Sel{
Nkeys: uint8(len(keys)),
Flags: netlink.TC_U32_TERMINAL,
Keys: keys,
}
}
classID := shapedClass.Handle
if excludeSubnets {
classID = unshapedClass.Handle
}
tcFilter := netlink.U32{
FilterAttrs: netlink.FilterAttrs{
LinkIndex: linkIndex,
Parent: qdisc.Handle,
Priority: prio,
Protocol: uint16(protocol),
},
ClassId: classID,
Sel: selector,
}
err = netlink.FilterAdd(&tcFilter)
if err != nil {
return fmt.Errorf("error, unable to create htb filter, details %s", err)
}
return fmt.Errorf("create qdisc: %s", err)
}
return nil
}
@ -346,3 +156,11 @@ func time2Tick(time uint32) uint32 {
func buffer(rate uint64, burst uint32) uint32 {
return time2Tick(uint32(float64(burst) * float64(netlink.TIME_UNITS_PER_SEC) / float64(rate)))
}
func limit(rate uint64, latency float64, buffer uint32) uint32 {
return uint32(float64(rate)*latency/float64(netlink.TIME_UNITS_PER_SEC)) + buffer
}
func latencyInUsec(latencyInMillis float64) float64 {
return float64(netlink.TIME_UNITS_PER_SEC) * (latencyInMillis / 1000.0)
}

View File

@ -18,7 +18,6 @@ import (
"encoding/json"
"fmt"
"math"
"net"
"github.com/vishvananda/netlink"
@ -40,12 +39,11 @@ const (
// BandwidthEntry corresponds to a single entry in the bandwidth argument,
// see CONVENTIONS.md
type BandwidthEntry struct {
UnshapedSubnets []string `json:"unshapedSubnets"` // Ipv4/ipv6 subnets to be excluded from traffic shaping. UnshapedSubnets and ShapedSubnets parameters are mutually exlusive
ShapedSubnets []string `json:"shapedSubnets"` // Ipv4/ipv6 subnets to be included in traffic shaping. UnshapedSubnets and ShapedSubnets parameters are mutually exlusive
IngressRate uint64 `json:"ingressRate"` // Bandwidth rate in bps for traffic through container. 0 for no limit. If ingressRate is set, ingressBurst must also be set
IngressBurst uint64 `json:"ingressBurst"` // Bandwidth burst in bits for traffic through container. 0 for no limit. If ingressBurst is set, ingressRate must also be set
EgressRate uint64 `json:"egressRate"` // Bandwidth rate in bps for traffic through container. 0 for no limit. If egressRate is set, egressBurst must also be set
EgressBurst uint64 `json:"egressBurst"` // Bandwidth burst in bits for traffic through container. 0 for no limit. If egressBurst is set, egressRate must also be set
IngressRate uint64 `json:"ingressRate"` // Bandwidth rate in bps for traffic through container. 0 for no limit. If ingressRate is set, ingressBurst must also be set
IngressBurst uint64 `json:"ingressBurst"` // Bandwidth burst in bits for traffic through container. 0 for no limit. If ingressBurst is set, ingressRate must also be set
EgressRate uint64 `json:"egressRate"` // Bandwidth rate in bps for traffic through container. 0 for no limit. If egressRate is set, egressBurst must also be set
EgressBurst uint64 `json:"egressBurst"` // Bandwidth burst in bits for traffic through container. 0 for no limit. If egressBurst is set, egressRate must also be set
}
func (bw *BandwidthEntry) isZero() bool {
@ -98,21 +96,10 @@ func parseConfig(stdin []byte) (*PluginConf, error) {
}
func getBandwidth(conf *PluginConf) *BandwidthEntry {
bw := conf.BandwidthEntry
if bw == nil && conf.RuntimeConfig.Bandwidth != nil {
bw = conf.RuntimeConfig.Bandwidth
if conf.BandwidthEntry == nil && conf.RuntimeConfig.Bandwidth != nil {
return conf.RuntimeConfig.Bandwidth
}
if bw != nil {
if bw.UnshapedSubnets == nil {
bw.UnshapedSubnets = make([]string, 0)
}
if bw.ShapedSubnets == nil {
bw.ShapedSubnets = make([]string, 0)
}
}
return bw
return conf.BandwidthEntry
}
func validateRateAndBurst(rate, burst uint64) error {
@ -132,13 +119,13 @@ func getIfbDeviceName(networkName string, containerID string) string {
return utils.MustFormatHashWithPrefix(maxIfbDeviceLength, ifbDevicePrefix, networkName+containerID)
}
func getMTUAndQLen(deviceName string) (int, int, error) {
func getMTU(deviceName string) (int, error) {
link, err := netlink.LinkByName(deviceName)
if err != nil {
return -1, -1, err
return -1, err
}
return link.Attrs().MTU, link.Attrs().TxQLen, nil
return link.Attrs().MTU, nil
}
// get the veth peer of container interface in host namespace
@ -172,28 +159,6 @@ func getHostInterface(interfaces []*current.Interface, containerIfName string, n
return nil, fmt.Errorf("no veth peer of container interface found in host ns")
}
func validateSubnets(unshapedSubnets []string, shapedSubnets []string) error {
if len(unshapedSubnets) > 0 && len(shapedSubnets) > 0 {
return fmt.Errorf("unshapedSubnets and shapedSubnets cannot be both specified, one of them should be discarded")
}
for _, subnet := range unshapedSubnets {
_, _, err := net.ParseCIDR(subnet)
if err != nil {
return fmt.Errorf("bad subnet %q provided, details %s", subnet, err)
}
}
for _, subnet := range shapedSubnets {
_, _, err := net.ParseCIDR(subnet)
if err != nil {
return fmt.Errorf("bad subnet %q provided, details %s", subnet, err)
}
}
return nil
}
func cmdAdd(args *skel.CmdArgs) error {
conf, err := parseConfig(args.StdinData)
if err != nil {
@ -205,10 +170,6 @@ func cmdAdd(args *skel.CmdArgs) error {
return types.PrintResult(conf.PrevResult, conf.CNIVersion)
}
if err = validateSubnets(bandwidth.UnshapedSubnets, bandwidth.ShapedSubnets); err != nil {
return err
}
if conf.PrevResult == nil {
return fmt.Errorf("must be called as chained plugin")
}
@ -230,22 +191,21 @@ func cmdAdd(args *skel.CmdArgs) error {
}
if bandwidth.IngressRate > 0 && bandwidth.IngressBurst > 0 {
err = CreateIngressQdisc(bandwidth.IngressRate, bandwidth.IngressBurst,
bandwidth.UnshapedSubnets, bandwidth.ShapedSubnets, hostInterface.Name)
err = CreateIngressQdisc(bandwidth.IngressRate, bandwidth.IngressBurst, hostInterface.Name)
if err != nil {
return err
}
}
if bandwidth.EgressRate > 0 && bandwidth.EgressBurst > 0 {
mtu, qlen, err := getMTUAndQLen(hostInterface.Name)
mtu, err := getMTU(hostInterface.Name)
if err != nil {
return err
}
ifbDeviceName := getIfbDeviceName(conf.Name, args.ContainerID)
err = CreateIfb(ifbDeviceName, mtu, qlen)
err = CreateIfb(ifbDeviceName, mtu)
if err != nil {
return err
}
@ -259,9 +219,7 @@ func cmdAdd(args *skel.CmdArgs) error {
Name: ifbDeviceName,
Mac: ifbDevice.Attrs().HardwareAddr.String(),
})
err = CreateEgressQdisc(bandwidth.EgressRate, bandwidth.EgressBurst,
bandwidth.UnshapedSubnets, bandwidth.ShapedSubnets, hostInterface.Name,
ifbDeviceName)
err = CreateEgressQdisc(bandwidth.EgressRate, bandwidth.EgressBurst, hostInterface.Name, ifbDeviceName)
if err != nil {
return err
}
@ -282,7 +240,13 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.VersionsStartingFrom("0.3.0"), bv.BuildString("bandwidth"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
/* FIXME GC */
/* FIXME Status */
}, version.VersionsStartingFrom("0.3.0"), bv.BuildString("bandwidth"))
}
func SafeQdiscList(link netlink.Link) ([]netlink.Qdisc, error) {
@ -334,99 +298,75 @@ func cmdCheck(args *skel.CmdArgs) error {
bandwidth := getBandwidth(bwConf)
if err = validateSubnets(bandwidth.UnshapedSubnets, bandwidth.ShapedSubnets); err != nil {
return fmt.Errorf("failed to check subnets, details %s", err)
}
if bandwidth.IngressRate > 0 && bandwidth.IngressBurst > 0 {
rateInBytes := bandwidth.IngressRate / 8
burstInBytes := bandwidth.IngressBurst / 8
bufferInBytes := buffer(rateInBytes, uint32(burstInBytes))
err = checkHTB(link, rateInBytes, bufferInBytes, bandwidth.ShapedSubnets)
latency := latencyInUsec(latencyInMillis)
limitInBytes := limit(rateInBytes, latency, uint32(burstInBytes))
qdiscs, err := SafeQdiscList(link)
if err != nil {
return err
}
if len(qdiscs) == 0 {
return fmt.Errorf("Failed to find qdisc")
}
for _, qdisc := range qdiscs {
tbf, isTbf := qdisc.(*netlink.Tbf)
if !isTbf {
break
}
if tbf.Rate != rateInBytes {
return fmt.Errorf("Rate doesn't match")
}
if tbf.Limit != limitInBytes {
return fmt.Errorf("Limit doesn't match")
}
if tbf.Buffer != bufferInBytes {
return fmt.Errorf("Buffer doesn't match")
}
}
}
if bandwidth.EgressRate > 0 && bandwidth.EgressBurst > 0 {
rateInBytes := bandwidth.EgressRate / 8
burstInBytes := bandwidth.EgressBurst / 8
bufferInBytes := buffer(rateInBytes, uint32(burstInBytes))
latency := latencyInUsec(latencyInMillis)
limitInBytes := limit(rateInBytes, latency, uint32(burstInBytes))
ifbDeviceName := getIfbDeviceName(bwConf.Name, args.ContainerID)
ifbDevice, err := netlink.LinkByName(ifbDeviceName)
if err != nil {
return fmt.Errorf("get ifb device: %s", err)
}
err = checkHTB(ifbDevice, rateInBytes, bufferInBytes, bandwidth.ShapedSubnets)
qdiscs, err := SafeQdiscList(ifbDevice)
if err != nil {
return err
}
}
return nil
}
func checkHTB(link netlink.Link, rateInBytes uint64, bufferInBytes uint32, shapedSubnets []string) error {
qdiscs, err := SafeQdiscList(link)
if err != nil {
return err
}
if len(qdiscs) == 0 {
return fmt.Errorf("Failed to find qdisc")
}
foundHTB := false
for _, qdisc := range qdiscs {
htb, isHtb := qdisc.(*netlink.Htb)
if !isHtb {
continue
if len(qdiscs) == 0 {
return fmt.Errorf("Failed to find qdisc")
}
if foundHTB {
return fmt.Errorf("Several htb qdisc found for device %s", link.Attrs().Name)
}
foundHTB = true
defaultClassMinorID := ShapedClassMinorID
if len(shapedSubnets) > 0 {
defaultClassMinorID = UnShapedClassMinorID
}
if htb.Defcls != uint32(defaultClassMinorID) {
return fmt.Errorf("Default class does not match")
}
classes, err := netlink.ClassList(link, htb.Handle)
if err != nil {
return fmt.Errorf("Unable to list classes bound to htb qdisc for device %s. Details %s",
link.Attrs().Name, err)
}
if len(classes) != 2 {
return fmt.Errorf("Number of htb classes does not match for device %s (%d != 2)",
link.Attrs().Name, len(classes))
}
for _, c := range classes {
htbClass, isHtb := c.(*netlink.HtbClass)
if !isHtb {
return fmt.Errorf("Unexpected class for parent htb qdisc bound to device %s", link.Attrs().Name)
for _, qdisc := range qdiscs {
tbf, isTbf := qdisc.(*netlink.Tbf)
if !isTbf {
break
}
if htbClass.Handle == htb.Defcls {
if htbClass.Rate != rateInBytes {
return fmt.Errorf("Rate does not match for the default class for device %s (%d != %d)",
link.Attrs().Name, htbClass.Rate, rateInBytes)
}
if htbClass.Buffer != bufferInBytes {
return fmt.Errorf("Burst buffer size does not match for the default class for device %s (%d != %d)",
link.Attrs().Name, htbClass.Buffer, bufferInBytes)
}
} else if htbClass.Handle == netlink.MakeHandle(1, 1) {
if htbClass.Rate != UncappedRate {
return fmt.Errorf("Rate does not match for the uncapped class for device %s (%d != %d)",
link.Attrs().Name, htbClass.Rate, UncappedRate)
}
if tbf.Rate != rateInBytes {
return fmt.Errorf("Rate doesn't match")
}
if tbf.Limit != limitInBytes {
return fmt.Errorf("Limit doesn't match")
}
if tbf.Buffer != bufferInBytes {
return fmt.Errorf("Buffer doesn't match")
}
}
// TODO: check subnet filters
}
return nil

View File

@ -179,7 +179,13 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.VersionsStartingFrom("0.4.0"), bv.BuildString("firewall"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
/* FIXME GC */
/* FIXME Status */
}, version.VersionsStartingFrom("0.4.0"), bv.BuildString("firewall"))
}
func cmdCheck(args *skel.CmdArgs) error {

View File

@ -205,10 +205,10 @@ var _ = Describe("firewall plugin iptables backend", func() {
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = IFNAME
err = netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: IFNAME,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
_, err = netlink.LinkByName(IFNAME)

View File

@ -37,9 +37,22 @@ import (
"github.com/containernetworking/cni/pkg/types"
current "github.com/containernetworking/cni/pkg/types/100"
"github.com/containernetworking/cni/pkg/version"
"github.com/containernetworking/plugins/pkg/utils"
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
)
type PortMapper interface {
forwardPorts(config *PortMapConf, containerNet net.IPNet) error
checkPorts(config *PortMapConf, containerNet net.IPNet) error
unforwardPorts(config *PortMapConf) error
}
// These are vars rather than consts so we can "&" them
var (
iptablesBackend = "iptables"
nftablesBackend = "nftables"
)
// PortMapEntry corresponds to a single entry in the port_mappings argument,
// see CONVENTIONS.md
type PortMapEntry struct {
@ -51,16 +64,23 @@ type PortMapEntry struct {
type PortMapConf struct {
types.NetConf
SNAT *bool `json:"snat,omitempty"`
ConditionsV4 *[]string `json:"conditionsV4"`
ConditionsV6 *[]string `json:"conditionsV6"`
MasqAll bool `json:"masqAll,omitempty"`
MarkMasqBit *int `json:"markMasqBit"`
ExternalSetMarkChain *string `json:"externalSetMarkChain"`
RuntimeConfig struct {
mapper PortMapper
// Generic config
Backend *string `json:"backend,omitempty"`
SNAT *bool `json:"snat,omitempty"`
ConditionsV4 *[]string `json:"conditionsV4"`
ConditionsV6 *[]string `json:"conditionsV6"`
MasqAll bool `json:"masqAll,omitempty"`
MarkMasqBit *int `json:"markMasqBit"`
RuntimeConfig struct {
PortMaps []PortMapEntry `json:"portMappings,omitempty"`
} `json:"runtimeConfig,omitempty"`
// iptables-backend-specific config
ExternalSetMarkChain *string `json:"externalSetMarkChain"`
// These are fields parsed out of the config or the environment;
// included here for convenience
ContainerID string `json:"-"`
@ -89,7 +109,7 @@ func cmdAdd(args *skel.CmdArgs) error {
netConf.ContainerID = args.ContainerID
if netConf.ContIPv4.IP != nil {
if err := forwardPorts(netConf, netConf.ContIPv4); err != nil {
if err := netConf.mapper.forwardPorts(netConf, netConf.ContIPv4); err != nil {
return err
}
// Delete conntrack entries for UDP to avoid conntrack blackholing traffic
@ -98,10 +118,21 @@ func cmdAdd(args *skel.CmdArgs) error {
if err := deletePortmapStaleConnections(netConf.RuntimeConfig.PortMaps, unix.AF_INET); err != nil {
log.Printf("failed to delete stale UDP conntrack entries for %s: %v", netConf.ContIPv4.IP, err)
}
if *netConf.SNAT {
// Set the route_localnet bit on the host interface, so that
// 127/8 can cross a routing boundary.
hostIfName := getRoutableHostIF(netConf.ContIPv4.IP)
if hostIfName != "" {
if err := enableLocalnetRouting(hostIfName); err != nil {
return fmt.Errorf("unable to enable route_localnet: %v", err)
}
}
}
}
if netConf.ContIPv6.IP != nil {
if err := forwardPorts(netConf, netConf.ContIPv6); err != nil {
if err := netConf.mapper.forwardPorts(netConf, netConf.ContIPv6); err != nil {
return err
}
// Delete conntrack entries for UDP to avoid conntrack blackholing traffic
@ -130,11 +161,17 @@ func cmdDel(args *skel.CmdArgs) error {
// We don't need to parse out whether or not we're using v6 or snat,
// deletion is idempotent
return unforwardPorts(netConf)
return netConf.mapper.unforwardPorts(netConf)
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("portmap"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
/* FIXME GC */
/* FIXME Status */
}, version.All, bv.BuildString("portmap"))
}
func cmdCheck(args *skel.CmdArgs) error {
@ -155,13 +192,13 @@ func cmdCheck(args *skel.CmdArgs) error {
conf.ContainerID = args.ContainerID
if conf.ContIPv4.IP != nil {
if err := checkPorts(conf, conf.ContIPv4); err != nil {
if err := conf.mapper.checkPorts(conf, conf.ContIPv4); err != nil {
return err
}
}
if conf.ContIPv6.IP != nil {
if err := checkPorts(conf, conf.ContIPv6); err != nil {
if err := conf.mapper.checkPorts(conf, conf.ContIPv6); err != nil {
return err
}
}
@ -191,6 +228,8 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, *current.Result, er
}
}
conf.mapper = &portMapperIPTables{}
if conf.SNAT == nil {
tvar := true
conf.SNAT = &tvar
@ -209,6 +248,21 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, *current.Result, er
return nil, nil, fmt.Errorf("MasqMarkBit must be between 0 and 31")
}
err := ensureBackend(&conf)
if err != nil {
return nil, nil, err
}
switch *conf.Backend {
case iptablesBackend:
conf.mapper = &portMapperIPTables{}
case nftablesBackend:
conf.mapper = &portMapperNFTables{}
default:
return nil, nil, fmt.Errorf("unrecognized backend %q", *conf.Backend)
}
// Reject invalid port numbers
for _, pm := range conf.RuntimeConfig.PortMaps {
if pm.ContainerPort <= 0 {
@ -248,3 +302,59 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, *current.Result, er
return &conf, result, nil
}
// ensureBackend validates and/or sets conf.Backend
func ensureBackend(conf *PortMapConf) error {
backendConfig := make(map[string][]string)
if conf.ExternalSetMarkChain != nil {
backendConfig[iptablesBackend] = append(backendConfig[iptablesBackend], "externalSetMarkChain")
}
if conditionsBackend := detectBackendOfConditions(conf.ConditionsV4); conditionsBackend != "" {
backendConfig[conditionsBackend] = append(backendConfig[conditionsBackend], "conditionsV4")
}
if conditionsBackend := detectBackendOfConditions(conf.ConditionsV6); conditionsBackend != "" {
backendConfig[conditionsBackend] = append(backendConfig[conditionsBackend], "conditionsV6")
}
// If backend wasn't requested explicitly, default to iptables, unless it is not
// available (and nftables is). FIXME: flip this default at some point.
if conf.Backend == nil {
if !utils.SupportsIPTables() && utils.SupportsNFTables() {
conf.Backend = &nftablesBackend
} else {
conf.Backend = &iptablesBackend
}
}
// Make sure we dont have config for the wrong backend
var wrongBackend string
if *conf.Backend == iptablesBackend {
wrongBackend = nftablesBackend
} else {
wrongBackend = iptablesBackend
}
if len(backendConfig[wrongBackend]) > 0 {
return fmt.Errorf("%s backend was requested but configuration contains %s-specific options %v", *conf.Backend, wrongBackend, backendConfig[wrongBackend])
}
// OK
return nil
}
// detectBackendOfConditions returns "iptables" if conditions contains iptables
// conditions, "nftables" if it contains nftables conditions, and "" if it is empty.
func detectBackendOfConditions(conditions *[]string) string {
if conditions == nil || len(*conditions) == 0 || (*conditions)[0] == "" {
return ""
}
// The first character of any iptables condition would either be an hyphen
// (e.g. "-d", "--sport", "-m") or an exclamation mark.
// No nftables condition would start that way. (An nftables condition might
// include a negative number, but not as the first token.)
if (*conditions)[0][0] == '-' || (*conditions)[0][0] == '!' {
return iptablesBackend
}
return nftablesBackend
}

View File

@ -25,7 +25,6 @@ import (
"github.com/vishvananda/netlink"
"github.com/containernetworking/plugins/pkg/utils"
"github.com/containernetworking/plugins/pkg/utils/sysctl"
)
// This creates the chains to be added to iptables. The basic structure is
@ -52,9 +51,11 @@ const (
OldTopLevelSNATChainName = "CNI-HOSTPORT-SNAT"
)
type portMapperIPTables struct{}
// forwardPorts establishes port forwarding to a given container IP.
// containerNet.IP can be either v4 or v6.
func forwardPorts(config *PortMapConf, containerNet net.IPNet) error {
func (*portMapperIPTables) forwardPorts(config *PortMapConf, containerNet net.IPNet) error {
isV6 := (containerNet.IP.To4() == nil)
var ipt *iptables.IPTables
@ -87,17 +88,6 @@ func forwardPorts(config *PortMapConf, containerNet net.IPNet) error {
return fmt.Errorf("unable to create chain %s: %v", setMarkChain.name, err)
}
}
if !isV6 {
// Set the route_localnet bit on the host interface, so that
// 127/8 can cross a routing boundary.
hostIfName := getRoutableHostIF(containerNet.IP)
if hostIfName != "" {
if err := enableLocalnetRouting(hostIfName); err != nil {
return fmt.Errorf("unable to enable route_localnet: %v", err)
}
}
}
}
// Generate the DNAT (actual port forwarding) rules
@ -117,7 +107,7 @@ func forwardPorts(config *PortMapConf, containerNet net.IPNet) error {
return nil
}
func checkPorts(config *PortMapConf, containerNet net.IPNet) error {
func (*portMapperIPTables) checkPorts(config *PortMapConf, containerNet net.IPNet) error {
isV6 := (containerNet.IP.To4() == nil)
dnatChain := genDnatChain(config.Name, config.ContainerID)
fillDnatRules(&dnatChain, config, containerNet)
@ -344,14 +334,6 @@ func genMarkMasqChain(markBit int) chain {
return ch
}
// 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
}
// genOldSnatChain is no longer used, but used to be created. We'll try and
// tear it down in case the plugin version changed between ADD and DEL
func genOldSnatChain(netName, containerID string) chain {
@ -372,7 +354,7 @@ func genOldSnatChain(netName, containerID string) chain {
// 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 {
func (*portMapperIPTables) unforwardPorts(config *PortMapConf) error {
dnatChain := genDnatChain(config.Name, config.ContainerID)
// Might be lying around from old versions

View File

@ -0,0 +1,252 @@
// 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"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/types"
)
var _ = Describe("portmapping configuration (iptables)", func() {
netName := "testNetName"
containerID := "icee6giejonei6sohng6ahngee7laquohquee9shiGo7fohferakah3Feiyoolu2pei7ciPhoh7shaoX6vai3vuf0ahfaeng8yohb9ceu0daez5hashee8ooYai5wa3y"
for _, ver := range []string{"0.3.0", "0.3.1", "0.4.0", "1.0.0"} {
// Redefine ver inside for scope so real value is picked up by each dynamically defined It()
// See Gingkgo's "Patterns for dynamically generating tests" documentation.
ver := ver
Describe("Generating iptables chains", func() {
Context("for DNAT", func() {
It(fmt.Sprintf("[%s] generates a correct standard container chain", ver), func() {
ch := genDnatChain(netName, containerID)
Expect(ch).To(Equal(chain{
table: "nat",
name: "CNI-DN-bfd599665540dd91d5d28",
entryChains: []string{TopLevelDNATChainName},
}))
configBytes := []byte(fmt.Sprintf(`{
"name": "test",
"type": "portmap",
"cniVersion": "%s",
"runtimeConfig": {
"portMappings": [
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"},
{ "hostPort": 8081, "containerPort": 80, "protocol": "tcp"},
{ "hostPort": 8080, "containerPort": 81, "protocol": "udp"},
{ "hostPort": 8082, "containerPort": 82, "protocol": "udp"},
{ "hostPort": 8083, "containerPort": 83, "protocol": "tcp", "hostIP": "192.168.0.2"},
{ "hostPort": 8084, "containerPort": 84, "protocol": "tcp", "hostIP": "0.0.0.0"},
{ "hostPort": 8085, "containerPort": 85, "protocol": "tcp", "hostIP": "2001:db8:a::1"},
{ "hostPort": 8086, "containerPort": 86, "protocol": "tcp", "hostIP": "::"}
]
},
"snat": true,
"conditionsV4": ["-a", "b"],
"conditionsV6": ["-c", "d"]
}`, ver))
conf, _, err := parseConfig(configBytes, "foo")
Expect(err).NotTo(HaveOccurred())
conf.ContainerID = containerID
ch = genDnatChain(conf.Name, containerID)
Expect(ch).To(Equal(chain{
table: "nat",
name: "CNI-DN-67e92b96e692a494b6b85",
entryChains: []string{"CNI-HOSTPORT-DNAT"},
}))
n, err := types.ParseCIDR("10.0.0.2/24")
Expect(err).NotTo(HaveOccurred())
fillDnatRules(&ch, conf, *n)
Expect(ch.entryRules).To(Equal([][]string{
{
"-m", "comment", "--comment",
fmt.Sprintf("dnat name: \"test\" id: \"%s\"", containerID),
"-m", "multiport",
"-p", "tcp",
"--destination-ports", "8080,8081,8083,8084,8085,8086",
"-a", "b",
},
{
"-m", "comment", "--comment",
fmt.Sprintf("dnat name: \"test\" id: \"%s\"", containerID),
"-m", "multiport",
"-p", "udp",
"--destination-ports", "8080,8082",
"-a", "b",
},
}))
Expect(ch.rules).To(Equal([][]string{
// tcp rules and not hostIP
{"-p", "tcp", "--dport", "8080", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8080", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
{"-p", "tcp", "--dport", "8081", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8081", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8081", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
// udp rules and not hostIP
{"-p", "udp", "--dport", "8080", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8080", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:81"},
{"-p", "udp", "--dport", "8082", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8082", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8082", "-j", "DNAT", "--to-destination", "10.0.0.2:82"},
// tcp rules and hostIP
{"-p", "tcp", "--dport", "8083", "-d", "192.168.0.2", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8083", "-d", "192.168.0.2", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8083", "-d", "192.168.0.2", "-j", "DNAT", "--to-destination", "10.0.0.2:83"},
// tcp rules and hostIP = "0.0.0.0"
{"-p", "tcp", "--dport", "8084", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8084", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8084", "-j", "DNAT", "--to-destination", "10.0.0.2:84"},
}))
ch.rules = nil
ch.entryRules = nil
n, err = types.ParseCIDR("2001:db8::2/64")
Expect(err).NotTo(HaveOccurred())
fillDnatRules(&ch, conf, *n)
Expect(ch.rules).To(Equal([][]string{
// tcp rules and not hostIP
{"-p", "tcp", "--dport", "8080", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "[2001:db8::2]:80"},
{"-p", "tcp", "--dport", "8081", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8081", "-j", "DNAT", "--to-destination", "[2001:db8::2]:80"},
// udp rules and not hostIP
{"-p", "udp", "--dport", "8080", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8080", "-j", "DNAT", "--to-destination", "[2001:db8::2]:81"},
{"-p", "udp", "--dport", "8082", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8082", "-j", "DNAT", "--to-destination", "[2001:db8::2]:82"},
// tcp rules and hostIP
{"-p", "tcp", "--dport", "8085", "-d", "2001:db8:a::1", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8085", "-d", "2001:db8:a::1", "-j", "DNAT", "--to-destination", "[2001:db8::2]:85"},
// tcp rules and hostIP = "::"
{"-p", "tcp", "--dport", "8086", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8086", "-j", "DNAT", "--to-destination", "[2001:db8::2]:86"},
}))
// Disable snat, generate rules
ch.rules = nil
ch.entryRules = nil
fvar := false
conf.SNAT = &fvar
n, err = types.ParseCIDR("10.0.0.2/24")
Expect(err).NotTo(HaveOccurred())
fillDnatRules(&ch, conf, *n)
Expect(ch.rules).To(Equal([][]string{
{"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
{"-p", "tcp", "--dport", "8081", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
{"-p", "udp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:81"},
{"-p", "udp", "--dport", "8082", "-j", "DNAT", "--to-destination", "10.0.0.2:82"},
{"-p", "tcp", "--dport", "8083", "-d", "192.168.0.2", "-j", "DNAT", "--to-destination", "10.0.0.2:83"},
{"-p", "tcp", "--dport", "8084", "-j", "DNAT", "--to-destination", "10.0.0.2:84"},
}))
})
It(fmt.Sprintf("[%s] generates a correct chain with external mark", ver), func() {
ch := genDnatChain(netName, containerID)
Expect(ch).To(Equal(chain{
table: "nat",
name: "CNI-DN-bfd599665540dd91d5d28",
entryChains: []string{TopLevelDNATChainName},
}))
configBytes := []byte(fmt.Sprintf(`{
"name": "test",
"type": "portmap",
"cniVersion": "%s",
"runtimeConfig": {
"portMappings": [
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
]
},
"externalSetMarkChain": "PLZ-SET-MARK",
"conditionsV4": ["-a", "b"],
"conditionsV6": ["-c", "d"]
}`, ver))
conf, _, err := parseConfig(configBytes, "foo")
Expect(err).NotTo(HaveOccurred())
conf.ContainerID = containerID
ch = genDnatChain(conf.Name, containerID)
n, err := types.ParseCIDR("10.0.0.2/24")
Expect(err).NotTo(HaveOccurred())
fillDnatRules(&ch, conf, *n)
Expect(ch.rules).To(Equal([][]string{
{"-p", "tcp", "--dport", "8080", "-s", "10.0.0.2/24", "-j", "PLZ-SET-MARK"},
{"-p", "tcp", "--dport", "8080", "-s", "127.0.0.1", "-j", "PLZ-SET-MARK"},
{"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
}))
})
It(fmt.Sprintf("[%s] generates a correct top-level chain", ver), func() {
ch := genToplevelDnatChain()
Expect(ch).To(Equal(chain{
table: "nat",
name: "CNI-HOSTPORT-DNAT",
entryChains: []string{"PREROUTING", "OUTPUT"},
entryRules: [][]string{{"-m", "addrtype", "--dst-type", "LOCAL"}},
}))
})
It(fmt.Sprintf("[%s] generates the correct mark chains", ver), func() {
masqBit := 5
ch := genSetMarkChain(masqBit)
Expect(ch).To(Equal(chain{
table: "nat",
name: "CNI-HOSTPORT-SETMARK",
rules: [][]string{{
"-m", "comment",
"--comment", "CNI portfwd masquerade mark",
"-j", "MARK",
"--set-xmark", "0x20/0x20",
}},
}))
ch = genMarkMasqChain(masqBit)
Expect(ch).To(Equal(chain{
table: "nat",
name: "CNI-HOSTPORT-MASQ",
entryChains: []string{"POSTROUTING"},
entryRules: [][]string{{
"-m", "comment",
"--comment", "CNI portfwd requiring masquerade",
}},
rules: [][]string{{
"-m", "mark",
"--mark", "0x20/0x20",
"-j", "MASQUERADE",
}},
prependEntry: true,
}))
})
})
})
}
})

View File

@ -0,0 +1,340 @@
// Copyright 2023 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 (
"context"
"fmt"
"net"
"strconv"
"sigs.k8s.io/knftables"
)
const (
tableName = "cni_hostport"
hostIPHostPortsChain = "hostip_hostports"
hostPortsChain = "hostports"
masqueradingChain = "masquerading"
)
// The nftables portmap implementation is fairly similar to the iptables implementation:
// we add a rule for each mapping, with a comment containing a hash of the container ID,
// so that we can later reliably delete the rules we want. (This is important because in
// edge cases, it's possible the plugin might see "ADD container A with IP 192.168.1.3",
// followed by "ADD container B with IP 192.168.1.3" followed by "DEL container A with IP
// 192.168.1.3", and we need to make sure that the DEL causes us to delete the rule for
// container A, and not the rule for container B.) This iptables implementation actually
// uses a separate chain per container but there's not really any need for that...
//
// As with pkg/ip/ipmasq_nftables_linux.go, it would be more nftables-y to have a chain
// with a single rule doing a lookup against a map with an element per mapping, rather
// than having a chain with a rule per mapping. But there's no easy, non-racy way to say
// "delete the element 192.168.1.3 from the map, but only if it was added for container A,
// not if it was added for container B".
type portMapperNFTables struct {
ipv4 knftables.Interface
ipv6 knftables.Interface
}
// getPortMapNFT creates an nftables.Interface for port mapping for the IP family of ipn
func (pmNFT *portMapperNFTables) getPortMapNFT(ipv6 bool) (knftables.Interface, error) {
var err error
if ipv6 {
if pmNFT.ipv6 == nil {
pmNFT.ipv6, err = knftables.New(knftables.IPv6Family, tableName)
if err != nil {
return nil, err
}
}
return pmNFT.ipv6, nil
}
if pmNFT.ipv4 == nil {
pmNFT.ipv4, err = knftables.New(knftables.IPv4Family, tableName)
if err != nil {
return nil, err
}
}
return pmNFT.ipv4, err
}
// forwardPorts establishes port forwarding to a given container IP.
// containerNet.IP can be either v4 or v6.
func (pmNFT *portMapperNFTables) forwardPorts(config *PortMapConf, containerNet net.IPNet) error {
isV6 := (containerNet.IP.To4() == nil)
nft, err := pmNFT.getPortMapNFT(isV6)
if err != nil {
return err
}
var ipX string
var conditions []string
if isV6 {
ipX = "ip6"
if config.ConditionsV6 != nil {
conditions = *config.ConditionsV6
}
} else if !isV6 {
ipX = "ip"
if config.ConditionsV4 != nil {
conditions = *config.ConditionsV4
}
}
tx := nft.NewTransaction()
// Ensure basic rule structure
tx.Add(&knftables.Table{
Comment: knftables.PtrTo("CNI portmap plugin"),
})
tx.Add(&knftables.Chain{
Name: "hostports",
})
tx.Add(&knftables.Chain{
Name: "hostip_hostports",
})
tx.Add(&knftables.Chain{
Name: "prerouting",
Type: knftables.PtrTo(knftables.NATType),
Hook: knftables.PtrTo(knftables.PreroutingHook),
Priority: knftables.PtrTo(knftables.DNATPriority),
})
tx.Flush(&knftables.Chain{
Name: "prerouting",
})
tx.Add(&knftables.Rule{
Chain: "prerouting",
Rule: knftables.Concat(
conditions,
"jump", hostIPHostPortsChain,
),
})
tx.Add(&knftables.Rule{
Chain: "prerouting",
Rule: knftables.Concat(
conditions,
"jump", hostPortsChain,
),
})
tx.Add(&knftables.Chain{
Name: "output",
Type: knftables.PtrTo(knftables.NATType),
Hook: knftables.PtrTo(knftables.OutputHook),
Priority: knftables.PtrTo(knftables.DNATPriority),
})
tx.Flush(&knftables.Chain{
Name: "output",
})
tx.Add(&knftables.Rule{
Chain: "output",
Rule: knftables.Concat(
conditions,
"jump", hostIPHostPortsChain,
),
})
tx.Add(&knftables.Rule{
Chain: "output",
Rule: knftables.Concat(
conditions,
"fib daddr type local",
"jump", hostPortsChain,
),
})
if *config.SNAT {
tx.Add(&knftables.Chain{
Name: masqueradingChain,
Type: knftables.PtrTo(knftables.NATType),
Hook: knftables.PtrTo(knftables.PostroutingHook),
Priority: knftables.PtrTo(knftables.SNATPriority),
})
}
// Set up this container
for _, e := range config.RuntimeConfig.PortMaps {
useHostIP := false
if e.HostIP != "" {
hostIP := net.ParseIP(e.HostIP)
isHostV6 := (hostIP.To4() == nil)
// Ignore wrong-IP-family HostIPs
if isV6 != isHostV6 {
continue
}
// Unspecified addresses cannot be used as destination
useHostIP = !hostIP.IsUnspecified()
}
if useHostIP {
tx.Add(&knftables.Rule{
Chain: hostIPHostPortsChain,
Rule: knftables.Concat(
ipX, "daddr", e.HostIP,
e.Protocol, "dport", e.HostPort,
"dnat to", net.JoinHostPort(containerNet.IP.String(), strconv.Itoa(e.ContainerPort)),
),
Comment: &config.ContainerID,
})
} else {
tx.Add(&knftables.Rule{
Chain: hostPortsChain,
Rule: knftables.Concat(
e.Protocol, "dport", e.HostPort,
"dnat to", net.JoinHostPort(containerNet.IP.String(), strconv.Itoa(e.ContainerPort)),
),
Comment: &config.ContainerID,
})
}
}
if *config.SNAT {
// Add mark-to-masquerade rules for hairpin and localhost
// In theory we should validate that the original dst IP and port are as
// expected, but *any* traffic matching one of these patterns would need
// to be masqueraded to be able to work correctly anyway.
tx.Add(&knftables.Rule{
Chain: masqueradingChain,
Rule: knftables.Concat(
ipX, "saddr", containerNet.IP,
ipX, "daddr", containerNet.IP,
"masquerade",
),
Comment: &config.ContainerID,
})
if !isV6 {
tx.Add(&knftables.Rule{
Chain: masqueradingChain,
Rule: knftables.Concat(
ipX, "saddr 127.0.0.1",
ipX, "daddr", containerNet.IP,
"masquerade",
),
Comment: &config.ContainerID,
})
}
}
err = nft.Run(context.TODO(), tx)
if err != nil {
return fmt.Errorf("unable to set up nftables rules for port mappings: %v", err)
}
return nil
}
func (pmNFT *portMapperNFTables) checkPorts(config *PortMapConf, containerNet net.IPNet) error {
isV6 := (containerNet.IP.To4() == nil)
var hostPorts, hostIPHostPorts, masqueradings int
for _, e := range config.RuntimeConfig.PortMaps {
if e.HostIP != "" {
hostIPHostPorts++
} else {
hostPorts++
}
}
if *config.SNAT {
masqueradings = len(config.RuntimeConfig.PortMaps)
if isV6 {
masqueradings *= 2
}
}
nft, err := pmNFT.getPortMapNFT(isV6)
if err != nil {
return err
}
if hostPorts > 0 {
err := checkPortsAgainstRules(nft, hostPortsChain, config.ContainerID, hostPorts)
if err != nil {
return err
}
}
if hostIPHostPorts > 0 {
err := checkPortsAgainstRules(nft, hostIPHostPortsChain, config.ContainerID, hostIPHostPorts)
if err != nil {
return err
}
}
if masqueradings > 0 {
err := checkPortsAgainstRules(nft, masqueradingChain, config.ContainerID, masqueradings)
if err != nil {
return err
}
}
return nil
}
func checkPortsAgainstRules(nft knftables.Interface, chain, comment string, nPorts int) error {
rules, err := nft.ListRules(context.TODO(), chain)
if err != nil {
return err
}
found := 0
for _, r := range rules {
if r.Comment != nil && *r.Comment == comment {
found++
}
}
if found < nPorts {
return fmt.Errorf("missing hostport rules in %q chain", chain)
}
return nil
}
// unforwardPorts deletes any nftables rules created by this plugin.
// It should be idempotent - it will not error if the chain does not exist.
func (pmNFT *portMapperNFTables) unforwardPorts(config *PortMapConf) error {
// Always clear both IPv4 and IPv6, just to be sure
for _, family := range []knftables.Family{knftables.IPv4Family, knftables.IPv6Family} {
nft, err := pmNFT.getPortMapNFT(family == knftables.IPv6Family)
if err != nil {
continue
}
tx := nft.NewTransaction()
for _, chain := range []string{hostPortsChain, hostIPHostPortsChain, masqueradingChain} {
rules, err := nft.ListRules(context.TODO(), chain)
if err != nil {
if knftables.IsNotFound(err) {
continue
}
return fmt.Errorf("could not list rules in table %s: %w", tableName, err)
}
for _, r := range rules {
if r.Comment != nil && *r.Comment == config.ContainerID {
tx.Delete(r)
}
}
}
err = nft.Run(context.TODO(), tx)
if err != nil {
return fmt.Errorf("error deleting nftables rules: %w", err)
}
}
return nil
}

View File

@ -0,0 +1,134 @@
// Copyright 2023 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/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"sigs.k8s.io/knftables"
"github.com/containernetworking/cni/pkg/types"
)
var _ = Describe("portmapping configuration (nftables)", func() {
containerID := "icee6giejonei6so"
for _, ver := range []string{"0.3.0", "0.3.1", "0.4.0", "1.0.0"} {
// Redefine ver inside for scope so real value is picked up by each dynamically defined It()
// See Gingkgo's "Patterns for dynamically generating tests" documentation.
ver := ver
Describe("nftables rules", func() {
var pmNFT *portMapperNFTables
var ipv4Fake, ipv6Fake *knftables.Fake
BeforeEach(func() {
ipv4Fake = knftables.NewFake(knftables.IPv4Family, tableName)
ipv6Fake = knftables.NewFake(knftables.IPv6Family, tableName)
pmNFT = &portMapperNFTables{
ipv4: ipv4Fake,
ipv6: ipv6Fake,
}
})
It(fmt.Sprintf("[%s] generates correct rules on ADD", ver), func() {
configBytes := []byte(fmt.Sprintf(`{
"name": "test",
"type": "portmap",
"cniVersion": "%s",
"backend": "nftables",
"runtimeConfig": {
"portMappings": [
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"},
{ "hostPort": 8081, "containerPort": 80, "protocol": "tcp"},
{ "hostPort": 8080, "containerPort": 81, "protocol": "udp"},
{ "hostPort": 8082, "containerPort": 82, "protocol": "udp"},
{ "hostPort": 8083, "containerPort": 83, "protocol": "tcp", "hostIP": "192.168.0.2"},
{ "hostPort": 8084, "containerPort": 84, "protocol": "tcp", "hostIP": "0.0.0.0"},
{ "hostPort": 8085, "containerPort": 85, "protocol": "tcp", "hostIP": "2001:db8:a::1"},
{ "hostPort": 8086, "containerPort": 86, "protocol": "tcp", "hostIP": "::"}
]
},
"snat": true,
"conditionsV4": ["a", "b"],
"conditionsV6": ["c", "d"]
}`, ver))
conf, _, err := parseConfig(configBytes, "foo")
Expect(err).NotTo(HaveOccurred())
conf.ContainerID = containerID
containerNet, err := types.ParseCIDR("10.0.0.2/24")
Expect(err).NotTo(HaveOccurred())
err = pmNFT.forwardPorts(conf, *containerNet)
Expect(err).NotTo(HaveOccurred())
expectedRules := strings.TrimSpace(`
add table ip cni_hostport { comment "CNI portmap plugin" ; }
add chain ip cni_hostport hostip_hostports
add chain ip cni_hostport hostports
add chain ip cni_hostport masquerading { type nat hook postrouting priority 100 ; }
add chain ip cni_hostport output { type nat hook output priority -100 ; }
add chain ip cni_hostport prerouting { type nat hook prerouting priority -100 ; }
add rule ip cni_hostport hostip_hostports ip daddr 192.168.0.2 tcp dport 8083 dnat to 10.0.0.2:83 comment "icee6giejonei6so"
add rule ip cni_hostport hostports tcp dport 8080 dnat to 10.0.0.2:80 comment "icee6giejonei6so"
add rule ip cni_hostport hostports tcp dport 8081 dnat to 10.0.0.2:80 comment "icee6giejonei6so"
add rule ip cni_hostport hostports udp dport 8080 dnat to 10.0.0.2:81 comment "icee6giejonei6so"
add rule ip cni_hostport hostports udp dport 8082 dnat to 10.0.0.2:82 comment "icee6giejonei6so"
add rule ip cni_hostport hostports tcp dport 8084 dnat to 10.0.0.2:84 comment "icee6giejonei6so"
add rule ip cni_hostport masquerading ip saddr 10.0.0.2 ip daddr 10.0.0.2 masquerade comment "icee6giejonei6so"
add rule ip cni_hostport masquerading ip saddr 127.0.0.1 ip daddr 10.0.0.2 masquerade comment "icee6giejonei6so"
add rule ip cni_hostport output a b jump hostip_hostports
add rule ip cni_hostport output a b fib daddr type local jump hostports
add rule ip cni_hostport prerouting a b jump hostip_hostports
add rule ip cni_hostport prerouting a b jump hostports
`)
actualRules := strings.TrimSpace(ipv4Fake.Dump())
Expect(actualRules).To(Equal(expectedRules))
// Disable snat, generate IPv6 rules
*conf.SNAT = false
containerNet, err = types.ParseCIDR("2001:db8::2/64")
Expect(err).NotTo(HaveOccurred())
err = pmNFT.forwardPorts(conf, *containerNet)
Expect(err).NotTo(HaveOccurred())
expectedRules = strings.TrimSpace(`
add table ip6 cni_hostport { comment "CNI portmap plugin" ; }
add chain ip6 cni_hostport hostip_hostports
add chain ip6 cni_hostport hostports
add chain ip6 cni_hostport output { type nat hook output priority -100 ; }
add chain ip6 cni_hostport prerouting { type nat hook prerouting priority -100 ; }
add rule ip6 cni_hostport hostip_hostports ip6 daddr 2001:db8:a::1 tcp dport 8085 dnat to [2001:db8::2]:85 comment "icee6giejonei6so"
add rule ip6 cni_hostport hostports tcp dport 8080 dnat to [2001:db8::2]:80 comment "icee6giejonei6so"
add rule ip6 cni_hostport hostports tcp dport 8081 dnat to [2001:db8::2]:80 comment "icee6giejonei6so"
add rule ip6 cni_hostport hostports udp dport 8080 dnat to [2001:db8::2]:81 comment "icee6giejonei6so"
add rule ip6 cni_hostport hostports udp dport 8082 dnat to [2001:db8::2]:82 comment "icee6giejonei6so"
add rule ip6 cni_hostport hostports tcp dport 8086 dnat to [2001:db8::2]:86 comment "icee6giejonei6so"
add rule ip6 cni_hostport output c d jump hostip_hostports
add rule ip6 cni_hostport output c d fib daddr type local jump hostports
add rule ip6 cni_hostport prerouting c d jump hostip_hostports
add rule ip6 cni_hostport prerouting c d jump hostports
`)
actualRules = strings.TrimSpace(ipv6Fake.Dump())
Expect(actualRules).To(Equal(expectedRules))
})
})
}
})

View File

@ -24,9 +24,6 @@ import (
)
var _ = Describe("portmapping configuration", func() {
netName := "testNetName"
containerID := "icee6giejonei6sohng6ahngee7laquohquee9shiGo7fohferakah3Feiyoolu2pei7ciPhoh7shaoX6vai3vuf0ahfaeng8yohb9ceu0daez5hashee8ooYai5wa3y"
for _, ver := range []string{"0.3.0", "0.3.1", "0.4.0", "1.0.0"} {
// Redefine ver inside for scope so real value is picked up by each dynamically defined It()
// See Gingkgo's "Patterns for dynamically generating tests" documentation.
@ -38,6 +35,7 @@ var _ = Describe("portmapping configuration", func() {
"name": "test",
"type": "portmap",
"cniVersion": "%s",
"backend": "iptables",
"runtimeConfig": {
"portMappings": [
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"},
@ -45,8 +43,8 @@ var _ = Describe("portmapping configuration", func() {
]
},
"snat": false,
"conditionsV4": ["a", "b"],
"conditionsV6": ["c", "d"],
"conditionsV4": ["-s", "1.2.3.4"],
"conditionsV6": ["!", "-s", "12::34"],
"prevResult": {
"interfaces": [
{"name": "host"},
@ -77,8 +75,8 @@ var _ = Describe("portmapping configuration", func() {
c, _, err := parseConfig(configBytes, "container")
Expect(err).NotTo(HaveOccurred())
Expect(c.CNIVersion).To(Equal(ver))
Expect(c.ConditionsV4).To(Equal(&[]string{"a", "b"}))
Expect(c.ConditionsV6).To(Equal(&[]string{"c", "d"}))
Expect(c.ConditionsV4).To(Equal(&[]string{"-s", "1.2.3.4"}))
Expect(c.ConditionsV6).To(Equal(&[]string{"!", "-s", "12::34"}))
fvar := false
Expect(c.SNAT).To(Equal(&fvar))
Expect(c.Name).To(Equal("test"))
@ -97,15 +95,16 @@ var _ = Describe("portmapping configuration", func() {
"name": "test",
"type": "portmap",
"cniVersion": "%s",
"backend": "iptables",
"snat": false,
"conditionsV4": ["a", "b"],
"conditionsV6": ["c", "d"]
"conditionsV4": ["-s", "1.2.3.4"],
"conditionsV6": ["-s", "12::34"]
}`, ver))
c, _, err := parseConfig(configBytes, "container")
Expect(err).NotTo(HaveOccurred())
Expect(c.CNIVersion).To(Equal(ver))
Expect(c.ConditionsV4).To(Equal(&[]string{"a", "b"}))
Expect(c.ConditionsV6).To(Equal(&[]string{"c", "d"}))
Expect(c.ConditionsV4).To(Equal(&[]string{"-s", "1.2.3.4"}))
Expect(c.ConditionsV6).To(Equal(&[]string{"-s", "12::34"}))
fvar := false
Expect(c.SNAT).To(Equal(&fvar))
Expect(c.Name).To(Equal("test"))
@ -116,9 +115,10 @@ var _ = Describe("portmapping configuration", func() {
"name": "test",
"type": "portmap",
"cniVersion": "%s",
"backend": "iptables",
"snat": false,
"conditionsV4": ["a", "b"],
"conditionsV6": ["c", "d"],
"conditionsV4": ["-s", "1.2.3.4"],
"conditionsV6": ["-s", "12::34"],
"runtimeConfig": {
"portMappings": [
{ "hostPort": 0, "containerPort": 80, "protocol": "tcp"}
@ -129,6 +129,82 @@ var _ = Describe("portmapping configuration", func() {
Expect(err).To(MatchError("Invalid host port number: 0"))
})
It(fmt.Sprintf("[%s] defaults to iptables when backend is not specified", ver), func() {
// "defaults to iptables" is only true if iptables is installed
// (or if neither iptables nor nftables is installed), but the
// other unit tests would fail if iptables wasn't installed, so
// we know it must be.
configBytes := []byte(fmt.Sprintf(`{
"name": "test",
"type": "portmap",
"cniVersion": "%s"
}`, ver))
c, _, err := parseConfig(configBytes, "container")
Expect(err).NotTo(HaveOccurred())
Expect(c.CNIVersion).To(Equal(ver))
Expect(c.Backend).To(Equal(&iptablesBackend))
Expect(c.Name).To(Equal("test"))
})
It(fmt.Sprintf("[%s] uses nftables if requested", ver), func() {
configBytes := []byte(fmt.Sprintf(`{
"name": "test",
"type": "portmap",
"cniVersion": "%s",
"backend": "nftables"
}`, ver))
c, _, err := parseConfig(configBytes, "container")
Expect(err).NotTo(HaveOccurred())
Expect(c.CNIVersion).To(Equal(ver))
Expect(c.Backend).To(Equal(&nftablesBackend))
Expect(c.Name).To(Equal("test"))
})
It(fmt.Sprintf("[%s] allows nftables conditions if nftables is requested", ver), func() {
configBytes := []byte(fmt.Sprintf(`{
"name": "test",
"type": "portmap",
"cniVersion": "%s",
"backend": "nftables",
"conditionsV4": ["ip", "saddr", "1.2.3.4"],
"conditionsV6": ["ip6", "saddr", "12::34"]
}`, ver))
c, _, err := parseConfig(configBytes, "container")
Expect(err).NotTo(HaveOccurred())
Expect(c.CNIVersion).To(Equal(ver))
Expect(c.Backend).To(Equal(&nftablesBackend))
Expect(c.ConditionsV4).To(Equal(&[]string{"ip", "saddr", "1.2.3.4"}))
Expect(c.ConditionsV6).To(Equal(&[]string{"ip6", "saddr", "12::34"}))
Expect(c.Name).To(Equal("test"))
})
It(fmt.Sprintf("[%s] rejects nftables options with 'backend: iptables'", ver), func() {
configBytes := []byte(fmt.Sprintf(`{
"name": "test",
"type": "portmap",
"cniVersion": "%s",
"backend": "iptables",
"conditionsV4": ["ip", "saddr", "1.2.3.4"],
"conditionsV6": ["ip6", "saddr", "12::34"]
}`, ver))
_, _, err := parseConfig(configBytes, "container")
Expect(err).To(MatchError("iptables backend was requested but configuration contains nftables-specific options [conditionsV4 conditionsV6]"))
})
It(fmt.Sprintf("[%s] rejects iptables options with 'backend: nftables'", ver), func() {
configBytes := []byte(fmt.Sprintf(`{
"name": "test",
"type": "portmap",
"cniVersion": "%s",
"backend": "nftables",
"externalSetMarkChain": "KUBE-MARK-MASQ",
"conditionsV4": ["-s", "1.2.3.4"],
"conditionsV6": ["-s", "12::34"]
}`, ver))
_, _, err := parseConfig(configBytes, "container")
Expect(err).To(MatchError("nftables backend was requested but configuration contains iptables-specific options [externalSetMarkChain conditionsV4 conditionsV6]"))
})
It(fmt.Sprintf("[%s] does not fail on missing prevResult interface index", ver), func() {
configBytes := []byte(fmt.Sprintf(`{
"name": "test",
@ -139,7 +215,7 @@ var _ = Describe("portmapping configuration", func() {
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
]
},
"conditionsV4": ["a", "b"],
"conditionsV4": ["-s", "1.2.3.4"],
"prevResult": {
"interfaces": [
{"name": "host"}
@ -157,222 +233,5 @@ var _ = Describe("portmapping configuration", func() {
Expect(err).NotTo(HaveOccurred())
})
})
Describe("Generating chains", func() {
Context("for DNAT", func() {
It(fmt.Sprintf("[%s] generates a correct standard container chain", ver), func() {
ch := genDnatChain(netName, containerID)
Expect(ch).To(Equal(chain{
table: "nat",
name: "CNI-DN-bfd599665540dd91d5d28",
entryChains: []string{TopLevelDNATChainName},
}))
configBytes := []byte(fmt.Sprintf(`{
"name": "test",
"type": "portmap",
"cniVersion": "%s",
"runtimeConfig": {
"portMappings": [
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"},
{ "hostPort": 8081, "containerPort": 80, "protocol": "tcp"},
{ "hostPort": 8080, "containerPort": 81, "protocol": "udp"},
{ "hostPort": 8082, "containerPort": 82, "protocol": "udp"},
{ "hostPort": 8083, "containerPort": 83, "protocol": "tcp", "hostIP": "192.168.0.2"},
{ "hostPort": 8084, "containerPort": 84, "protocol": "tcp", "hostIP": "0.0.0.0"},
{ "hostPort": 8085, "containerPort": 85, "protocol": "tcp", "hostIP": "2001:db8:a::1"},
{ "hostPort": 8086, "containerPort": 86, "protocol": "tcp", "hostIP": "::"}
]
},
"snat": true,
"conditionsV4": ["a", "b"],
"conditionsV6": ["c", "d"]
}`, ver))
conf, _, err := parseConfig(configBytes, "foo")
Expect(err).NotTo(HaveOccurred())
conf.ContainerID = containerID
ch = genDnatChain(conf.Name, containerID)
Expect(ch).To(Equal(chain{
table: "nat",
name: "CNI-DN-67e92b96e692a494b6b85",
entryChains: []string{"CNI-HOSTPORT-DNAT"},
}))
n, err := types.ParseCIDR("10.0.0.2/24")
Expect(err).NotTo(HaveOccurred())
fillDnatRules(&ch, conf, *n)
Expect(ch.entryRules).To(Equal([][]string{
{
"-m", "comment", "--comment",
fmt.Sprintf("dnat name: \"test\" id: \"%s\"", containerID),
"-m", "multiport",
"-p", "tcp",
"--destination-ports", "8080,8081,8083,8084,8085,8086",
"a", "b",
},
{
"-m", "comment", "--comment",
fmt.Sprintf("dnat name: \"test\" id: \"%s\"", containerID),
"-m", "multiport",
"-p", "udp",
"--destination-ports", "8080,8082",
"a", "b",
},
}))
Expect(ch.rules).To(Equal([][]string{
// tcp rules and not hostIP
{"-p", "tcp", "--dport", "8080", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8080", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
{"-p", "tcp", "--dport", "8081", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8081", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8081", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
// udp rules and not hostIP
{"-p", "udp", "--dport", "8080", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8080", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:81"},
{"-p", "udp", "--dport", "8082", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8082", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8082", "-j", "DNAT", "--to-destination", "10.0.0.2:82"},
// tcp rules and hostIP
{"-p", "tcp", "--dport", "8083", "-d", "192.168.0.2", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8083", "-d", "192.168.0.2", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8083", "-d", "192.168.0.2", "-j", "DNAT", "--to-destination", "10.0.0.2:83"},
// tcp rules and hostIP = "0.0.0.0"
{"-p", "tcp", "--dport", "8084", "-s", "10.0.0.2/24", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8084", "-s", "127.0.0.1", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8084", "-j", "DNAT", "--to-destination", "10.0.0.2:84"},
}))
ch.rules = nil
ch.entryRules = nil
n, err = types.ParseCIDR("2001:db8::2/64")
Expect(err).NotTo(HaveOccurred())
fillDnatRules(&ch, conf, *n)
Expect(ch.rules).To(Equal([][]string{
// tcp rules and not hostIP
{"-p", "tcp", "--dport", "8080", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "[2001:db8::2]:80"},
{"-p", "tcp", "--dport", "8081", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8081", "-j", "DNAT", "--to-destination", "[2001:db8::2]:80"},
// udp rules and not hostIP
{"-p", "udp", "--dport", "8080", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8080", "-j", "DNAT", "--to-destination", "[2001:db8::2]:81"},
{"-p", "udp", "--dport", "8082", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "udp", "--dport", "8082", "-j", "DNAT", "--to-destination", "[2001:db8::2]:82"},
// tcp rules and hostIP
{"-p", "tcp", "--dport", "8085", "-d", "2001:db8:a::1", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8085", "-d", "2001:db8:a::1", "-j", "DNAT", "--to-destination", "[2001:db8::2]:85"},
// tcp rules and hostIP = "::"
{"-p", "tcp", "--dport", "8086", "-s", "2001:db8::2/64", "-j", "CNI-HOSTPORT-SETMARK"},
{"-p", "tcp", "--dport", "8086", "-j", "DNAT", "--to-destination", "[2001:db8::2]:86"},
}))
// Disable snat, generate rules
ch.rules = nil
ch.entryRules = nil
fvar := false
conf.SNAT = &fvar
n, err = types.ParseCIDR("10.0.0.2/24")
Expect(err).NotTo(HaveOccurred())
fillDnatRules(&ch, conf, *n)
Expect(ch.rules).To(Equal([][]string{
{"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
{"-p", "tcp", "--dport", "8081", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
{"-p", "udp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:81"},
{"-p", "udp", "--dport", "8082", "-j", "DNAT", "--to-destination", "10.0.0.2:82"},
{"-p", "tcp", "--dport", "8083", "-d", "192.168.0.2", "-j", "DNAT", "--to-destination", "10.0.0.2:83"},
{"-p", "tcp", "--dport", "8084", "-j", "DNAT", "--to-destination", "10.0.0.2:84"},
}))
})
It(fmt.Sprintf("[%s] generates a correct chain with external mark", ver), func() {
ch := genDnatChain(netName, containerID)
Expect(ch).To(Equal(chain{
table: "nat",
name: "CNI-DN-bfd599665540dd91d5d28",
entryChains: []string{TopLevelDNATChainName},
}))
configBytes := []byte(fmt.Sprintf(`{
"name": "test",
"type": "portmap",
"cniVersion": "%s",
"runtimeConfig": {
"portMappings": [
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
]
},
"externalSetMarkChain": "PLZ-SET-MARK",
"conditionsV4": ["a", "b"],
"conditionsV6": ["c", "d"]
}`, ver))
conf, _, err := parseConfig(configBytes, "foo")
Expect(err).NotTo(HaveOccurred())
conf.ContainerID = containerID
ch = genDnatChain(conf.Name, containerID)
n, err := types.ParseCIDR("10.0.0.2/24")
Expect(err).NotTo(HaveOccurred())
fillDnatRules(&ch, conf, *n)
Expect(ch.rules).To(Equal([][]string{
{"-p", "tcp", "--dport", "8080", "-s", "10.0.0.2/24", "-j", "PLZ-SET-MARK"},
{"-p", "tcp", "--dport", "8080", "-s", "127.0.0.1", "-j", "PLZ-SET-MARK"},
{"-p", "tcp", "--dport", "8080", "-j", "DNAT", "--to-destination", "10.0.0.2:80"},
}))
})
It(fmt.Sprintf("[%s] generates a correct top-level chain", ver), func() {
ch := genToplevelDnatChain()
Expect(ch).To(Equal(chain{
table: "nat",
name: "CNI-HOSTPORT-DNAT",
entryChains: []string{"PREROUTING", "OUTPUT"},
entryRules: [][]string{{"-m", "addrtype", "--dst-type", "LOCAL"}},
}))
})
It(fmt.Sprintf("[%s] generates the correct mark chains", ver), func() {
masqBit := 5
ch := genSetMarkChain(masqBit)
Expect(ch).To(Equal(chain{
table: "nat",
name: "CNI-HOSTPORT-SETMARK",
rules: [][]string{{
"-m", "comment",
"--comment", "CNI portfwd masquerade mark",
"-j", "MARK",
"--set-xmark", "0x20/0x20",
}},
}))
ch = genMarkMasqChain(masqBit)
Expect(ch).To(Equal(chain{
table: "nat",
name: "CNI-HOSTPORT-MASQ",
entryChains: []string{"POSTROUTING"},
entryRules: [][]string{{
"-m", "comment",
"--comment", "CNI portfwd requiring masquerade",
}},
rules: [][]string{{
"-m", "mark",
"--mark", "0x20/0x20",
"-j", "MASQUERADE",
}},
prependEntry: true,
}))
})
})
})
}
})

View File

@ -21,6 +21,8 @@ import (
"strings"
"github.com/vishvananda/netlink"
"github.com/containernetworking/plugins/pkg/utils/sysctl"
)
// fmtIpPort correctly formats ip:port literals for iptables and ip6tables -
@ -52,6 +54,14 @@ func getRoutableHostIF(containerIP net.IP) string {
return ""
}
// 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
}
// groupByProto groups port numbers by protocol
func groupByProto(entries []PortMapEntry) map[string][]int {
if len(entries) == 0 {

View File

@ -47,6 +47,7 @@ type PluginConf struct {
PrevResult *current.Result `json:"-"`
// Add plugin-specific flags here
Table *int `json:"table,omitempty"`
}
// Wrapper that does a lock before and unlock after operations to serialise
@ -163,6 +164,9 @@ func cmdAdd(args *skel.CmdArgs) error {
// Do the actual work.
err = withLockAndNetNS(args.Netns, func(_ ns.NetNS) error {
if conf.Table != nil {
return doRoutesWithTable(ipCfgs, *conf.Table)
}
return doRoutes(ipCfgs, args.IfName)
})
if err != nil {
@ -330,31 +334,73 @@ func doRoutes(ipCfgs []*current.IPConfig, iface string) error {
return nil
}
func doRoutesWithTable(ipCfgs []*current.IPConfig, table int) error {
for _, ipCfg := range ipCfgs {
log.Printf("Set rule for source %s", ipCfg.String())
rule := netlink.NewRule()
rule.Table = table
// Source must be restricted to a single IP, not a full subnet
var src net.IPNet
src.IP = ipCfg.Address.IP
if src.IP.To4() != nil {
src.Mask = net.CIDRMask(32, 32)
} else {
src.Mask = net.CIDRMask(128, 128)
}
log.Printf("Source to use %s", src.String())
rule.Src = &src
if err := netlink.RuleAdd(rule); err != nil {
return fmt.Errorf("failed to add rule: %v", err)
}
}
return nil
}
// cmdDel is called for DELETE requests
func cmdDel(args *skel.CmdArgs) error {
// We care a bit about config because it sets log level.
_, err := parseConfig(args.StdinData)
conf, err := parseConfig(args.StdinData)
if err != nil {
return err
}
log.Printf("Cleaning up SBR for %s", args.IfName)
err = withLockAndNetNS(args.Netns, func(_ ns.NetNS) error {
return tidyRules(args.IfName)
return tidyRules(args.IfName, conf.Table)
})
return err
}
// Tidy up the rules for the deleted interface
func tidyRules(iface string) error {
func tidyRules(iface string, table *int) error {
// We keep on going on rule deletion error, but return the last failure.
var errReturn error
var err error
var rules []netlink.Rule
rules, err := netlink.RuleList(netlink.FAMILY_ALL)
if err != nil {
log.Printf("Failed to list all rules to tidy: %v", err)
return fmt.Errorf("Failed to list all rules to tidy: %v", err)
if table != nil {
rules, err = netlink.RuleListFiltered(
netlink.FAMILY_ALL,
&netlink.Rule{
Table: *table,
},
netlink.RT_FILTER_TABLE,
)
if err != nil {
log.Printf("Failed to list rules of table %d to tidy: %v", *table, err)
return fmt.Errorf("failed to list rules of table %d to tidy: %v", *table, err)
}
} else {
rules, err = netlink.RuleList(netlink.FAMILY_ALL)
if err != nil {
log.Printf("Failed to list all rules to tidy: %v", err)
return fmt.Errorf("Failed to list all rules to tidy: %v", err)
}
}
link, err := netlink.LinkByName(iface)
@ -401,7 +447,13 @@ RULE_LOOP:
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("sbr"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
/* FIXME GC */
/* FIXME Status */
}, version.All, bv.BuildString("sbr"))
}
func cmdCheck(_ *skel.CmdArgs) error {

View File

@ -53,7 +53,9 @@ func setup(targetNs ns.NetNS, status netStatus) error {
err := targetNs.Do(func(_ ns.NetNS) error {
for _, dev := range status.Devices {
log.Printf("Adding dev %s\n", dev.Name)
link := &netlink.Dummy{LinkAttrs: netlink.LinkAttrs{Name: dev.Name}}
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = dev.Name
link := &netlink.Dummy{LinkAttrs: linkAttrs}
err := netlink.LinkAdd(link)
if err != nil {
return err
@ -117,11 +119,16 @@ func readback(targetNs ns.NetNS, devNames []string) (netStatus, error) {
return err
}
routesNoLinkLocal := []netlink.Route{}
for _, route := range routes {
if route.Dst.IP.IsLinkLocalMulticast() || route.Dst.IP.IsLinkLocalUnicast() {
continue
}
log.Printf("Got %s route %v", name, route)
routesNoLinkLocal = append(routesNoLinkLocal, route)
}
retVal.Devices[i].Routes = routes
retVal.Devices[i].Routes = routesNoLinkLocal
}
rules, err := netlink.RuleList(netlink.FAMILY_ALL)
@ -291,6 +298,7 @@ var _ = Describe("sbr test", func() {
expNet1.Routes = append(expNet1.Routes,
netlink.Route{
Gw: net.IPv4(192, 168, 1, 1),
Dst: &net.IPNet{IP: net.IPv4zero, Mask: net.IPMask(net.IPv4zero)},
Table: 100,
LinkIndex: expNet1.Routes[0].LinkIndex,
})
@ -491,6 +499,7 @@ var _ = Describe("sbr test", func() {
}
expNet1.Routes = append(expNet1.Routes,
netlink.Route{
Dst: &net.IPNet{IP: net.IPv4zero, Mask: net.IPMask(net.IPv4zero)},
Gw: net.IPv4(192, 168, 1, 1),
Table: 100,
LinkIndex: expNet1.Routes[0].LinkIndex,
@ -498,6 +507,7 @@ var _ = Describe("sbr test", func() {
expNet1.Routes = append(expNet1.Routes,
netlink.Route{
Dst: &net.IPNet{IP: net.IPv4zero, Mask: net.IPMask(net.IPv4zero)},
Gw: net.IPv4(192, 168, 101, 1),
Table: 101,
LinkIndex: expNet1.Routes[0].LinkIndex,
@ -540,4 +550,81 @@ var _ = Describe("sbr test", func() {
_, _, err = testutils.CmdAddWithArgs(args, func() error { return cmdAdd(args) })
Expect(err).To(MatchError("This plugin must be called as chained plugin"))
})
It("Works with Table ID", func() {
ifname := "net1"
tableID := 5000
conf := `{
"cniVersion": "0.3.0",
"name": "cni-plugin-sbr-test",
"type": "sbr",
"table": %d,
"prevResult": {
"cniVersion": "0.3.0",
"interfaces": [
{
"name": "%s",
"sandbox": "%s"
}
],
"ips": [
{
"address": "192.168.1.209/24",
"interface": 0
},
{
"address": "192.168.101.209/24",
"interface": 0
}
],
"routes": []
}
}`
conf = fmt.Sprintf(conf, tableID, ifname, targetNs.Path())
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: targetNs.Path(),
IfName: ifname,
StdinData: []byte(conf),
}
preStatus := createDefaultStatus()
err := setup(targetNs, preStatus)
Expect(err).NotTo(HaveOccurred())
oldStatus, err := readback(targetNs, []string{"net1", "eth0"})
Expect(err).NotTo(HaveOccurred())
_, _, err = testutils.CmdAddWithArgs(args, func() error { return cmdAdd(args) })
Expect(err).NotTo(HaveOccurred())
newStatus, err := readback(targetNs, []string{"net1", "eth0"})
Expect(err).NotTo(HaveOccurred())
// Routes have not been moved.
Expect(newStatus).To(Equal(oldStatus))
// Fetch all rules for the requested table ID.
var rules []netlink.Rule
err = targetNs.Do(func(_ ns.NetNS) error {
var err error
rules, err = netlink.RuleListFiltered(
netlink.FAMILY_ALL, &netlink.Rule{
Table: tableID,
},
netlink.RT_FILTER_TABLE,
)
return err
})
Expect(err).NotTo(HaveOccurred())
Expect(rules).To(HaveLen(2))
// Both IPs have been added as source based routes with requested table ID.
Expect(rules[0].Table).To(Equal(tableID))
Expect(rules[0].Src.String()).To(Equal("192.168.101.209/32"))
Expect(rules[1].Table).To(Equal(tableID))
Expect(rules[1].Src.String()).To(Equal("192.168.1.209/32"))
})
})

View File

@ -319,7 +319,7 @@ func restoreBackup(ifName, containerID, backupPath string) error {
}
if len(errStr) > 0 {
return fmt.Errorf(strings.Join(errStr, "; "))
return errors.New(strings.Join(errStr, "; "))
}
if err = os.Remove(filePath); err != nil {
@ -433,7 +433,13 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("tuning"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
/* FIXME GC */
/* FIXME Status */
}, version.All, bv.BuildString("tuning"))
}
func cmdCheck(args *skel.CmdArgs) error {

View File

@ -110,10 +110,10 @@ var _ = Describe("tuning plugin", func() {
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = IFNAME
err = netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: IFNAME,
},
LinkAttrs: linkAttrs,
})
Expect(err).NotTo(HaveOccurred())
link, err := netlink.LinkByName(IFNAME)

View File

@ -39,7 +39,13 @@ type VRFNetConf struct {
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.VersionsStartingFrom("0.3.1"), bv.BuildString("vrf"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
/* FIXME GC */
/* FIXME Status */
}, version.VersionsStartingFrom("0.3.1"), bv.BuildString("vrf"))
}
func cmdAdd(args *skel.CmdArgs) error {

View File

@ -17,6 +17,8 @@ package main
import (
"fmt"
"math"
"net"
"time"
"github.com/vishvananda/netlink"
)
@ -48,11 +50,11 @@ func createVRF(name string, tableID uint32) (*netlink.Vrf, error) {
}
}
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = name
vrf := &netlink.Vrf{
LinkAttrs: netlink.LinkAttrs{
Name: name,
},
Table: tableID,
LinkAttrs: linkAttrs,
Table: tableID,
}
err = netlink.LinkAdd(vrf)
@ -124,7 +126,7 @@ func addInterface(vrf *netlink.Vrf, intf string) error {
afterAddresses, err := netlink.AddrList(i, netlink.FAMILY_V6)
if err != nil {
return fmt.Errorf("failed getting ipv6 new addresses for %s", intf)
return fmt.Errorf("failed getting ipv6 new addresses for %s: %v", intf, err)
}
// Since keeping the ipv6 address depends on net.ipv6.conf.all.keep_addr_on_down ,
@ -141,6 +143,37 @@ CONTINUE:
if err != nil {
return fmt.Errorf("could not restore address %s to %s @ %s: %v", toFind, intf, vrf.Name, err)
}
// Waits for local/host routes to be added by the kernel.
maxRetry := 10
for {
routesVRFTable, err := netlink.RouteListFiltered(
netlink.FAMILY_ALL,
&netlink.Route{
Dst: &net.IPNet{
IP: toFind.IP,
Mask: net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
},
Table: int(vrf.Table),
LinkIndex: i.Attrs().Index,
},
netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE|netlink.RT_FILTER_DST,
)
if err != nil {
return fmt.Errorf("failed getting routes for %s table %d for dst %s: %v", intf, vrf.Table, toFind.IPNet.String(), err)
}
if len(routesVRFTable) >= 1 {
break
}
maxRetry--
if maxRetry <= 0 {
return fmt.Errorf("failed getting local/host addresses for %s in table %d with dst %s", intf, vrf.Table, toFind.IPNet.String())
}
time.Sleep(10 * time.Millisecond)
}
}
// Apply all saved routes for the interface that was moved to the VRF

View File

@ -19,6 +19,7 @@ import (
"fmt"
"net"
"strings"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@ -94,19 +95,19 @@ var _ = Describe("vrf plugin", func() {
err = targetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
la0 := netlink.NewLinkAttrs()
la0.Name = IF0Name
err = netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: IF0Name,
},
LinkAttrs: la0,
})
Expect(err).NotTo(HaveOccurred())
_, err = netlink.LinkByName(IF0Name)
Expect(err).NotTo(HaveOccurred())
la1 := netlink.NewLinkAttrs()
la1.Name = IF1Name
err = netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: IF1Name,
},
LinkAttrs: la1,
})
Expect(err).NotTo(HaveOccurred())
_, err = netlink.LinkByName(IF1Name)
@ -207,6 +208,18 @@ var _ = Describe("vrf plugin", func() {
// Add IP addresses for network reachability
netlink.AddrAdd(link, &netlink.Addr{IPNet: ipv4})
netlink.AddrAdd(link, &netlink.Addr{IPNet: ipv6})
// Wait for the corresponding route to be addeded
Eventually(func() bool {
ipv6RouteDst := &net.IPNet{
IP: ipv6.IP,
Mask: net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
}
routes, _ := netlink.RouteListFiltered(netlink.FAMILY_ALL, &netlink.Route{
Dst: ipv6RouteDst,
Table: 0,
}, netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE)
return err == nil && len(routes) >= 1
}, time.Second, 500*time.Millisecond).Should(BeTrue())
ipAddrs, err := netlink.AddrList(link, netlink.FAMILY_V4)
Expect(err).NotTo(HaveOccurred())
@ -304,6 +317,18 @@ var _ = Describe("vrf plugin", func() {
// Add IP addresses for network reachability
netlink.AddrAdd(link, &netlink.Addr{IPNet: ipv4})
netlink.AddrAdd(link, &netlink.Addr{IPNet: ipv6})
// Wait for the corresponding route to be addeded
Eventually(func() bool {
ipv6RouteDst := &net.IPNet{
IP: ipv6.IP,
Mask: net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
}
routes, _ := netlink.RouteListFiltered(netlink.FAMILY_ALL, &netlink.Route{
Dst: ipv6RouteDst,
Table: 0,
}, netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE)
return err == nil && len(routes) >= 1
}, time.Second, 500*time.Millisecond).Should(BeTrue())
ipAddrs, err := netlink.AddrList(link, netlink.FAMILY_V4)
Expect(err).NotTo(HaveOccurred())
@ -362,6 +387,18 @@ var _ = Describe("vrf plugin", func() {
// Add IP addresses for network reachability
netlink.AddrAdd(link, &netlink.Addr{IPNet: ipv4})
netlink.AddrAdd(link, &netlink.Addr{IPNet: ipv6})
// Wait for the corresponding route to be addeded
Eventually(func() bool {
ipv6RouteDst := &net.IPNet{
IP: ipv6.IP,
Mask: net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
}
routes, _ := netlink.RouteListFiltered(netlink.FAMILY_ALL, &netlink.Route{
Dst: ipv6RouteDst,
Table: 0,
}, netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE)
return err == nil && len(routes) >= 1
}, time.Second, 500*time.Millisecond).Should(BeTrue())
ipAddrs, err := netlink.AddrList(link, netlink.FAMILY_V4)
Expect(err).NotTo(HaveOccurred())
@ -437,10 +474,10 @@ var _ = Describe("vrf plugin", func() {
defer GinkgoRecover()
l, err := netlink.LinkByName(IF0Name)
Expect(err).NotTo(HaveOccurred())
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = "testrbridge"
br := &netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{
Name: "testrbridge",
},
LinkAttrs: linkAttrs,
}
err = netlink.LinkAdd(br)
Expect(err).NotTo(HaveOccurred())

View File

@ -24,6 +24,7 @@ import (
"github.com/containernetworking/cni/pkg/types"
current "github.com/containernetworking/cni/pkg/types/100"
"github.com/containernetworking/cni/pkg/version"
"github.com/containernetworking/plugins/pkg/ipam"
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
)
@ -150,10 +151,40 @@ func cmdDel(args *skel.CmdArgs) error {
func main() {
// replace TODO with your plugin name
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("TODO"))
skel.PluginMainFuncs(skel.CNIFuncs{
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
Status: cmdStatus,
/* FIXME GC */
}, version.All, bv.BuildString("TODO"))
}
func cmdCheck(_ *skel.CmdArgs) error {
// TODO: implement
return fmt.Errorf("not implemented")
}
// cmdStatus implements the STATUS command, which indicates whether or not
// this plugin is able to accept ADD requests.
//
// If the plugin has external dependencies, such as a daemon
// or chained ipam plugin, it should determine their status. If all is well,
// and an ADD can be successfully processed, return nil
func cmdStatus(args *skel.CmdArgs) error {
conf, err := parseConfig(args.StdinData)
if err != nil {
return err
}
_ = conf
// If this plugins delegates IPAM, ensure that IPAM is also running
if err := ipam.ExecStatus(conf.IPAM.Type, args.StdinData); err != nil {
return err
}
// TODO: implement STATUS here
// e.g. querying an external deamon, or delegating STATUS to an IPAM plugin
return nil
}

View File

@ -1,49 +0,0 @@
#!/usr/bin/env sh
set -xe
SRC_DIR="${SRC_DIR:-$PWD}"
DOCKER="${DOCKER:-docker}"
GOLANG="${GOLANG:-golang:1.22-alpine}"
TAG=$(git describe --tags --dirty)
RELEASE_DIR=release-${TAG}
BUILDFLAGS="-ldflags '-extldflags -static -X github.com/containernetworking/plugins/pkg/utils/buildversion.BuildVersion=${TAG}'"
OUTPUT_DIR=bin
# Always clean first
rm -Rf ${SRC_DIR}/${RELEASE_DIR}
mkdir -p ${SRC_DIR}/${RELEASE_DIR}
mkdir -p ${OUTPUT_DIR}
$DOCKER run -ti -v ${SRC_DIR}:/go/src/github.com/containernetworking/plugins:z --rm "${GOLANG}" \
/bin/sh -xe -c "\
apk --no-cache add bash tar;
cd /go/src/github.com/containernetworking/plugins; umask 0022;
for arch in amd64 arm arm64 ppc64le s390x mips64le riscv64; do \
rm -f ${OUTPUT_DIR}/*; \
CGO_ENABLED=0 GOARCH=\$arch ./build_linux.sh ${BUILDFLAGS}; \
for format in tgz; do \
FILENAME=cni-plugins-linux-\$arch-${TAG}.\$format; \
FILEPATH=${RELEASE_DIR}/\$FILENAME; \
tar -C ${OUTPUT_DIR} --owner=0 --group=0 -caf \$FILEPATH .; \
done; \
done;
rm -rf ${OUTPUT_DIR}/*; \
CGO_ENABLED=0 GOARCH=amd64 ./build_windows.sh ${BUILDFLAGS}; \
for format in tgz; do \
FILENAME=cni-plugins-windows-amd64-${TAG}.\$format; \
FILEPATH=${RELEASE_DIR}/\$FILENAME; \
tar -C ${OUTPUT_DIR} --owner=0 --group=0 -caf \$FILEPATH .; \
done;
cd ${RELEASE_DIR};
for f in *.tgz; do sha1sum \$f > \$f.sha1; done;
for f in *.tgz; do sha256sum \$f > \$f.sha256; done;
for f in *.tgz; do sha512sum \$f > \$f.sha512; done;
cd ..
chown -R ${UID} ${OUTPUT_DIR} ${RELEASE_DIR}"

View File

@ -1,4 +1,4 @@
#!/usr/bin/env sh
#!/usr/bin/env bash
#
# Run CNI plugin tests.
#
@ -18,6 +18,23 @@ testrun() {
sudo -E sh -c "umask 0; PATH=${GOPATH}/bin:$(pwd)/bin:${PATH} go test -race $*"
}
ensure_sysctl() {
local key
local val
local existing
key="$1"
val="$2"
existing="$(sysctl -ben "$key")"
sysctl -r
if [ "$val" -ne "$existing" ]; then
echo "sudo sysctl -we '$key'='$val'"
sudo sysctl -we "$key"="$val"
fi
}
COVERALLS=${COVERALLS:-""}
if [ -n "${COVERALLS}" ]; then
@ -40,4 +57,7 @@ done
# Run the pkg/ns tests as non root user
mkdir -p /tmp/cni-rootless
ensure_sysctl kernel.unprivileged_userns_clone 1
ensure_sysctl kernel.apparmor_restrict_unprivileged_userns 0
(export XDG_RUNTIME_DIR=/tmp/cni-rootless; cd pkg/ns/; unshare -rmn go test)

View File

@ -264,6 +264,18 @@ func SetPolicySupported() error {
return platformDoesNotSupportError("SetPolicy")
}
// ModifyLoadbalancerSupported returns an error if the HCN version does not support ModifyLoadbalancer.
func ModifyLoadbalancerSupported() error {
supported, err := GetCachedSupportedFeatures()
if err != nil {
return err
}
if supported.ModifyLoadbalancer {
return nil
}
return platformDoesNotSupportError("ModifyLoadbalancer")
}
// VxlanPortSupported returns an error if the HCN version does not support configuring the VXLAN TCP port.
func VxlanPortSupported() error {
supported, err := GetCachedSupportedFeatures()
@ -324,6 +336,18 @@ func DisableHostPortSupported() error {
return platformDoesNotSupportError("DisableHostPort")
}
// AccelnetSupported returns an error if the HCN version does not support Accelnet Feature.
func AccelnetSupported() error {
supported, err := GetCachedSupportedFeatures()
if err != nil {
return err
}
if supported.Accelnet {
return nil
}
return platformDoesNotSupportError("Accelnet")
}
// RequestType are the different operations performed to settings.
// Used to update the settings of Endpoint/Namespace objects.
type RequestType string

View File

@ -52,6 +52,7 @@ type ErrorCode uint32
const (
ERROR_NOT_FOUND = ErrorCode(windows.ERROR_NOT_FOUND)
HCN_E_PORT_ALREADY_EXISTS ErrorCode = ErrorCode(windows.HCN_E_PORT_ALREADY_EXISTS)
HCN_E_NOTIMPL ErrorCode = ErrorCode(windows.E_NOTIMPL)
)
type HcnError struct {
@ -79,6 +80,10 @@ func IsPortAlreadyExistsError(err error) bool {
return CheckErrorWithCode(err, HCN_E_PORT_ALREADY_EXISTS)
}
func IsNotImplemented(err error) bool {
return CheckErrorWithCode(err, HCN_E_NOTIMPL)
}
func new(hr error, title string, rest string) error {
err := &HcnError{}
hcsError := hcserror.New(hr, title, rest)

View File

@ -87,6 +87,10 @@ var (
//HNS 15.1 allows support for DisableHostPort flag.
DisableHostPortVersion = VersionRanges{VersionRange{MinVersion: Version{Major: 15, Minor: 1}, MaxVersion: Version{Major: math.MaxInt32, Minor: math.MaxInt32}}}
// HNS 15.4 allows for Modify Loadbalancer support
ModifyLoadbalancerVersion = VersionRanges{VersionRange{MinVersion: Version{Major: 15, Minor: 4}, MaxVersion: Version{Major: math.MaxInt32, Minor: math.MaxInt32}}}
// HNS 15.4 allows for Accelnet support
AccelnetVersion = VersionRanges{VersionRange{MinVersion: Version{Major: 15, Minor: 4}, MaxVersion: Version{Major: math.MaxInt32, Minor: math.MaxInt32}}}
)
// GetGlobals returns the global properties of the HCN Service.

View File

@ -163,6 +163,49 @@ func createLoadBalancer(settings string) (*HostComputeLoadBalancer, error) {
return &outputLoadBalancer, nil
}
func updateLoadBalancer(loadbalancerId string, settings string) (*HostComputeLoadBalancer, error) {
loadBalancerGuid, err := guid.FromString(loadbalancerId)
if err != nil {
return nil, errInvalidLoadBalancerID
}
// Update loadBalancer.
var (
loadBalancerHandle hcnLoadBalancer
resultBuffer *uint16
propertiesBuffer *uint16
)
hr := hcnOpenLoadBalancer(&loadBalancerGuid, &loadBalancerHandle, &resultBuffer)
if err := checkForErrors("hcnOpenLoadBalancer", hr, resultBuffer); err != nil {
return nil, err
}
hr = hcnModifyLoadBalancer(loadBalancerHandle, settings, &resultBuffer)
if err := checkForErrors("hcnModifyLoadBalancer", hr, resultBuffer); err != nil {
return nil, err
}
// Query loadBalancer.
hcnQuery := defaultQuery()
query, err := json.Marshal(hcnQuery)
if err != nil {
return nil, err
}
hr = hcnQueryLoadBalancerProperties(loadBalancerHandle, string(query), &propertiesBuffer, &resultBuffer)
if err := checkForErrors("hcnQueryLoadBalancerProperties", hr, resultBuffer); err != nil {
return nil, err
}
properties := interop.ConvertAndFreeCoTaskMemString(propertiesBuffer)
// Close loadBalancer.
hr = hcnCloseLoadBalancer(loadBalancerHandle)
if err := checkForErrors("hcnCloseLoadBalancer", hr, nil); err != nil {
return nil, err
}
// Convert output to HostComputeLoadBalancer
var outputLoadBalancer HostComputeLoadBalancer
if err := json.Unmarshal([]byte(properties), &outputLoadBalancer); err != nil {
return nil, err
}
return &outputLoadBalancer, nil
}
func deleteLoadBalancer(loadBalancerID string) error {
loadBalancerGUID, err := guid.FromString(loadBalancerID)
if err != nil {
@ -237,6 +280,23 @@ func (loadBalancer *HostComputeLoadBalancer) Create() (*HostComputeLoadBalancer,
return loadBalancer, nil
}
// Update Loadbalancer.
func (loadBalancer *HostComputeLoadBalancer) Update(hnsLoadbalancerID string) (*HostComputeLoadBalancer, error) {
logrus.Debugf("hcn::HostComputeLoadBalancer::Create id=%s", hnsLoadbalancerID)
jsonString, err := json.Marshal(loadBalancer)
if err != nil {
return nil, err
}
logrus.Debugf("hcn::HostComputeLoadBalancer::Update JSON: %s", jsonString)
loadBalancer, hcnErr := updateLoadBalancer(hnsLoadbalancerID, string(jsonString))
if hcnErr != nil {
return nil, hcnErr
}
return loadBalancer, nil
}
// Delete LoadBalancer.
func (loadBalancer *HostComputeLoadBalancer) Delete() error {
logrus.Debugf("hcn::HostComputeLoadBalancer::Delete id=%s", loadBalancer.Id)

View File

@ -73,6 +73,7 @@ const (
None NetworkFlags = 0
EnableNonPersistent NetworkFlags = 8
DisableHostPort NetworkFlags = 1024
EnableIov NetworkFlags = 8192
)
// HostComputeNetwork represents a network

View File

@ -144,10 +144,11 @@ type QosPolicySetting struct {
// OutboundNatPolicySetting sets outbound Network Address Translation on an Endpoint.
type OutboundNatPolicySetting struct {
VirtualIP string `json:",omitempty"`
Exceptions []string `json:",omitempty"`
Destinations []string `json:",omitempty"`
Flags NatFlags `json:",omitempty"`
VirtualIP string `json:",omitempty"`
Exceptions []string `json:",omitempty"`
Destinations []string `json:",omitempty"`
Flags NatFlags `json:",omitempty"`
MaxPortPoolUsage uint16 `json:",omitempty"`
}
// SDNRoutePolicySetting sets SDN Route on an Endpoint.

View File

@ -38,6 +38,8 @@ type SupportedFeatures struct {
NetworkACL bool `json:"NetworkACL"`
NestedIpSet bool `json:"NestedIpSet"`
DisableHostPort bool `json:"DisableHostPort"`
ModifyLoadbalancer bool `json:"ModifyLoadbalancer"`
Accelnet bool `json:"Accelnet"`
}
// AclFeatures are the supported ACL possibilities.
@ -116,6 +118,8 @@ func getSupportedFeatures() (SupportedFeatures, error) {
features.NetworkACL = isFeatureSupported(globals.Version, NetworkACLPolicyVersion)
features.NestedIpSet = isFeatureSupported(globals.Version, NestedIpSetVersion)
features.DisableHostPort = isFeatureSupported(globals.Version, DisableHostPortVersion)
features.ModifyLoadbalancer = isFeatureSupported(globals.Version, ModifyLoadbalancerVersion)
features.Accelnet = isFeatureSupported(globals.Version, AccelnetVersion)
log.L.WithFields(logrus.Fields{
"version": globals.Version,

46
vendor/github.com/Microsoft/hcsshim/hnsaccelnet.go generated vendored Normal file
View File

@ -0,0 +1,46 @@
//go:build windows
package hcsshim
import (
"errors"
"github.com/Microsoft/hcsshim/internal/hns"
)
// HNSNnvManagementMacAddress represents management mac address
// which needs to be excluded from VF reassignment
type HNSNnvManagementMacAddress = hns.HNSNnvManagementMacAddress
// HNSNnvManagementMacList represents a list of management
// mac addresses for exclusion from VF reassignment
type HNSNnvManagementMacList = hns.HNSNnvManagementMacList
var (
ErrorEmptyMacAddressList = errors.New("management mac_address list is empty")
)
// SetNnvManagementMacAddresses sets a list of
// management mac addresses in hns for exclusion from VF reassignment.
func SetNnvManagementMacAddresses(managementMacAddresses []string) (*HNSNnvManagementMacList, error) {
if len(managementMacAddresses) == 0 {
return nil, ErrorEmptyMacAddressList
}
nnvManagementMacList := &HNSNnvManagementMacList{}
for _, mac := range managementMacAddresses {
nnvManagementMacList.MacAddressList = append(nnvManagementMacList.MacAddressList, HNSNnvManagementMacAddress{MacAddress: mac})
}
return nnvManagementMacList.Set()
}
// GetNnvManagementMacAddresses retrieves a list of
// management mac addresses in hns for exclusion from VF reassignment.
func GetNnvManagementMacAddresses() (*HNSNnvManagementMacList, error) {
return hns.GetNnvManagementMacAddressList()
}
// DeleteNnvManagementMacAddresses delete list of
// management mac addresses in hns which are excluded from VF reassignment.
func DeleteNnvManagementMacAddresses() (*HNSNnvManagementMacList, error) {
return hns.DeleteNnvManagementMacAddressList()
}

View File

@ -0,0 +1,60 @@
//go:build windows
package hns
import (
"encoding/json"
"github.com/sirupsen/logrus"
)
// HNSNnvManagementMacAddress represents management mac address
// which needs to be excluded from VF reassignment
type HNSNnvManagementMacAddress struct {
MacAddress string `json:",omitempty"`
}
// HNSNnvManagementMacList represents a list of management
// mac addresses for exclusion from VF reassignment
type HNSNnvManagementMacList struct {
MacAddressList []HNSNnvManagementMacAddress `json:",omitempty"`
}
// HNSNnvManagementMacRequest makes a HNS call to modify/query NnvManagementMacList
func HNSNnvManagementMacRequest(method, path, request string) (*HNSNnvManagementMacList, error) {
nnvManagementMacList := &HNSNnvManagementMacList{}
err := hnsCall(method, "/accelnet/"+path, request, &nnvManagementMacList)
if err != nil {
return nil, err
}
return nnvManagementMacList, nil
}
// Set ManagementMacAddressList by sending "POST" NnvManagementMacRequest to HNS.
func (nnvManagementMacList *HNSNnvManagementMacList) Set() (*HNSNnvManagementMacList, error) {
operation := "Set"
title := "hcsshim::nnvManagementMacList::" + operation
logrus.Debugf(title+" id=%s", nnvManagementMacList.MacAddressList)
jsonString, err := json.Marshal(nnvManagementMacList)
if err != nil {
return nil, err
}
return HNSNnvManagementMacRequest("POST", "", string(jsonString))
}
// Get ManagementMacAddressList by sending "GET" NnvManagementMacRequest to HNS.
func GetNnvManagementMacAddressList() (*HNSNnvManagementMacList, error) {
operation := "Get"
title := "hcsshim::nnvManagementMacList::" + operation
logrus.Debugf(title)
return HNSNnvManagementMacRequest("GET", "", "")
}
// Delete ManagementMacAddressList by sending "DELETE" NnvManagementMacRequest to HNS.
func DeleteNnvManagementMacAddressList() (*HNSNnvManagementMacList, error) {
operation := "Delete"
title := "hcsshim::nnvManagementMacList::" + operation
logrus.Debugf(title)
return HNSNnvManagementMacRequest("DELETE", "", "")
}

View File

@ -10,6 +10,28 @@ import (
"github.com/sirupsen/logrus"
)
// EndpointState represents the states of an HNS Endpoint lifecycle.
type EndpointState uint16
// EndpointState const
// The lifecycle of an Endpoint goes through created, attached, AttachedSharing - endpoint is being shared with other containers,
// detached, after being attached, degraded and finally destroyed.
// Note: This attribute is used by calico to define stale containers and is dependent on HNS v1 api, if we move to HNS v2 api we will need
// to update the current calico code and cordinate the change with calico. Reach out to Microsoft to facilate the change via HNS.
const (
Uninitialized EndpointState = iota
Created EndpointState = 1
Attached EndpointState = 2
AttachedSharing EndpointState = 3
Detached EndpointState = 4
Degraded EndpointState = 5
Destroyed EndpointState = 6
)
func (es EndpointState) String() string {
return [...]string{"Uninitialized", "Created", "Attached", "AttachedSharing", "Detached", "Degraded", "Destroyed"}[es]
}
// HNSEndpoint represents a network endpoint in HNS
type HNSEndpoint struct {
Id string `json:"ID,omitempty"`
@ -34,6 +56,7 @@ type HNSEndpoint struct {
Namespace *Namespace `json:",omitempty"`
EncapOverhead uint16 `json:",omitempty"`
SharedContainers []string `json:",omitempty"`
State EndpointState `json:",omitempty"`
}
// SystemType represents the type of the system on which actions are done

View File

@ -57,9 +57,10 @@ type PaPolicy struct {
type OutboundNatPolicy struct {
Policy
VIP string `json:"VIP,omitempty"`
Exceptions []string `json:"ExceptionList,omitempty"`
Destinations []string `json:",omitempty"`
VIP string `json:"VIP,omitempty"`
Exceptions []string `json:"ExceptionList,omitempty"`
Destinations []string `json:",omitempty"`
MaxPortPoolUsage uint16 `json:",omitempty"`
}
type ProxyPolicy struct {

View File

@ -188,7 +188,7 @@ func Open(ctx context.Context, options *Options) (_ *JobObject, err error) {
return nil, winapi.RtlNtStatusToDosError(status)
}
} else {
jobHandle, err = winapi.OpenJobObject(winapi.JOB_OBJECT_ALL_ACCESS, 0, unicodeJobName.Buffer)
jobHandle, err = winapi.OpenJobObject(winapi.JOB_OBJECT_ALL_ACCESS, false, unicodeJobName.Buffer)
if err != nil {
return nil, err
}
@ -523,12 +523,9 @@ func (job *JobObject) ApplyFileBinding(root, target string, readOnly bool) error
func isJobSilo(h windows.Handle) bool {
// None of the information from the structure that this info class expects will be used, this is just used as
// the call will fail if the job hasn't been upgraded to a silo so we can use this to tell when we open a job
// if it's a silo or not. Because none of the info matters simply define a dummy struct with the size that the call
// expects which is 16 bytes.
type isSiloObj struct {
_ [16]byte
}
var siloInfo isSiloObj
// if it's a silo or not. We still need to define the struct layout as expected by Win32, else the struct
// alignment might be different and the call will fail.
var siloInfo winapi.SILOOBJECT_BASIC_INFORMATION
err := winapi.QueryInformationJobObject(
h,
winapi.JobObjectSiloBasicInformation,

View File

@ -6,7 +6,7 @@ import (
"net"
"os"
"github.com/containerd/errdefs"
errdefs "github.com/containerd/errdefs/pkg/errgrpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

View File

@ -28,7 +28,7 @@ const (
// https://docs.microsoft.com/en-us/windows/win32/procthread/job-object-security-and-access-rights
const (
JOB_OBJECT_QUERY = 0x0004
JOB_OBJECT_ALL_ACCESS = 0x1F001F
JOB_OBJECT_ALL_ACCESS = 0x1F003F
)
// IO limit flags
@ -160,6 +160,21 @@ type JOBOBJECT_ASSOCIATE_COMPLETION_PORT struct {
CompletionPort windows.Handle
}
// typedef struct _SILOOBJECT_BASIC_INFORMATION {
// DWORD SiloId;
// DWORD SiloParentId;
// DWORD NumberOfProcesses;
// BOOLEAN IsInServerSilo;
// BYTE Reserved[3];
// } SILOOBJECT_BASIC_INFORMATION, *PSILOOBJECT_BASIC_INFORMATION;
type SILOOBJECT_BASIC_INFORMATION struct {
SiloID uint32
SiloParentID uint32
NumberOfProcesses uint32
IsInServerSilo bool
Reserved [3]uint8
}
// BOOL IsProcessInJob(
// HANDLE ProcessHandle,
// HANDLE JobHandle,
@ -184,7 +199,7 @@ type JOBOBJECT_ASSOCIATE_COMPLETION_PORT struct {
// LPCWSTR lpName
// );
//
//sys OpenJobObject(desiredAccess uint32, inheritHandle int32, lpName *uint16) (handle windows.Handle, err error) = kernel32.OpenJobObjectW
//sys OpenJobObject(desiredAccess uint32, inheritHandle bool, lpName *uint16) (handle windows.Handle, err error) = kernel32.OpenJobObjectW
// DWORD SetIoRateControlInformationJobObject(
// HANDLE hJob,

View File

@ -470,8 +470,12 @@ func LocalFree(ptr uintptr) {
return
}
func OpenJobObject(desiredAccess uint32, inheritHandle int32, lpName *uint16) (handle windows.Handle, err error) {
r0, _, e1 := syscall.SyscallN(procOpenJobObjectW.Addr(), uintptr(desiredAccess), uintptr(inheritHandle), uintptr(unsafe.Pointer(lpName)))
func OpenJobObject(desiredAccess uint32, inheritHandle bool, lpName *uint16) (handle windows.Handle, err error) {
var _p0 uint32
if inheritHandle {
_p0 = 1
}
r0, _, e1 := syscall.SyscallN(procOpenJobObjectW.Addr(), uintptr(desiredAccess), uintptr(_p0), uintptr(unsafe.Pointer(lpName)))
handle = windows.Handle(r0)
if handle == 0 {
err = errnoErr(e1)

View File

@ -21,9 +21,6 @@
//
// To detect an error class, use the IsXXX functions to tell whether an error
// is of a certain type.
//
// The functions ToGRPC and FromGRPC can be used to map server-side and
// client-side errors to the correct types.
package errdefs
import (
@ -36,57 +33,411 @@ import (
// Packages should return errors of these types when they want to instruct a
// client to take a particular action.
//
// For the most part, we just try to provide local grpc errors. Most conditions
// map very well to those defined by grpc.
// These errors map closely to grpc errors.
var (
ErrUnknown = errors.New("unknown") // used internally to represent a missed mapping.
ErrInvalidArgument = errors.New("invalid argument")
ErrNotFound = errors.New("not found")
ErrAlreadyExists = errors.New("already exists")
ErrFailedPrecondition = errors.New("failed precondition")
ErrUnavailable = errors.New("unavailable")
ErrNotImplemented = errors.New("not implemented") // represents not supported and unimplemented
ErrUnknown = errUnknown{}
ErrInvalidArgument = errInvalidArgument{}
ErrNotFound = errNotFound{}
ErrAlreadyExists = errAlreadyExists{}
ErrPermissionDenied = errPermissionDenied{}
ErrResourceExhausted = errResourceExhausted{}
ErrFailedPrecondition = errFailedPrecondition{}
ErrConflict = errConflict{}
ErrNotModified = errNotModified{}
ErrAborted = errAborted{}
ErrOutOfRange = errOutOfRange{}
ErrNotImplemented = errNotImplemented{}
ErrInternal = errInternal{}
ErrUnavailable = errUnavailable{}
ErrDataLoss = errDataLoss{}
ErrUnauthenticated = errUnauthorized{}
)
// IsInvalidArgument returns true if the error is due to an invalid argument
func IsInvalidArgument(err error) bool {
return errors.Is(err, ErrInvalidArgument)
}
// IsNotFound returns true if the error is due to a missing object
func IsNotFound(err error) bool {
return errors.Is(err, ErrNotFound)
}
// IsAlreadyExists returns true if the error is due to an already existing
// metadata item
func IsAlreadyExists(err error) bool {
return errors.Is(err, ErrAlreadyExists)
}
// IsFailedPrecondition returns true if an operation could not proceed to the
// lack of a particular condition
func IsFailedPrecondition(err error) bool {
return errors.Is(err, ErrFailedPrecondition)
}
// IsUnavailable returns true if the error is due to a resource being unavailable
func IsUnavailable(err error) bool {
return errors.Is(err, ErrUnavailable)
}
// IsNotImplemented returns true if the error is due to not being implemented
func IsNotImplemented(err error) bool {
return errors.Is(err, ErrNotImplemented)
// cancelled maps to Moby's "ErrCancelled"
type cancelled interface {
Cancelled()
}
// IsCanceled returns true if the error is due to `context.Canceled`.
func IsCanceled(err error) bool {
return errors.Is(err, context.Canceled)
return errors.Is(err, context.Canceled) || isInterface[cancelled](err)
}
type errUnknown struct{}
func (errUnknown) Error() string { return "unknown" }
func (errUnknown) Unknown() {}
func (e errUnknown) WithMessage(msg string) error {
return customMessage{e, msg}
}
// unknown maps to Moby's "ErrUnknown"
type unknown interface {
Unknown()
}
// IsUnknown returns true if the error is due to an unknown error,
// unhandled condition or unexpected response.
func IsUnknown(err error) bool {
return errors.Is(err, errUnknown{}) || isInterface[unknown](err)
}
type errInvalidArgument struct{}
func (errInvalidArgument) Error() string { return "invalid argument" }
func (errInvalidArgument) InvalidParameter() {}
func (e errInvalidArgument) WithMessage(msg string) error {
return customMessage{e, msg}
}
// invalidParameter maps to Moby's "ErrInvalidParameter"
type invalidParameter interface {
InvalidParameter()
}
// IsInvalidArgument returns true if the error is due to an invalid argument
func IsInvalidArgument(err error) bool {
return errors.Is(err, ErrInvalidArgument) || isInterface[invalidParameter](err)
}
// deadlineExceed maps to Moby's "ErrDeadline"
type deadlineExceeded interface {
DeadlineExceeded()
}
// IsDeadlineExceeded returns true if the error is due to
// `context.DeadlineExceeded`.
func IsDeadlineExceeded(err error) bool {
return errors.Is(err, context.DeadlineExceeded)
return errors.Is(err, context.DeadlineExceeded) || isInterface[deadlineExceeded](err)
}
type errNotFound struct{}
func (errNotFound) Error() string { return "not found" }
func (errNotFound) NotFound() {}
func (e errNotFound) WithMessage(msg string) error {
return customMessage{e, msg}
}
// notFound maps to Moby's "ErrNotFound"
type notFound interface {
NotFound()
}
// IsNotFound returns true if the error is due to a missing object
func IsNotFound(err error) bool {
return errors.Is(err, ErrNotFound) || isInterface[notFound](err)
}
type errAlreadyExists struct{}
func (errAlreadyExists) Error() string { return "already exists" }
func (errAlreadyExists) AlreadyExists() {}
func (e errAlreadyExists) WithMessage(msg string) error {
return customMessage{e, msg}
}
type alreadyExists interface {
AlreadyExists()
}
// IsAlreadyExists returns true if the error is due to an already existing
// metadata item
func IsAlreadyExists(err error) bool {
return errors.Is(err, ErrAlreadyExists) || isInterface[alreadyExists](err)
}
type errPermissionDenied struct{}
func (errPermissionDenied) Error() string { return "permission denied" }
func (errPermissionDenied) Forbidden() {}
func (e errPermissionDenied) WithMessage(msg string) error {
return customMessage{e, msg}
}
// forbidden maps to Moby's "ErrForbidden"
type forbidden interface {
Forbidden()
}
// IsPermissionDenied returns true if the error is due to permission denied
// or forbidden (403) response
func IsPermissionDenied(err error) bool {
return errors.Is(err, ErrPermissionDenied) || isInterface[forbidden](err)
}
type errResourceExhausted struct{}
func (errResourceExhausted) Error() string { return "resource exhausted" }
func (errResourceExhausted) ResourceExhausted() {}
func (e errResourceExhausted) WithMessage(msg string) error {
return customMessage{e, msg}
}
type resourceExhausted interface {
ResourceExhausted()
}
// IsResourceExhausted returns true if the error is due to
// a lack of resources or too many attempts.
func IsResourceExhausted(err error) bool {
return errors.Is(err, errResourceExhausted{}) || isInterface[resourceExhausted](err)
}
type errFailedPrecondition struct{}
func (e errFailedPrecondition) Error() string { return "failed precondition" }
func (errFailedPrecondition) FailedPrecondition() {}
func (e errFailedPrecondition) WithMessage(msg string) error {
return customMessage{e, msg}
}
type failedPrecondition interface {
FailedPrecondition()
}
// IsFailedPrecondition returns true if an operation could not proceed due to
// the lack of a particular condition
func IsFailedPrecondition(err error) bool {
return errors.Is(err, errFailedPrecondition{}) || isInterface[failedPrecondition](err)
}
type errConflict struct{}
func (errConflict) Error() string { return "conflict" }
func (errConflict) Conflict() {}
func (e errConflict) WithMessage(msg string) error {
return customMessage{e, msg}
}
// conflict maps to Moby's "ErrConflict"
type conflict interface {
Conflict()
}
// IsConflict returns true if an operation could not proceed due to
// a conflict.
func IsConflict(err error) bool {
return errors.Is(err, errConflict{}) || isInterface[conflict](err)
}
type errNotModified struct{}
func (errNotModified) Error() string { return "not modified" }
func (errNotModified) NotModified() {}
func (e errNotModified) WithMessage(msg string) error {
return customMessage{e, msg}
}
// notModified maps to Moby's "ErrNotModified"
type notModified interface {
NotModified()
}
// IsNotModified returns true if an operation could not proceed due
// to an object not modified from a previous state.
func IsNotModified(err error) bool {
return errors.Is(err, errNotModified{}) || isInterface[notModified](err)
}
type errAborted struct{}
func (errAborted) Error() string { return "aborted" }
func (errAborted) Aborted() {}
func (e errAborted) WithMessage(msg string) error {
return customMessage{e, msg}
}
type aborted interface {
Aborted()
}
// IsAborted returns true if an operation was aborted.
func IsAborted(err error) bool {
return errors.Is(err, errAborted{}) || isInterface[aborted](err)
}
type errOutOfRange struct{}
func (errOutOfRange) Error() string { return "out of range" }
func (errOutOfRange) OutOfRange() {}
func (e errOutOfRange) WithMessage(msg string) error {
return customMessage{e, msg}
}
type outOfRange interface {
OutOfRange()
}
// IsOutOfRange returns true if an operation could not proceed due
// to data being out of the expected range.
func IsOutOfRange(err error) bool {
return errors.Is(err, errOutOfRange{}) || isInterface[outOfRange](err)
}
type errNotImplemented struct{}
func (errNotImplemented) Error() string { return "not implemented" }
func (errNotImplemented) NotImplemented() {}
func (e errNotImplemented) WithMessage(msg string) error {
return customMessage{e, msg}
}
// notImplemented maps to Moby's "ErrNotImplemented"
type notImplemented interface {
NotImplemented()
}
// IsNotImplemented returns true if the error is due to not being implemented
func IsNotImplemented(err error) bool {
return errors.Is(err, errNotImplemented{}) || isInterface[notImplemented](err)
}
type errInternal struct{}
func (errInternal) Error() string { return "internal" }
func (errInternal) System() {}
func (e errInternal) WithMessage(msg string) error {
return customMessage{e, msg}
}
// system maps to Moby's "ErrSystem"
type system interface {
System()
}
// IsInternal returns true if the error returns to an internal or system error
func IsInternal(err error) bool {
return errors.Is(err, errInternal{}) || isInterface[system](err)
}
type errUnavailable struct{}
func (errUnavailable) Error() string { return "unavailable" }
func (errUnavailable) Unavailable() {}
func (e errUnavailable) WithMessage(msg string) error {
return customMessage{e, msg}
}
// unavailable maps to Moby's "ErrUnavailable"
type unavailable interface {
Unavailable()
}
// IsUnavailable returns true if the error is due to a resource being unavailable
func IsUnavailable(err error) bool {
return errors.Is(err, errUnavailable{}) || isInterface[unavailable](err)
}
type errDataLoss struct{}
func (errDataLoss) Error() string { return "data loss" }
func (errDataLoss) DataLoss() {}
func (e errDataLoss) WithMessage(msg string) error {
return customMessage{e, msg}
}
// dataLoss maps to Moby's "ErrDataLoss"
type dataLoss interface {
DataLoss()
}
// IsDataLoss returns true if data during an operation was lost or corrupted
func IsDataLoss(err error) bool {
return errors.Is(err, errDataLoss{}) || isInterface[dataLoss](err)
}
type errUnauthorized struct{}
func (errUnauthorized) Error() string { return "unauthorized" }
func (errUnauthorized) Unauthorized() {}
func (e errUnauthorized) WithMessage(msg string) error {
return customMessage{e, msg}
}
// unauthorized maps to Moby's "ErrUnauthorized"
type unauthorized interface {
Unauthorized()
}
// IsUnauthorized returns true if the error indicates that the user was
// unauthenticated or unauthorized.
func IsUnauthorized(err error) bool {
return errors.Is(err, errUnauthorized{}) || isInterface[unauthorized](err)
}
func isInterface[T any](err error) bool {
for {
switch x := err.(type) {
case T:
return true
case customMessage:
err = x.err
case interface{ Unwrap() error }:
err = x.Unwrap()
if err == nil {
return false
}
case interface{ Unwrap() []error }:
for _, err := range x.Unwrap() {
if isInterface[T](err) {
return true
}
}
return false
default:
return false
}
}
}
// customMessage is used to provide a defined error with a custom message.
// The message is not wrapped but can be compared by the `Is(error) bool` interface.
type customMessage struct {
err error
msg string
}
func (c customMessage) Is(err error) bool {
return c.err == err
}
func (c customMessage) As(target any) bool {
return errors.As(c.err, target)
}
func (c customMessage) Error() string {
return c.msg
}

View File

@ -1,147 +0,0 @@
/*
Copyright The containerd 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 errdefs
import (
"context"
"fmt"
"strings"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// ToGRPC will attempt to map the backend containerd error into a grpc error,
// using the original error message as a description.
//
// Further information may be extracted from certain errors depending on their
// type.
//
// If the error is unmapped, the original error will be returned to be handled
// by the regular grpc error handling stack.
func ToGRPC(err error) error {
if err == nil {
return nil
}
if isGRPCError(err) {
// error has already been mapped to grpc
return err
}
switch {
case IsInvalidArgument(err):
return status.Errorf(codes.InvalidArgument, err.Error())
case IsNotFound(err):
return status.Errorf(codes.NotFound, err.Error())
case IsAlreadyExists(err):
return status.Errorf(codes.AlreadyExists, err.Error())
case IsFailedPrecondition(err):
return status.Errorf(codes.FailedPrecondition, err.Error())
case IsUnavailable(err):
return status.Errorf(codes.Unavailable, err.Error())
case IsNotImplemented(err):
return status.Errorf(codes.Unimplemented, err.Error())
case IsCanceled(err):
return status.Errorf(codes.Canceled, err.Error())
case IsDeadlineExceeded(err):
return status.Errorf(codes.DeadlineExceeded, err.Error())
}
return err
}
// ToGRPCf maps the error to grpc error codes, assembling the formatting string
// and combining it with the target error string.
//
// This is equivalent to errdefs.ToGRPC(fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err))
func ToGRPCf(err error, format string, args ...interface{}) error {
return ToGRPC(fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err))
}
// FromGRPC returns the underlying error from a grpc service based on the grpc error code
func FromGRPC(err error) error {
if err == nil {
return nil
}
var cls error // divide these into error classes, becomes the cause
switch code(err) {
case codes.InvalidArgument:
cls = ErrInvalidArgument
case codes.AlreadyExists:
cls = ErrAlreadyExists
case codes.NotFound:
cls = ErrNotFound
case codes.Unavailable:
cls = ErrUnavailable
case codes.FailedPrecondition:
cls = ErrFailedPrecondition
case codes.Unimplemented:
cls = ErrNotImplemented
case codes.Canceled:
cls = context.Canceled
case codes.DeadlineExceeded:
cls = context.DeadlineExceeded
default:
cls = ErrUnknown
}
msg := rebaseMessage(cls, err)
if msg != "" {
err = fmt.Errorf("%s: %w", msg, cls)
} else {
err = cls
}
return err
}
// rebaseMessage removes the repeats for an error at the end of an error
// string. This will happen when taking an error over grpc then remapping it.
//
// Effectively, we just remove the string of cls from the end of err if it
// appears there.
func rebaseMessage(cls error, err error) string {
desc := errDesc(err)
clss := cls.Error()
if desc == clss {
return ""
}
return strings.TrimSuffix(desc, ": "+clss)
}
func isGRPCError(err error) bool {
_, ok := status.FromError(err)
return ok
}
func code(err error) codes.Code {
if s, ok := status.FromError(err); ok {
return s.Code()
}
return codes.Unknown
}
func errDesc(err error) string {
if s, ok := status.FromError(err); ok {
return s.Message()
}
return err.Error()
}

191
vendor/github.com/containerd/errdefs/pkg/LICENSE generated vendored Normal file
View File

@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
https://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:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) 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
(d) 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
Copyright The containerd 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
https://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.

Some files were not shown because too many files have changed in this diff Show More