-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
336 lines (301 loc) · 10.3 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
package main
// A CNI IPAM plugin that takes /proc/cmdline and the environment variables and
// outputs the CNI configuration required for the external IP address for the
// pod in question. IPAM plugins send and receive JSON on stdout and stdin,
// respectively, and are passed arguments and configuration information via
// environment variables and the aforementioned JSON.
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"regexp"
"strconv"
"strings"
"github.com/containernetworking/cni/pkg/types"
cni "github.com/containernetworking/cni/pkg/types/040"
"github.com/m-lab/go/rtx"
)
// This value determines the output schema, and 0.3.1 is compatible with the
// schema defined in CniConfig.
const cniVersion = "0.3.1"
// Configuration objects to hold the CNI config that must be marshalled into Stdout
// IPConfig holds the IP configuration. The elements are strings to support v4 or v6.
type IPConfig struct {
Version IPaf `json:"version"`
Address string `json:"address"`
Gateway string `json:"gateway"`
}
// RouteConfig holds the subnets for which an interface should receive packets.
type RouteConfig struct {
Destination string `json:"dst"`
Gateway string `json:"gw"`
}
// DNSConfig holds a list of IP addresses for nameservers.
type DNSConfig struct {
Nameservers []string `json:"nameservers"`
}
// CniResult holds a complete CNI result, including the protocol version.
type CniResult struct {
CniVersion string `json:"cniVersion"`
IPs []*IPConfig `json:"ips,omitempty"`
Routes []*RouteConfig `json:"routes,omitempty"`
DNS *DNSConfig `json:"dns,omitempty"`
}
type JSONInput struct {
CNIVersion string `json:"cniVersion"`
Ipam struct {
Index int64 `json:"index"`
} `json:"ipam"`
}
// IPaf represents the IP address family.
type IPaf string
const (
v4 IPaf = "4"
v6 IPaf = "6"
)
var (
CNIConfig JSONInput
// ErrNoIPv6 is returned when we attempt to configure IPv6 on a system which has no v6 address.
ErrNoIPv6 = errors.New("IPv6 is not supported or configured")
)
// MakeGenericIPConfig makes IPConfig and DNSConfig objects out of the epoxy command line.
func MakeGenericIPConfig(procCmdline string, version IPaf) (*IPConfig, *RouteConfig, *DNSConfig, error) {
var destination string
switch version {
case v4:
destination = "0.0.0.0/0"
case v6:
destination = "::/0"
default:
return nil, nil, nil, errors.New("IP version can only be v4 or v6")
}
// Example substring: epoxy.ipv4=4.14.159.112/26,4.14.159.65,8.8.8.8,8.8.4.4
ipargsRe := regexp.MustCompile("epoxy.ipv" + string(version) + "=([^ ]+)")
matches := ipargsRe.FindStringSubmatch(procCmdline)
if len(matches) < 2 {
if version == v6 {
return nil, nil, nil, ErrNoIPv6
}
return nil, nil, nil, fmt.Errorf("Could not find epoxy.ip" + string(version) + " args")
}
// Example substring: 4.14.159.112/26,4.14.159.65,8.8.8.8,8.8.4.4
config := strings.Split(matches[1], ",")
if len(config) != 4 {
return nil, nil, nil, errors.New("Could not split up " + matches[1] + " into 4 parts")
}
Route := &RouteConfig{
Destination: destination,
Gateway: config[1],
}
IP := &IPConfig{
Version: version,
Address: config[0],
Gateway: config[1],
}
DNS := &DNSConfig{Nameservers: []string{config[2], config[3]}}
return IP, Route, DNS, nil
}
// MakeIPConfig makes the initial config from /proc/cmdline without incrementing up to the index.
func MakeIPConfig(procCmdline string) (*CniResult, error) {
config := &CniResult{CniVersion: cniVersion}
ipv4, route4, dnsv4, err := MakeGenericIPConfig(procCmdline, v4)
if err != nil {
// v4 config is required. Return an error if it is not present.
return nil, err
}
config.IPs = append(config.IPs, ipv4)
config.Routes = append(config.Routes, route4)
config.DNS = dnsv4
ipv6, route6, dnsv6, err := MakeGenericIPConfig(procCmdline, v6)
switch err {
case nil:
// v6 config is optional. Only set it up if the error is nil.
config.IPs = append(config.IPs, ipv6)
config.Routes = append(config.Routes, route6)
config.DNS.Nameservers = append(config.DNS.Nameservers, dnsv6.Nameservers...)
case ErrNoIPv6:
// Do nothing, but also don't return an error
default:
return nil, err
}
return config, nil
}
// Base10AdditionInBase16 implements a subtle addition operation that aids
// M-Lab operations staff in visually aligning IPv6 and IPv4 data.
//
// In an effort to keep with an established M-Lab deployment pattern, we
// ensure that the IPv4 last octet (printed in base 10) and the IPv6 last
// grouping (printed in base 16) match up visually.
//
// Some examples:
// IPv4 address 1.0.0.9 + index 12 -> 1.0.0.21
// IPv6 address 1f::9 + index 12 -> 1f::21
// IPv4 address 1.0.0.201 + index 12 -> 1.0.0.213
// IPv6 address 1f::201 + index 12 -> 1f::213
//
// Note that in the first example, the IPv6 address, when printed in decimal,
// is actually 33 (0x21). M-Lab already assigns IPv6 subnets in matching
// base-10 configurations, so this should work for our use case.
//
// The second example is a nice illustration of why this process has to involve
// the last two bytes of the IPv6 address, because 0x213 > 0xFF. It is for this
// reason that the function returns two bytes.
func Base10AdditionInBase16(octets []byte, index int64) ([]byte, error) {
if len(octets) != 2 {
return []byte{0, 0}, fmt.Errorf("passed-in slice %v was not of length 2", octets)
}
base16Number := 256*int64(octets[0]) + int64(octets[1])
base16String := strconv.FormatInt(base16Number, 16)
base10Number, err := strconv.ParseInt(base16String, 10, 64)
if err != nil {
return []byte{0, 0}, err
}
base10Number += index
base10String := strconv.FormatInt(base10Number, 10)
// All base 10 strings are valid base 16 numbers, so parse errors are
// impossible here in one sense, but it is also true that the number could (in
// the case of some edge-case strange inputs) be outside of the range of int16.
base16Result, err := strconv.ParseInt(base10String, 16, 16)
if err != nil {
return []byte{0, 0}, err
}
return []byte{byte(base16Result / 256), byte(base16Result % 256)}, nil
}
// AddIndexToIP updates a single IP in light of the discovered index.
func AddIndexToIP(config *IPConfig, index int64) error {
switch config.Version {
case v4:
// Add the index to the IPv4 address.
var a, b, c, d, subnet int64
_, err := fmt.Sscanf(config.Address, "%d.%d.%d.%d/%d", &a, &b, &c, &d, &subnet)
if err != nil {
return errors.New("Could not parse IPv4 address: " + config.Address)
}
if d+index > 255 || index <= 0 {
return errors.New("ihdex out of range for address")
}
config.Address = fmt.Sprintf("%d.%d.%d.%d/%d", a, b, c, d+index, subnet)
case v6:
// Add the index to the IPv6 address.
addrSubnet := strings.Split(config.Address, "/")
if len(addrSubnet) != 2 {
return fmt.Errorf("could not parse IPv6 IP/subnet %v", config.Address)
}
ipv6 := net.ParseIP(addrSubnet[0])
if ipv6 == nil {
return fmt.Errorf("cloud not parse IPv6 address %v", addrSubnet[0])
}
// Ensure that the byte array is 16 bytes. According to the "net" API docs,
// the byte array length and the IP address family are purposely decoupled. To
// ensure a 16 byte array as the underlying storage (which is what we need) we
// call To16() which has the job of ensuring 16 bytes of storage backing.
ipv6 = ipv6.To16()
lastoctets, err := Base10AdditionInBase16(ipv6[14:16], index)
if err != nil {
return err
}
ipv6[14] = lastoctets[0]
ipv6[15] = lastoctets[1]
config.Address = ipv6.String() + "/" + addrSubnet[1]
default:
return errors.New("unknown IP version")
}
return nil
}
// AddIndexToIPs updates the config in light of the discovered index.
func AddIndexToIPs(config *CniResult, index int64) error {
for _, ip := range config.IPs {
if err := AddIndexToIP(ip, index); err != nil {
return err
}
}
return nil
}
// MustReadProcCmdline reads /proc/cmdline or (if present) the environment
// variable PROC_CMDLINE_FOR_TESTING. The PROC_CMDLINE_FOR_TESTING environment
// variable should only be used for unit testing, and should not be used in
// production. No guarantee of future compatibility is made or implied if you
// use PROC_CMDLINE_FOR_TESTING for anything other than unit testing. If the
// environment variable and the file /proc/cmdline are both unreadable, call
// log.Fatal and exit.
func MustReadProcCmdline() string {
if text, isPresent := os.LookupEnv("PROC_CMDLINE_FOR_TESTING"); isPresent {
return text
}
procCmdline, err := ioutil.ReadFile("/proc/cmdline")
rtx.Must(err, "Could not read /proc/cmdline")
return string(procCmdline)
}
// ReadJSONInput unmarshals JSON input from stdin into a global variable.
func ReadJSONInput(r io.Reader) error {
dec := json.NewDecoder(r)
err := dec.Decode(&CNIConfig)
return err
}
// Cmd represents the possible CNI operations for an IPAM plugin.
type Cmd int
// The CNI operations we know about.
const (
AddCmd = iota
DelCmd
VersionCmd
CheckCmd
UnknownCmd
)
// ParseCmd returns the corresponding Cmd for a string.
func ParseCmd(cmd string) Cmd {
cmd = strings.ToLower(cmd)
switch cmd {
case "add":
return AddCmd
case "del":
return DelCmd
case "version":
return VersionCmd
case "check":
return CheckCmd
}
return UnknownCmd
}
// Add responds to the ADD command.
func Add() error {
err := ReadJSONInput(os.Stdin)
rtx.Must(err, "Could not unmarshall JSON from stdin")
procCmdline := MustReadProcCmdline()
config, err := MakeIPConfig(procCmdline)
rtx.Must(err, "Could not populate the IP configuration")
rtx.Must(AddIndexToIPs(config, CNIConfig.Ipam.Index), "Could not manipulate the IP")
data, err := json.Marshal(config)
rtx.Must(err, "failed to marshall CNI output to JSON")
result, err := cni.NewResult(data)
rtx.Must(err, "failed to create new result type from JSON")
return types.PrintResult(result, CNIConfig.CNIVersion)
}
// Version responds to the VERSION command.
func Version() {
fmt.Fprintf(os.Stdout, `{
"cniVersion": "0.3.1",
"supportedVersions": [ "0.2.0", "0.3.0", "0.3.1", "0.4.0" ]
}`)
}
// Put it all together.
func main() {
cmd := os.Getenv("CNI_COMMAND")
switch ParseCmd(cmd) {
case AddCmd:
Add()
case VersionCmd:
Version()
case DelCmd, CheckCmd:
// For DEL and CHECK we affirmatively and successfully do nothing.
default:
// To preserve old behavior: when in doubt, Add()
log.Printf("Unknown CNI_COMMAND value %q. Treating it like ADD.\n", cmd)
Add()
}
}