Skip to content

Commit 48383e4

Browse files
committed
Add generic IPvlan support with Alibaba HPN cloud provider
Introduce a configurable IPvlan driver that creates IPvlan slave interfaces in the pod namespace while keeping the parent netdev on the host. The implementation supports L2/L3/L3S modes, bridge/private/ vepa flags, and three addressing strategies (none, static, parentIPv6PrefixPodIPv4). Key changes: - Expand IPVlanConfig API with mode, flag, addressing, and route/ neighbor copy options - Add IPvlan validation in the config validation pipeline - Implement idempotent attach: detect existing target interface in pod netns on retry, tolerate EEXIST on address assignment - Use deterministic temp names (parentName_iv) serialized by mutex - Support user-configured routes with full Scope/Table/Source fields, sorted link-scope-first for gateway reachability - Fail-fast on neighbor resolution errors instead of silently degrading connectivity - Make route/neighbor copy from parent opt-in via config - Decouple RDMA discovery from IPvlan config - Add Alibaba Cloud provider that auto-detects HPN instances and generates the IPvlan preset for bond devices with global IPv6 Signed-off-by: Hongqi Yu <yuhongqi.yhq@alibaba-inc.com>
1 parent 7dc9ab5 commit 48383e4

13 files changed

Lines changed: 1632 additions & 13 deletions

File tree

pkg/apis/constants.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,9 @@ const (
3333
// VRFTableOffset is the offset used for VRF routing tables to avoid ID collisions
3434
// with reserved tables (0, 253, 254, 255) and to identify DRANET managed tables.
3535
VRFTableOffset = 1000
36+
37+
// IPvlan addressing strategy constants.
38+
IPVlanAddrNone = "none"
39+
IPVlanAddrStatic = "static"
40+
IPVlanAddrParentIPv6PrefixPodIPv4 = "parentIPv6PrefixPodIPv4"
3641
)

pkg/apis/types.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ type InterfaceConfig struct {
8686
// If provided, the interface will be enslaved to a VRF device with this name.
8787
// This enables grouping multiple network interfaces into the same VRF.
8888
VRF *VRFConfig `json:"vrf,omitempty"`
89+
90+
// IPVlan, when set, instructs the driver to create an IPvlan slave interface
91+
// from the parent netdev instead of moving the host netdev into the Pod's
92+
// network namespace. The parent netdev remains in the host namespace.
93+
// This is used for HPN (High-Performance Networking) fabrics where the
94+
// parent netdev must retain its identity and address on the fabric.
95+
IPVlan *IPVlanConfig `json:"ipvlan,omitempty"`
8996
}
9097

9198
// VRFConfig represents the configuration for a Virtual Routing and Forwarding domain.
@@ -100,6 +107,32 @@ type VRFConfig struct {
100107
Table *int `json:"table,omitempty"`
101108
}
102109

110+
// IPVlanConfig specifies that the driver should create an IPvlan slave interface
111+
// rather than moving the host netdev into the Pod namespace.
112+
type IPVlanConfig struct {
113+
// Mode is the kernel IPvlan mode: "l2" (default), "l3", or "l3s".
114+
Mode string `json:"mode,omitempty"`
115+
// Flag is the kernel IPvlan flag, only applicable in L2 mode:
116+
// "bridge" (default for L2), "private", or "vepa".
117+
Flag string `json:"flag,omitempty"`
118+
// Addressing controls how IP addresses are assigned to the slave.
119+
// When nil, defaults to IPVlanAddrParentIPv6PrefixPodIPv4 for backward compatibility.
120+
Addressing *IPVlanAddressConfig `json:"addressing,omitempty"`
121+
// CopyRoutesFromParent, when true, copies IPv6 routes from the parent
122+
// interface into the pod namespace.
123+
CopyRoutesFromParent *bool `json:"copyRoutesFromParent,omitempty"`
124+
// CopyNeighborsFromParent, when true, pre-resolves gateway neighbors
125+
// from the parent's NDP table into the pod namespace.
126+
CopyNeighborsFromParent *bool `json:"copyNeighborsFromParent,omitempty"`
127+
}
128+
129+
// IPVlanAddressConfig describes how IP addresses are assigned to an IPvlan slave.
130+
type IPVlanAddressConfig struct {
131+
// Type controls the addressing strategy.
132+
// Supported values: IPVlanAddrNone, IPVlanAddrStatic, IPVlanAddrParentIPv6PrefixPodIPv4.
133+
Type string `json:"type"`
134+
}
135+
103136
// RouteConfig represents a network route configuration.
104137
type RouteConfig struct {
105138
// Destination is the target network in CIDR format (e.g., "0.0.0.0/0", "10.0.0.0/8").

pkg/apis/validation.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,42 @@ func validateInterfaceConfig(cfg *InterfaceConfig, fieldPath string) (allErrors
181181
allErrors = append(allErrors, validateVRFConfig(cfg.VRF, fieldPath+".vrf")...)
182182
}
183183

184+
if cfg.IPVlan != nil {
185+
allErrors = append(allErrors, validateIPVlanConfig(cfg.IPVlan, cfg, fieldPath+".ipvlan")...)
186+
}
187+
188+
return allErrors
189+
}
190+
191+
func validateIPVlanConfig(cfg *IPVlanConfig, iface *InterfaceConfig, fieldPath string) (allErrors []error) {
192+
validModes := map[string]bool{"": true, "l2": true, "l3": true, "l3s": true}
193+
if !validModes[cfg.Mode] {
194+
allErrors = append(allErrors, fmt.Errorf("%s.mode: unsupported value %q, must be one of: l2, l3, l3s", fieldPath, cfg.Mode))
195+
}
196+
197+
effectiveMode := cfg.Mode
198+
if effectiveMode == "" {
199+
effectiveMode = "l2"
200+
}
201+
202+
validFlags := map[string]bool{"": true, "bridge": true, "private": true, "vepa": true}
203+
if !validFlags[cfg.Flag] {
204+
allErrors = append(allErrors, fmt.Errorf("%s.flag: unsupported value %q, must be one of: bridge, private, vepa", fieldPath, cfg.Flag))
205+
}
206+
if cfg.Flag != "" && effectiveMode != "l2" {
207+
allErrors = append(allErrors, fmt.Errorf("%s.flag: only valid in l2 mode, got mode=%q", fieldPath, effectiveMode))
208+
}
209+
210+
if cfg.Addressing != nil {
211+
validTypes := map[string]bool{IPVlanAddrNone: true, IPVlanAddrStatic: true, IPVlanAddrParentIPv6PrefixPodIPv4: true}
212+
if !validTypes[cfg.Addressing.Type] {
213+
allErrors = append(allErrors, fmt.Errorf("%s.addressing.type: unsupported value %q, must be one of: %s, %s, %s", fieldPath, cfg.Addressing.Type, IPVlanAddrNone, IPVlanAddrStatic, IPVlanAddrParentIPv6PrefixPodIPv4))
214+
}
215+
if cfg.Addressing.Type == IPVlanAddrStatic && len(iface.Addresses) == 0 {
216+
allErrors = append(allErrors, fmt.Errorf("%s.addressing.type=static requires interface.addresses to be set", fieldPath))
217+
}
218+
}
219+
184220
return allErrors
185221
}
186222

@@ -300,7 +336,8 @@ func ValidateRDMAOnlyConfig(raw *runtime.RawExtension) []error {
300336
config.Interface.MTU != nil || config.Interface.HardwareAddr != nil ||
301337
config.Interface.DHCP != nil || config.Interface.GSOMaxSize != nil ||
302338
config.Interface.GROMaxSize != nil || config.Interface.GSOIPv4MaxSize != nil ||
303-
config.Interface.GROIPv4MaxSize != nil || config.Interface.DisableEBPFPrograms != nil {
339+
config.Interface.GROIPv4MaxSize != nil || config.Interface.DisableEBPFPrograms != nil ||
340+
config.Interface.IPVlan != nil {
304341
allErrors = append(allErrors, fmt.Errorf("interface configuration is not supported for RDMA-only devices (no network interface present)"))
305342
}
306343
if len(config.Routes) > 0 {

pkg/apis/validation_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,3 +567,110 @@ func TestValidateNeighborConfig(t *testing.T) {
567567
})
568568
}
569569
}
570+
571+
func TestValidateIPVlanConfig(t *testing.T) {
572+
tests := []struct {
573+
name string
574+
cfg *IPVlanConfig
575+
iface *InterfaceConfig
576+
expectErr bool
577+
errCount int
578+
}{
579+
{
580+
name: "valid l2 bridge with parentIPv6PrefixPodIPv4",
581+
cfg: &IPVlanConfig{Mode: "l2", Flag: "bridge", Addressing: &IPVlanAddressConfig{Type: IPVlanAddrParentIPv6PrefixPodIPv4}},
582+
iface: &InterfaceConfig{},
583+
},
584+
{
585+
name: "valid l3 with none addressing",
586+
cfg: &IPVlanConfig{Mode: "l3", Addressing: &IPVlanAddressConfig{Type: IPVlanAddrNone}},
587+
iface: &InterfaceConfig{},
588+
},
589+
{
590+
name: "valid l3s with none addressing",
591+
cfg: &IPVlanConfig{Mode: "l3s", Addressing: &IPVlanAddressConfig{Type: IPVlanAddrNone}},
592+
iface: &InterfaceConfig{},
593+
},
594+
{
595+
name: "valid empty mode defaults to l2",
596+
cfg: &IPVlanConfig{},
597+
iface: &InterfaceConfig{},
598+
},
599+
{
600+
name: "valid static with addresses",
601+
cfg: &IPVlanConfig{Mode: "l2", Addressing: &IPVlanAddressConfig{Type: IPVlanAddrStatic}},
602+
iface: &InterfaceConfig{Addresses: []string{"10.0.0.1/24"}},
603+
},
604+
{
605+
name: "invalid mode",
606+
cfg: &IPVlanConfig{Mode: "ipv6"},
607+
iface: &InterfaceConfig{},
608+
expectErr: true,
609+
errCount: 1,
610+
},
611+
{
612+
name: "invalid flag",
613+
cfg: &IPVlanConfig{Flag: "unknown"},
614+
iface: &InterfaceConfig{},
615+
expectErr: true,
616+
errCount: 1,
617+
},
618+
{
619+
name: "flag on l3 mode",
620+
cfg: &IPVlanConfig{Mode: "l3", Flag: "bridge"},
621+
iface: &InterfaceConfig{},
622+
expectErr: true,
623+
errCount: 1,
624+
},
625+
{
626+
name: "static without addresses",
627+
cfg: &IPVlanConfig{Addressing: &IPVlanAddressConfig{Type: IPVlanAddrStatic}},
628+
iface: &InterfaceConfig{},
629+
expectErr: true,
630+
errCount: 1,
631+
},
632+
{
633+
name: "unknown addressing type",
634+
cfg: &IPVlanConfig{Addressing: &IPVlanAddressConfig{Type: "unknown"}},
635+
iface: &InterfaceConfig{},
636+
expectErr: true,
637+
errCount: 1,
638+
},
639+
{
640+
name: "multiple errors: invalid mode + invalid flag + flag on non-l2",
641+
cfg: &IPVlanConfig{Mode: "bad", Flag: "also-bad"},
642+
iface: &InterfaceConfig{},
643+
expectErr: true,
644+
errCount: 3,
645+
},
646+
}
647+
648+
for _, tt := range tests {
649+
t.Run(tt.name, func(t *testing.T) {
650+
errs := validateIPVlanConfig(tt.cfg, tt.iface, "interface.ipvlan")
651+
if (len(errs) > 0) != tt.expectErr {
652+
t.Errorf("validateIPVlanConfig() got errors: %v, want error=%v", errs, tt.expectErr)
653+
}
654+
if tt.expectErr && len(errs) != tt.errCount {
655+
t.Errorf("validateIPVlanConfig() got %d errors (%v), want %d", len(errs), errs, tt.errCount)
656+
}
657+
})
658+
}
659+
}
660+
661+
func TestValidateRDMAOnlyConfig_RejectsIPVlan(t *testing.T) {
662+
raw := newRawExtensionFromString(t, `{"interface":{"ipvlan":{"mode":"l2"}}}`)
663+
errs := ValidateRDMAOnlyConfig(raw)
664+
if len(errs) == 0 {
665+
t.Fatal("expected error for IPvlan on RDMA-only device, got none")
666+
}
667+
found := false
668+
for _, e := range errs {
669+
if strings.Contains(e.Error(), "interface configuration is not supported for RDMA-only devices") {
670+
found = true
671+
}
672+
}
673+
if !found {
674+
t.Errorf("expected 'interface configuration is not supported' error, got: %v", errs)
675+
}
676+
}

0 commit comments

Comments
 (0)