Skip to content

Commit cf01bf6

Browse files
types: add gws field to Route for ECMP suppor
Add a GWs []net.IP field to the Route struct to enable ECMP (Equal-Cost Multi-Path) routing via multiple next hops. When both GW and GWs are set, GW is treated as an additional next hop appended after GWs. The NextHops() method returns the combined list for use by plugin implementations. Also document the gws field and its merge behavior in SPEC.md. Signed-off-by: Yusho Yamaguchi <ys-yamaguchi@kddi.com>
1 parent 7c27007 commit cf01bf6

3 files changed

Lines changed: 79 additions & 9 deletions

File tree

SPEC.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,7 @@ Plugins must output a JSON object with the following keys upon a successful `ADD
585585
- `routes`: Routes created by this attachment:
586586
- `dst`: The destination of the route, in CIDR notation
587587
- `gw`: The next hop address. If unset, a value in `gateway` in the `ips` array may be used.
588+
- `gws` (list of strings, optional): A list of next hop addresses for ECMP (Equal-Cost Multi-Path) routing. Each entry is an IP address string. If both `gw` and `gws` are set, `gw` is treated as an additional next hop appended after the entries in `gws`.
588589
- `mtu` (uint): The MTU (Maximum transmission unit) along the path to the destination.
589590
- `advmss` (uint): The MSS (Maximal Segment Size) to advertise to these destinations when establishing TCP connections.
590591
- `priority` (uint): The priority of route, lower is higher.

pkg/types/types.go

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ func (d *DNS) Copy() *DNS {
180180
type Route struct {
181181
Dst net.IPNet
182182
GW net.IP
183+
GWs []net.IP
183184
MTU int
184185
AdvMSS int
185186
Priority int
@@ -198,7 +199,7 @@ func (r *Route) String() string {
198199
scope = fmt.Sprintf("%d", *r.Scope)
199200
}
200201

201-
return fmt.Sprintf("{Dst:%+v GW:%v MTU:%d AdvMSS:%d Priority:%d Table:%s Scope:%s}", r.Dst, r.GW, r.MTU, r.AdvMSS, r.Priority, table, scope)
202+
return fmt.Sprintf("{Dst:%+v GW:%v GWs:%v MTU:%d AdvMSS:%d Priority:%d Table:%s Scope:%s}", r.Dst, r.GW, r.GWs, r.MTU, r.AdvMSS, r.Priority, table, scope)
202203
}
203204

204205
func (r *Route) Copy() *Route {
@@ -215,6 +216,11 @@ func (r *Route) Copy() *Route {
215216
Scope: r.Scope,
216217
}
217218

219+
if len(r.GWs) > 0 {
220+
route.GWs = make([]net.IP, len(r.GWs))
221+
copy(route.GWs, r.GWs)
222+
}
223+
218224
if r.Table != nil {
219225
table := *r.Table
220226
route.Table = &table
@@ -228,6 +234,22 @@ func (r *Route) Copy() *Route {
228234
return route
229235
}
230236

237+
// NextHops returns the combined gateway list for ECMP routing.
238+
// If GW is set and not already present in GWs, it is appended after GWs.
239+
func (r *Route) NextHops() []net.IP {
240+
gws := make([]net.IP, len(r.GWs))
241+
copy(gws, r.GWs)
242+
if r.GW != nil {
243+
for _, gw := range r.GWs {
244+
if gw.Equal(r.GW) {
245+
return gws
246+
}
247+
}
248+
gws = append(gws, r.GW)
249+
}
250+
return gws
251+
}
252+
231253
// Well known error codes
232254
// see https://github.com/containernetworking/cni/blob/main/SPEC.md#error
233255
const (
@@ -277,13 +299,14 @@ func (e *Error) Print() error {
277299

278300
// JSON (un)marshallable types
279301
type route struct {
280-
Dst IPNet `json:"dst"`
281-
GW net.IP `json:"gw,omitempty"`
282-
MTU int `json:"mtu,omitempty"`
283-
AdvMSS int `json:"advmss,omitempty"`
284-
Priority int `json:"priority,omitempty"`
285-
Table *int `json:"table,omitempty"`
286-
Scope *int `json:"scope,omitempty"`
302+
Dst IPNet `json:"dst"`
303+
GW net.IP `json:"gw,omitempty"`
304+
GWs []net.IP `json:"gws,omitempty"`
305+
MTU int `json:"mtu,omitempty"`
306+
AdvMSS int `json:"advmss,omitempty"`
307+
Priority int `json:"priority,omitempty"`
308+
Table *int `json:"table,omitempty"`
309+
Scope *int `json:"scope,omitempty"`
287310
}
288311

289312
func (r *Route) UnmarshalJSON(data []byte) error {
@@ -294,6 +317,7 @@ func (r *Route) UnmarshalJSON(data []byte) error {
294317

295318
r.Dst = net.IPNet(rt.Dst)
296319
r.GW = rt.GW
320+
r.GWs = rt.GWs
297321
r.MTU = rt.MTU
298322
r.AdvMSS = rt.AdvMSS
299323
r.Priority = rt.Priority
@@ -307,6 +331,7 @@ func (r Route) MarshalJSON() ([]byte, error) {
307331
rt := route{
308332
Dst: IPNet(r.Dst),
309333
GW: r.GW,
334+
GWs: r.GWs,
310335
MTU: r.MTU,
311336
AdvMSS: r.AdvMSS,
312337
Priority: r.Priority,

pkg/types/types_test.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@ var _ = Describe("Types", func() {
107107
Expect(unmarshaled).To(Equal(example))
108108
})
109109

110+
It("marshals and unmarshals gws to JSON", func() {
111+
example.GWs = []net.IP{net.ParseIP("1.2.3.2"), net.ParseIP("1.2.3.3")}
112+
jsonBytes, err := json.Marshal(example)
113+
Expect(err).NotTo(HaveOccurred())
114+
Expect(jsonBytes).To(MatchJSON(`{ "dst": "1.2.3.0/24", "gw": "1.2.3.1", "gws": ["1.2.3.2", "1.2.3.3"], "mtu": 1500, "advmss": 1340, "priority": 100, "table": 50, "scope": 253 }`))
115+
116+
var unmarshaled types.Route
117+
Expect(json.Unmarshal(jsonBytes, &unmarshaled)).To(Succeed())
118+
Expect(unmarshaled.GWs).To(HaveLen(2))
119+
Expect(unmarshaled.GWs[0].String()).To(Equal("1.2.3.2"))
120+
Expect(unmarshaled.GWs[1].String()).To(Equal("1.2.3.3"))
121+
})
122+
110123
Context("when the json data is not valid", func() {
111124
Specify("UnmarshalJSON returns an error", func() {
112125
route := new(types.Route)
@@ -116,7 +129,38 @@ var _ = Describe("Types", func() {
116129
})
117130

118131
It("formats as a string with a hex mask", func() {
119-
Expect(example.String()).To(Equal(`{Dst:{IP:1.2.3.0 Mask:ffffff00} GW:1.2.3.1 MTU:1500 AdvMSS:1340 Priority:100 Table:50 Scope:253}`))
132+
Expect(example.String()).To(Equal(`{Dst:{IP:1.2.3.0 Mask:ffffff00} GW:1.2.3.1 GWs:[] MTU:1500 AdvMSS:1340 Priority:100 Table:50 Scope:253}`))
133+
})
134+
135+
It("NextHops returns only GWs when GW is nil", func() {
136+
example.GW = nil
137+
example.GWs = []net.IP{net.ParseIP("1.2.3.2"), net.ParseIP("1.2.3.3")}
138+
gws := example.NextHops()
139+
Expect(gws).To(HaveLen(2))
140+
Expect(gws[0].String()).To(Equal("1.2.3.2"))
141+
Expect(gws[1].String()).To(Equal("1.2.3.3"))
142+
})
143+
144+
It("NextHops appends GW after GWs when both are set", func() {
145+
example.GWs = []net.IP{net.ParseIP("1.2.3.2"), net.ParseIP("1.2.3.3")}
146+
gws := example.NextHops()
147+
Expect(gws).To(HaveLen(3))
148+
Expect(gws[0].String()).To(Equal("1.2.3.2"))
149+
Expect(gws[1].String()).To(Equal("1.2.3.3"))
150+
Expect(gws[2].String()).To(Equal("1.2.3.1"))
151+
})
152+
153+
It("NextHops returns empty slice when both GW and GWs are unset", func() {
154+
example.GW = nil
155+
Expect(example.NextHops()).To(BeEmpty())
156+
})
157+
158+
It("NextHops does not append GW if already present in GWs", func() {
159+
example.GWs = []net.IP{net.ParseIP("1.2.3.2"), example.GW}
160+
gws := example.NextHops()
161+
Expect(gws).To(HaveLen(2))
162+
Expect(gws[0].String()).To(Equal("1.2.3.2"))
163+
Expect(gws[1].String()).To(Equal("1.2.3.1"))
120164
})
121165
})
122166

0 commit comments

Comments
 (0)