Skip to content

Commit ac86731

Browse files
authored
Merge pull request #743 from arista-eosplus/dummy-plugin
dummy: Create a Dummy CNI plugin that creates a virtual interface.
2 parents fcf14d3 + 7fcf8e4 commit ac86731

File tree

6 files changed

+774
-0
lines changed

6 files changed

+774
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Read [CONTRIBUTING](CONTRIBUTING.md) for build and test instructions.
1414
* `ptp`: Creates a veth pair.
1515
* `vlan`: Allocates a vlan device.
1616
* `host-device`: Move an already-existing device into a container.
17+
* `dummy`: Creates a new Dummy device in the container.
1718
#### Windows: Windows specific
1819
* `win-bridge`: Creates a bridge, adds the host and the container to it.
1920
* `win-overlay`: Creates an overlay interface to the container.

plugins/linux_only.txt

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ plugins/main/loopback
66
plugins/main/macvlan
77
plugins/main/ptp
88
plugins/main/vlan
9+
plugins/main/dummy
910
plugins/meta/portmap
1011
plugins/meta/tuning
1112
plugins/meta/bandwidth

plugins/main/dummy/README.md

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
title: dummy plugin
3+
description: "plugins/main/dummy/README.md"
4+
date: 2022-05-12
5+
toc: true
6+
draft: true
7+
weight: 200
8+
---
9+
10+
## Overview
11+
12+
dummy is a useful feature for routing packets through the Linux kernel without transmitting.
13+
14+
Like loopback, it is a purely virtual interface that allows packets to be routed to a designated IP address. Unlike loopback, the IP address can be arbitrary and is not restricted to the `127.0.0.0/8` range.
15+
16+
## Example configuration
17+
18+
```json
19+
{
20+
"name": "mynet",
21+
"type": "dummy",
22+
"ipam": {
23+
"type": "host-local",
24+
"subnet": "10.1.2.0/24"
25+
}
26+
}
27+
```
28+
29+
## Network configuration reference
30+
31+
* `name` (string, required): the name of the network.
32+
* `type` (string, required): "dummy".
33+
* `ipam` (dictionary, required): IPAM configuration to be used for this network.
34+
35+
## Notes
36+
37+
* `dummy` does not transmit packets.
38+
Therefore the container will not be able to reach any external network.
39+
This solution is designed to be used in conjunction with other CNI plugins (e.g., `bridge`) to provide an internal non-loopback address for applications to use.

plugins/main/dummy/dummy.go

+300
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
// Copyright 2022 Arista Networks
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"encoding/json"
19+
"errors"
20+
"fmt"
21+
"net"
22+
23+
"github.com/vishvananda/netlink"
24+
25+
"github.com/containernetworking/cni/pkg/skel"
26+
"github.com/containernetworking/cni/pkg/types"
27+
current "github.com/containernetworking/cni/pkg/types/100"
28+
"github.com/containernetworking/cni/pkg/version"
29+
30+
"github.com/containernetworking/plugins/pkg/ip"
31+
"github.com/containernetworking/plugins/pkg/ipam"
32+
"github.com/containernetworking/plugins/pkg/ns"
33+
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
34+
)
35+
36+
func parseNetConf(bytes []byte) (*types.NetConf, error) {
37+
conf := &types.NetConf{}
38+
if err := json.Unmarshal(bytes, conf); err != nil {
39+
return nil, fmt.Errorf("failed to parse network config: %v", err)
40+
}
41+
return conf, nil
42+
}
43+
44+
func createDummy(conf *types.NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) {
45+
46+
dummy := &current.Interface{}
47+
48+
dm := &netlink.Dummy{
49+
LinkAttrs: netlink.LinkAttrs{
50+
Name: ifName,
51+
Namespace: netlink.NsFd(int(netns.Fd())),
52+
},
53+
}
54+
55+
if err := netlink.LinkAdd(dm); err != nil {
56+
return nil, fmt.Errorf("failed to create dummy: %v", err)
57+
}
58+
dummy.Name = ifName
59+
60+
err := netns.Do(func(_ ns.NetNS) error {
61+
// Re-fetch interface to get all properties/attributes
62+
contDummy, err := netlink.LinkByName(ifName)
63+
if err != nil {
64+
return fmt.Errorf("failed to fetch dummy%q: %v", ifName, err)
65+
}
66+
67+
dummy.Mac = contDummy.Attrs().HardwareAddr.String()
68+
dummy.Sandbox = netns.Path()
69+
70+
return nil
71+
})
72+
if err != nil {
73+
return nil, err
74+
}
75+
76+
return dummy, nil
77+
}
78+
79+
func cmdAdd(args *skel.CmdArgs) error {
80+
conf, err := parseNetConf(args.StdinData)
81+
if err != nil {
82+
return err
83+
}
84+
85+
if conf.IPAM.Type == "" {
86+
return errors.New("dummy interface requires an IPAM configuration")
87+
}
88+
89+
netns, err := ns.GetNS(args.Netns)
90+
if err != nil {
91+
return fmt.Errorf("failed to open netns %q: %v", netns, err)
92+
}
93+
defer netns.Close()
94+
95+
dummyInterface, err := createDummy(conf, args.IfName, netns)
96+
if err != nil {
97+
return err
98+
}
99+
100+
// Delete link if err to avoid link leak in this ns
101+
defer func() {
102+
if err != nil {
103+
netns.Do(func(_ ns.NetNS) error {
104+
return ip.DelLinkByName(args.IfName)
105+
})
106+
}
107+
}()
108+
109+
r, err := ipam.ExecAdd(conf.IPAM.Type, args.StdinData)
110+
if err != nil {
111+
return err
112+
}
113+
114+
// defer ipam deletion to avoid ip leak
115+
defer func() {
116+
if err != nil {
117+
ipam.ExecDel(conf.IPAM.Type, args.StdinData)
118+
}
119+
}()
120+
121+
// convert IPAMResult to current Result type
122+
result, err := current.NewResultFromResult(r)
123+
if err != nil {
124+
return err
125+
}
126+
127+
if len(result.IPs) == 0 {
128+
return errors.New("IPAM plugin returned missing IP config")
129+
}
130+
131+
for _, ipc := range result.IPs {
132+
// all addresses apply to the container dummy interface
133+
ipc.Interface = current.Int(0)
134+
}
135+
136+
result.Interfaces = []*current.Interface{dummyInterface}
137+
138+
err = netns.Do(func(_ ns.NetNS) error {
139+
if err := ipam.ConfigureIface(args.IfName, result); err != nil {
140+
return err
141+
}
142+
return nil
143+
})
144+
145+
if err != nil {
146+
return err
147+
}
148+
149+
return types.PrintResult(result, conf.CNIVersion)
150+
}
151+
152+
func cmdDel(args *skel.CmdArgs) error {
153+
conf, err := parseNetConf(args.StdinData)
154+
if err != nil {
155+
return err
156+
}
157+
158+
if err = ipam.ExecDel(conf.IPAM.Type, args.StdinData); err != nil {
159+
return err
160+
}
161+
162+
if args.Netns == "" {
163+
return nil
164+
}
165+
166+
err = ns.WithNetNSPath(args.Netns, func(ns.NetNS) error {
167+
err = ip.DelLinkByName(args.IfName)
168+
if err != nil && err == ip.ErrLinkNotFound {
169+
return nil
170+
}
171+
return err
172+
})
173+
174+
if err != nil {
175+
// if NetNs is passed down by the Cloud Orchestration Engine, or if it called multiple times
176+
// so don't return an error if the device is already removed.
177+
// https://github.com/kubernetes/kubernetes/issues/43014#issuecomment-287164444
178+
_, ok := err.(ns.NSPathNotExistErr)
179+
if ok {
180+
return nil
181+
}
182+
return err
183+
}
184+
185+
return nil
186+
}
187+
188+
func main() {
189+
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("dummy"))
190+
}
191+
192+
func cmdCheck(args *skel.CmdArgs) error {
193+
conf, err := parseNetConf(args.StdinData)
194+
if err != nil {
195+
return err
196+
}
197+
198+
if conf.IPAM.Type == "" {
199+
return errors.New("dummy interface requires an IPAM configuration")
200+
}
201+
202+
netns, err := ns.GetNS(args.Netns)
203+
if err != nil {
204+
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
205+
}
206+
defer netns.Close()
207+
208+
// run the IPAM plugin and get back the config to apply
209+
err = ipam.ExecCheck(conf.IPAM.Type, args.StdinData)
210+
if err != nil {
211+
return err
212+
}
213+
214+
if conf.RawPrevResult == nil {
215+
return fmt.Errorf("dummy: Required prevResult missing")
216+
}
217+
218+
if err := version.ParsePrevResult(conf); err != nil {
219+
return err
220+
}
221+
222+
// Convert whatever the IPAM result was into the current Result type
223+
result, err := current.NewResultFromResult(conf.PrevResult)
224+
if err != nil {
225+
return err
226+
}
227+
228+
var contMap current.Interface
229+
// Find interfaces for name whe know, that of dummy device inside container
230+
for _, intf := range result.Interfaces {
231+
if args.IfName == intf.Name {
232+
if args.Netns == intf.Sandbox {
233+
contMap = *intf
234+
continue
235+
}
236+
}
237+
}
238+
239+
// The namespace must be the same as what was configured
240+
if args.Netns != contMap.Sandbox {
241+
return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s",
242+
contMap.Sandbox, args.Netns)
243+
}
244+
245+
//
246+
// Check prevResults for ips, routes and dns against values found in the container
247+
if err := netns.Do(func(_ ns.NetNS) error {
248+
249+
// Check interface against values found in the container
250+
err := validateCniContainerInterface(contMap)
251+
if err != nil {
252+
return err
253+
}
254+
255+
err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs)
256+
if err != nil {
257+
return err
258+
}
259+
return nil
260+
}); err != nil {
261+
return err
262+
}
263+
264+
return nil
265+
266+
}
267+
268+
func validateCniContainerInterface(intf current.Interface) error {
269+
270+
var link netlink.Link
271+
var err error
272+
273+
if intf.Name == "" {
274+
return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name)
275+
}
276+
link, err = netlink.LinkByName(intf.Name)
277+
if err != nil {
278+
return fmt.Errorf("Container Interface name in prevResult: %s not found", intf.Name)
279+
}
280+
if intf.Sandbox == "" {
281+
return fmt.Errorf("Error: Container interface %s should not be in host namespace", link.Attrs().Name)
282+
}
283+
284+
_, isDummy := link.(*netlink.Dummy)
285+
if !isDummy {
286+
return fmt.Errorf("Error: Container interface %s not of type dummy", link.Attrs().Name)
287+
}
288+
289+
if intf.Mac != "" {
290+
if intf.Mac != link.Attrs().HardwareAddr.String() {
291+
return fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr)
292+
}
293+
}
294+
295+
if link.Attrs().Flags&net.FlagUp != net.FlagUp {
296+
return fmt.Errorf("Interface %s is down", intf.Name)
297+
}
298+
299+
return nil
300+
}
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2022 Arista Networks
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main_test
16+
17+
import (
18+
"github.com/onsi/gomega/gexec"
19+
20+
. "github.com/onsi/ginkgo"
21+
. "github.com/onsi/gomega"
22+
23+
"testing"
24+
)
25+
26+
var pathToLoPlugin string
27+
28+
func TestLoopback(t *testing.T) {
29+
RegisterFailHandler(Fail)
30+
RunSpecs(t, "plugins/main/dummy")
31+
}
32+
33+
var _ = BeforeSuite(func() {
34+
var err error
35+
pathToLoPlugin, err = gexec.Build("github.com/containernetworking/plugins/plugins/main/dummy")
36+
Expect(err).NotTo(HaveOccurred())
37+
})
38+
39+
var _ = AfterSuite(func() {
40+
gexec.CleanupBuildArtifacts()
41+
})

0 commit comments

Comments
 (0)