Skip to content

Commit ab5787b

Browse files
committed
fix: validate prevResult from preceding CNI plugin
The galactic-cni plugin silently ignored prevResult from a preceding plugin in the CNI chain, leaving it blind to prior chain state. - Validate prevResult is parseable as a valid CNI result during config parsing, failing fast rather than operating on garbage state - Validate prevResult contains at least one interface or IP assignment during ADD, rejecting empty or structurally broken chain results - Wire validation into parseConf and cmdAdd so invalid prevResult causes immediate failure before any kernel resources are created - Add unit tests for parseConf, validatePrevResult, validatePrevResultAdd, and cmdAdd prevResult handling across valid, empty, and invalid cases fixes #157
1 parent 99644ef commit ab5787b

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"
@@ -31,6 +32,15 @@ const (
3132
testRD65000_1 = "65000:1" // RD/RT for ASN 65000, NN 1
3233
testContainerID = "test-container"
3334
testInvalidBase62 = "abc-def" // shared invalid base62 string for tests
35+
testNetns = "/proc/1/ns/net"
36+
testMac = "aa:bb:cc:dd:ee:ff"
37+
testIfName = "eth0"
38+
39+
// testPrevResult is a valid CNI v1.0.0 result used in prevResult tests.
40+
testPrevResult = `{"cniVersion":"1.0.0",` +
41+
`"interfaces":[{"name":"` + testIfName + `","mac":"` + testMac + `",` +
42+
`"sandbox":"/proc/1/ns/net"}],` +
43+
`"ips":[{"version":"6","address":"fd00:1::1/64"}]}`
3444
)
3545

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

298320
for _, tt := range tests {
@@ -415,6 +437,92 @@ func TestSanitizeForError(t *testing.T) {
415437
}
416438
}
417439

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

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

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>"
@@ -52,6 +55,54 @@ func isValidSRv6Locator(s string) bool {
5255
return maskLen <= 64
5356
}
5457

58+
// validatePrevResult checks that the prevResult (from a preceding plugin in
59+
// the CNI chain) is a valid, parseable CNI result. Returns an error if the
60+
// result is non-nil but cannot be parsed as a versioned CNI result, ensuring
61+
// galactic-cni fails fast rather than silently operating on garbage state.
62+
func validatePrevResult(res types.Result) error {
63+
if res == nil {
64+
return nil
65+
}
66+
// Marshal to JSON and re-parse to verify the result is structurally valid.
67+
// This catches malformed results that survived CNI framework unmarshaling.
68+
jsonBytes, err := json.Marshal(res)
69+
if err != nil {
70+
return fmt.Errorf("marshal prevResult: %w", err)
71+
}
72+
if _, err := type100.NewResult(jsonBytes); err != nil {
73+
return fmt.Errorf("parse prevResult: %w", err)
74+
}
75+
return nil
76+
}
77+
78+
// validatePrevResultAdd performs content-level validation of prevResult during
79+
// the ADD operation. It ensures the preceding plugin produced a result with at
80+
// least one interface or IP assignment, which is the minimum expected structure
81+
// for any meaningful CNI chain. Returns nil when prevResult is nil (no
82+
// preceding plugin) or structurally valid with expected content.
83+
func validatePrevResultAdd(res types.Result) error {
84+
if res == nil {
85+
return nil
86+
}
87+
jsonBytes, err := json.Marshal(res)
88+
if err != nil {
89+
return fmt.Errorf("marshal prevResult: %w", err)
90+
}
91+
result, err := type100.NewResult(jsonBytes)
92+
if err != nil {
93+
return fmt.Errorf("parse prevResult: %w", err)
94+
}
95+
versioned, err := type100.GetResult(result)
96+
if err != nil {
97+
return fmt.Errorf("get prevResult version: %w", err)
98+
}
99+
// A valid prevResult must declare at least one interface or IP assignment.
100+
if len(versioned.Interfaces) == 0 && len(versioned.IPs) == 0 {
101+
return errors.New("prevResult declares no interfaces or IP assignments")
102+
}
103+
return nil
104+
}
105+
55106
// parseConf unmarshals the CNI configuration from stdin data and validates
56107
// the interface type and base62-encoded identifier fields. Returns an error
57108
// if the config is malformed, interface_type is unsupported, or VPC/
@@ -83,6 +134,11 @@ func parseConf(data []byte) (*PluginConf, error) {
83134
srv6LocatorErrMsg, sanitizeForError(conf.SRv6Locator),
84135
)
85136
}
137+
if conf.PrevResult != nil {
138+
if err := validatePrevResult(conf.PrevResult); err != nil {
139+
return nil, fmt.Errorf("invalid prevResult: %w", err)
140+
}
141+
}
86142
if conf.InterfaceType == "" {
87143
conf.InterfaceType = interfaceTypeVeth
88144
}

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)