Skip to content

Commit 3ed3985

Browse files
authored
Merge pull request #185 from datum-cloud/fix/issue-157
fix: validate prevResult from preceding CNI plugin
2 parents c3c62fd + db66426 commit 3ed3985

3 files changed

Lines changed: 204 additions & 0 deletions

File tree

internal/cni/cni_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/containernetworking/cni/pkg/skel"
1717
"github.com/containernetworking/cni/pkg/types"
18+
type100 "github.com/containernetworking/cni/pkg/types/100"
1819
bgpv1alpha1 "go.miloapis.com/cosmos/api/bgp/v1alpha1"
1920
apierrors "k8s.io/apimachinery/pkg/api/errors"
2021
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -30,6 +31,15 @@ const (
3031
testRD65000_1 = "65000:1" // RD/RT for ASN 65000, NN 1
3132
testContainerID = "test-container"
3233
testInvalidBase62 = "abc-def" // shared invalid base62 string for tests
34+
testNetns = "/proc/1/ns/net"
35+
testMac = "aa:bb:cc:dd:ee:ff"
36+
testIfName = "eth0"
37+
38+
// testPrevResult is a valid CNI v1.0.0 result used in prevResult tests.
39+
testPrevResult = `{"cniVersion":"1.0.0",` +
40+
`"interfaces":[{"name":"` + testIfName + `","mac":"` + testMac + `",` +
41+
`"sandbox":"/proc/1/ns/net"}],` +
42+
`"ips":[{"version":"6","address":"fd00:1::1/64"}]}`
3343
)
3444

3545
func fakeClient(objs ...client.Object) client.Client {
@@ -292,6 +302,18 @@ func TestParseConf(t *testing.T) {
292302
),
293303
wantErr: srv6LocatorErrMsg,
294304
},
305+
{
306+
name: "prevResult valid JSON result is accepted",
307+
input: fmt.Sprintf(
308+
`{"cniVersion":"1.0.0","name":"test",`+
309+
`"type":"galactic-cni","vpc":"%s",`+
310+
`"vpcattachment":"%s",`+
311+
`"prevResult":%s}`,
312+
testVPC, testAttachment, testPrevResult,
313+
),
314+
wantVPC: testVPC,
315+
wantIfType: interfaceTypeVeth,
316+
},
295317
}
296318

297319
for _, tt := range tests {
@@ -414,6 +436,92 @@ func TestSanitizeForError(t *testing.T) {
414436
}
415437
}
416438

439+
// ---- validatePrevResult --------------------------------------------------
440+
441+
func TestValidatePrevResult(t *testing.T) {
442+
validResult := &type100.Result{
443+
CNIVersion: cniVersion100,
444+
Interfaces: []*type100.Interface{
445+
{Name: testIfName, Mac: testMac, Sandbox: testNetns},
446+
},
447+
IPs: []*type100.IPConfig{
448+
{Address: *mustParseCIDR(t, "fd00:1::1/64")},
449+
},
450+
}
451+
452+
tests := []struct {
453+
name string
454+
input types.Result
455+
wantErr bool
456+
}{
457+
{"nil result allowed", nil, false},
458+
{"valid CNI result", validResult, false},
459+
}
460+
461+
for _, tt := range tests {
462+
t.Run(tt.name, func(t *testing.T) {
463+
err := validatePrevResult(tt.input)
464+
if tt.wantErr {
465+
if err == nil {
466+
t.Fatal("expected error, got nil")
467+
}
468+
return
469+
}
470+
if err != nil {
471+
t.Fatalf("unexpected error: %v", err)
472+
}
473+
})
474+
}
475+
}
476+
477+
func TestValidatePrevResultAdd(t *testing.T) {
478+
validWithInterface := &type100.Result{
479+
CNIVersion: cniVersion100,
480+
Interfaces: []*type100.Interface{
481+
{Name: testIfName, Mac: testMac, Sandbox: testNetns},
482+
},
483+
IPs: []*type100.IPConfig{
484+
{Address: *mustParseCIDR(t, "fd00:1::1/64")},
485+
},
486+
}
487+
validWithIPsOnly := &type100.Result{
488+
CNIVersion: cniVersion100,
489+
IPs: []*type100.IPConfig{
490+
{Address: *mustParseCIDR(t, "fd00:1::1/64")},
491+
},
492+
}
493+
emptyResult := &type100.Result{
494+
CNIVersion: cniVersion100,
495+
// No interfaces, no IPs — should fail content validation.
496+
}
497+
498+
tests := []struct {
499+
name string
500+
input types.Result
501+
wantErr bool
502+
}{
503+
{"nil result allowed", nil, false},
504+
{"valid result with interface", validWithInterface, false},
505+
{"valid result with IPs only", validWithIPsOnly, false},
506+
{"empty result (no interfaces or IPs)", emptyResult, true},
507+
}
508+
509+
for _, tt := range tests {
510+
t.Run(tt.name, func(t *testing.T) {
511+
err := validatePrevResultAdd(tt.input)
512+
if tt.wantErr {
513+
if err == nil {
514+
t.Fatal("expected error, got nil")
515+
}
516+
return
517+
}
518+
if err != nil {
519+
t.Fatalf("unexpected error: %v", err)
520+
}
521+
})
522+
}
523+
}
524+
417525
// ---- bgpVRFInstanceName --------------------------------------------------
418526

419527
func TestBGPVRFInstanceName(t *testing.T) {
@@ -1336,3 +1444,33 @@ func TestProbeAPIServerMalformedKubeconfig(t *testing.T) {
13361444
t.Fatalf("error %q does not contain original error", err.Error())
13371445
}
13381446
}
1447+
1448+
// ---- cmdAdd prevResult validation ----------------------------------------
1449+
1450+
func TestCmdAddPrevResultValid(t *testing.T) {
1451+
// prevResult that is a valid CNI result. cmdAdd should pass prevResult
1452+
// validation and fail later due to missing NODE_NAME env var.
1453+
conf := fmt.Sprintf(
1454+
`{"cniVersion":"1.0.0","name":"test",`+
1455+
`"type":"galactic-cni","vpc":"%s",`+
1456+
`"vpcattachment":"%s",`+
1457+
`"prevResult":%s}`,
1458+
testVPC, testAttachment, testPrevResult,
1459+
)
1460+
args := &skel.CmdArgs{
1461+
ContainerID: testContainerID,
1462+
StdinData: []byte(conf),
1463+
}
1464+
1465+
err := cmdAdd(args)
1466+
if err == nil {
1467+
t.Fatal("expected cmdAdd to fail for missing NODE_NAME, got nil")
1468+
}
1469+
// Should fail on NODE_NAME, not prevResult.
1470+
if strings.Contains(err.Error(), "prevResult validation in ADD") {
1471+
t.Fatalf("prevResult should not cause error when valid, got: %v", err)
1472+
}
1473+
if !strings.Contains(err.Error(), "NODE_NAME") {
1474+
t.Fatalf("expected NODE_NAME error, got: %v", err)
1475+
}
1476+
}

internal/cni/config.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import (
99
"errors"
1010
"fmt"
1111
"net"
12+
13+
"github.com/containernetworking/cni/pkg/types"
14+
type100 "github.com/containernetworking/cni/pkg/types/100"
1215
)
1316

1417
const sanitizeForErrorBinary = "<binary>"
@@ -93,6 +96,54 @@ func parseStatusConf(data []byte) error {
9396
return nil
9497
}
9598

99+
// validatePrevResult checks that the prevResult (from a preceding plugin in
100+
// the CNI chain) is a valid, parseable CNI result. Returns an error if the
101+
// result is non-nil but cannot be parsed as a versioned CNI result, ensuring
102+
// galactic-cni fails fast rather than silently operating on garbage state.
103+
func validatePrevResult(res types.Result) error {
104+
if res == nil {
105+
return nil
106+
}
107+
// Marshal to JSON and re-parse to verify the result is structurally valid.
108+
// This catches malformed results that survived CNI framework unmarshaling.
109+
jsonBytes, err := json.Marshal(res)
110+
if err != nil {
111+
return fmt.Errorf("marshal prevResult: %w", err)
112+
}
113+
if _, err := type100.NewResult(jsonBytes); err != nil {
114+
return fmt.Errorf("parse prevResult: %w", err)
115+
}
116+
return nil
117+
}
118+
119+
// validatePrevResultAdd performs content-level validation of prevResult during
120+
// the ADD operation. It ensures the preceding plugin produced a result with at
121+
// least one interface or IP assignment, which is the minimum expected structure
122+
// for any meaningful CNI chain. Returns nil when prevResult is nil (no
123+
// preceding plugin) or structurally valid with expected content.
124+
func validatePrevResultAdd(res types.Result) error {
125+
if res == nil {
126+
return nil
127+
}
128+
jsonBytes, err := json.Marshal(res)
129+
if err != nil {
130+
return fmt.Errorf("marshal prevResult: %w", err)
131+
}
132+
result, err := type100.NewResult(jsonBytes)
133+
if err != nil {
134+
return fmt.Errorf("parse prevResult: %w", err)
135+
}
136+
versioned, err := type100.GetResult(result)
137+
if err != nil {
138+
return fmt.Errorf("get prevResult version: %w", err)
139+
}
140+
// A valid prevResult must declare at least one interface or IP assignment.
141+
if len(versioned.Interfaces) == 0 && len(versioned.IPs) == 0 {
142+
return errors.New("prevResult declares no interfaces or IP assignments")
143+
}
144+
return nil
145+
}
146+
96147
// parseConf unmarshals the CNI configuration from stdin data and validates
97148
// the interface type and base62-encoded identifier fields. Returns an error
98149
// if the config is malformed, interface_type is unsupported, or VPC/
@@ -124,6 +175,11 @@ func parseConf(data []byte) (*PluginConf, error) {
124175
srv6LocatorErrMsg, sanitizeForError(conf.SRv6Locator),
125176
)
126177
}
178+
if conf.PrevResult != nil {
179+
if err := validatePrevResult(conf.PrevResult); err != nil {
180+
return nil, fmt.Errorf("invalid prevResult: %w", err)
181+
}
182+
}
127183
if conf.InterfaceType == "" {
128184
conf.InterfaceType = interfaceTypeVeth
129185
}

internal/cni/ops_add.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ func cmdAdd(args *skel.CmdArgs) error {
2727
return err
2828
}
2929

30+
// Validate prevResult structure when present. The preceding plugin in the
31+
// CNI chain should have produced a result with at least one interface or IP
32+
// assignment. A nil or structurally broken prevResult indicates a mis-
33+
// configured chain that galactic-cni should not silently ignore.
34+
if pluginConf.PrevResult != nil {
35+
if err := validatePrevResultAdd(pluginConf.PrevResult); err != nil {
36+
return fmt.Errorf("prevResult validation in ADD: %w", err)
37+
}
38+
}
39+
3040
nodeName := os.Getenv("NODE_NAME")
3141
if nodeName == "" {
3242
return errors.New("NODE_NAME environment variable is not set")

0 commit comments

Comments
 (0)