Skip to content

Commit 7fcf8e4

Browse files
committed
dummy: Create a Dummy CNI plugin that creates a virtual interface.
Leverages the Linux dummy interface type to create network interfaces that permists routing packets through the Linux kernel without them being transmitted. This solution allows use of arbitrary non-loopback IP addresses within the container. Related to #466 Signed-off-by: Mircea Iordache-Sica <[email protected]>
1 parent 6264f7b commit 7fcf8e4

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)