Skip to content

Commit 67a02b9

Browse files
authored
add support for configure static IPv6 addresses on NLB (#2790)
* add support for static ipv6 addresses * address comments
1 parent 116c108 commit 67a02b9

File tree

8 files changed

+855
-167
lines changed

8 files changed

+855
-167
lines changed

docs/guide/service/annotations.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
| [service.beta.kubernetes.io/aws-load-balancer-healthcheck-interval](#healthcheck-interval) | integer | 10 | |
4242
| [service.beta.kubernetes.io/aws-load-balancer-eip-allocations](#eip-allocations) | stringList | | internet-facing lb only. Length must match the number of subnets|
4343
| [service.beta.kubernetes.io/aws-load-balancer-private-ipv4-addresses](#private-ipv4-addresses) | stringList | | internal lb only. Length must match the number of subnets |
44+
| [service.beta.kubernetes.io/aws-load-balancer-ipv6-addresses](#ipv6-addresses) | stringList | | dualstack lb only. Length must match the number of subnets |
4445
| [service.beta.kubernetes.io/aws-load-balancer-target-group-attributes](#target-group-attributes) | stringMap | | |
4546
| [service.beta.kubernetes.io/aws-load-balancer-subnets](#subnets) | stringList | | |
4647
| [service.beta.kubernetes.io/aws-load-balancer-alpn-policy](#alpn-policy) | stringList | | |
@@ -146,7 +147,7 @@ on the load balancer.
146147
- <a name="eip-allocations">`service.beta.kubernetes.io/aws-load-balancer-eip-allocations`</a> specifies a list of [elastic IP address](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html) configuration for an internet-facing NLB.
147148

148149
!!!note
149-
- This configuration is optional and use can use it to assign static IP addresses to your NLB
150+
- This configuration is optional, and you can use it to assign static IP addresses to your NLB
150151
- You must specify the same number of eip allocations as load balancer subnets [annotation](#subnets)
151152
- NLB must be internet-facing
152153

@@ -160,7 +161,7 @@ on the load balancer.
160161

161162
!!!note
162163
- NLB must be internal
163-
- This configuration is optional and use can use it to assign static IP addresses to your NLB
164+
- This configuration is optional, and you can use it to assign static IPv4 addresses to your NLB
164165
- You must specify the same number of private IPv4 addresses as load balancer subnets [annotation](#subnets)
165166
- You must specify the IPv4 addresses from the load balancer subnet IPv4 ranges
166167

@@ -169,6 +170,19 @@ on the load balancer.
169170
service.beta.kubernetes.io/aws-load-balancer-private-ipv4-addresses: 192.168.10.15, 192.168.32.16
170171
```
171172

173+
- <a name="ipv6-addresses">`service.beta.kubernetes.io/aws-load-balancer-ipv6-addresses`</a> specifies a list of IPv6 addresses for an dualstack NLB.
174+
175+
!!!note
176+
- NLB must be dualstack
177+
- This configuration is optional, and you can use it to assign static IPv6 addresses to your NLB
178+
- You must specify the same number of private IPv6 addresses as load balancer subnets [annotation](#subnets)
179+
- You must specify the IPv6 addresses from the load balancer subnet IPv6 ranges
180+
181+
!!!example
182+
```
183+
service.beta.kubernetes.io/aws-load-balancer-ipv6-addresses: 2600:1f13:837:8501::1, 2600:1f13:837:8504::1
184+
```
185+
172186
## Traffic Listening
173187
Traffic Listening can be controlled with following annotations:
174188

pkg/annotations/constants.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,11 @@ const (
7373
SvcLBSuffixHCProtocol = "aws-load-balancer-healthcheck-protocol"
7474
SvcLBSuffixHCPort = "aws-load-balancer-healthcheck-port"
7575
SvcLBSuffixHCPath = "aws-load-balancer-healthcheck-path"
76-
SvcLBSuffixEIPAllocations = "aws-load-balancer-eip-allocations"
77-
SvcLBSuffixPrivateIpv4Addresses = "aws-load-balancer-private-ipv4-addresses"
7876
SvcLBSuffixTargetGroupAttributes = "aws-load-balancer-target-group-attributes"
7977
SvcLBSuffixSubnets = "aws-load-balancer-subnets"
78+
SvcLBSuffixEIPAllocations = "aws-load-balancer-eip-allocations"
79+
SvcLBSuffixPrivateIpv4Addresses = "aws-load-balancer-private-ipv4-addresses"
80+
SvcLBSuffixIpv6Addresses = "aws-load-balancer-ipv6-addresses"
8081
SvcLBSuffixALPNPolicy = "aws-load-balancer-alpn-policy"
8182
SvcLBSuffixTargetNodeLabels = "aws-load-balancer-target-node-labels"
8283
SvcLBSuffixLoadBalancerAttributes = "aws-load-balancer-attributes"

pkg/deploy/elbv2/load_balancer_manager.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ func buildSDKSubnetMapping(modelSubnetMapping elbv2model.SubnetMapping) *elbv2sd
287287
return &elbv2sdk.SubnetMapping{
288288
AllocationId: modelSubnetMapping.AllocationID,
289289
PrivateIPv4Address: modelSubnetMapping.PrivateIPv4Address,
290+
IPv6Address: modelSubnetMapping.IPv6Address,
290291
SubnetId: awssdk.String(modelSubnetMapping.SubnetID),
291292
}
292293
}

pkg/model/elbv2/load_balancer.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ type SubnetMapping struct {
102102
// [Network Load Balancers] The private IPv4 address for an internal load balancer.
103103
PrivateIPv4Address *string `json:"privateIPv4Address,omitempty"`
104104

105+
// [Network Load Balancers] The IPv6 address.
106+
IPv6Address *string `json:"ipv6Address,omitempty"`
107+
105108
// The ID of the subnet.
106109
SubnetID string `json:"subnetID"`
107110
}

pkg/networking/utils.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package networking
22

3-
import "inet.af/netaddr"
3+
import (
4+
awssdk "github.com/aws/aws-sdk-go/aws"
5+
ec2sdk "github.com/aws/aws-sdk-go/service/ec2"
6+
"inet.af/netaddr"
7+
"net/netip"
8+
)
49

510
// TODO: replace netaddr package with built-in netip package once golang 1.18 released: https://pkg.go.dev/net/netip@master#Prefix
611

@@ -26,3 +31,47 @@ func IsIPWithinCIDRs(ip netaddr.IP, cidrs []netaddr.IPPrefix) bool {
2631
}
2732
return false
2833
}
34+
35+
// FilterIPsWithinCIDRs returns IP addresses that were within specified CIDRs.
36+
func FilterIPsWithinCIDRs(ips []netip.Addr, cidrs []netip.Prefix) []netip.Addr {
37+
var ipsWithinCIDRs []netip.Addr
38+
for _, ip := range ips {
39+
for _, cidr := range cidrs {
40+
if cidr.Contains(ip) {
41+
ipsWithinCIDRs = append(ipsWithinCIDRs, ip)
42+
break
43+
}
44+
}
45+
}
46+
return ipsWithinCIDRs
47+
}
48+
49+
// GetSubnetAssociatedIPv4CIDRs returns the IPv4 CIDRs associated with EC2 subnet
50+
func GetSubnetAssociatedIPv4CIDRs(subnet *ec2sdk.Subnet) ([]netip.Prefix, error) {
51+
if subnet.CidrBlock == nil {
52+
return nil, nil
53+
}
54+
cidrBlock := awssdk.StringValue(subnet.CidrBlock)
55+
ipv4CIDR, err := netip.ParsePrefix(cidrBlock)
56+
if err != nil {
57+
return nil, err
58+
}
59+
return []netip.Prefix{ipv4CIDR}, nil
60+
}
61+
62+
// GetSubnetAssociatedIPv6CIDRs returns the IPv6 CIDRs associated with EC2 subnet
63+
func GetSubnetAssociatedIPv6CIDRs(subnet *ec2sdk.Subnet) ([]netip.Prefix, error) {
64+
var ipv6CIDRs []netip.Prefix
65+
for _, cidrAssociation := range subnet.Ipv6CidrBlockAssociationSet {
66+
if awssdk.StringValue(cidrAssociation.Ipv6CidrBlockState.State) != ec2sdk.SubnetCidrBlockStateCodeAssociated {
67+
continue
68+
}
69+
cidrBlock := awssdk.StringValue(cidrAssociation.Ipv6CidrBlock)
70+
ipv6CIDR, err := netip.ParsePrefix(cidrBlock)
71+
if err != nil {
72+
return nil, err
73+
}
74+
ipv6CIDRs = append(ipv6CIDRs, ipv6CIDR)
75+
}
76+
return ipv6CIDRs, nil
77+
}

pkg/networking/utils_test.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package networking
22

33
import (
4+
awssdk "github.com/aws/aws-sdk-go/aws"
5+
ec2sdk "github.com/aws/aws-sdk-go/service/ec2"
46
"github.com/pkg/errors"
57
"github.com/stretchr/testify/assert"
68
"inet.af/netaddr"
9+
"net/netip"
710
"testing"
811
)
912

@@ -137,3 +140,201 @@ func TestIsIPWithinCIDRs(t *testing.T) {
137140
})
138141
}
139142
}
143+
144+
func TestFilterIPsWithinCIDRs(t *testing.T) {
145+
type args struct {
146+
ips []netip.Addr
147+
cidrs []netip.Prefix
148+
}
149+
tests := []struct {
150+
name string
151+
args args
152+
want []netip.Addr
153+
}{
154+
{
155+
name: "ipv4 addresses within one CIDR",
156+
args: args{
157+
ips: []netip.Addr{
158+
netip.MustParseAddr("192.168.1.42"),
159+
netip.MustParseAddr("192.168.2.42"),
160+
netip.MustParseAddr("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
161+
},
162+
cidrs: []netip.Prefix{
163+
netip.MustParsePrefix("192.168.1.0/24"),
164+
},
165+
},
166+
want: []netip.Addr{
167+
netip.MustParseAddr("192.168.1.42"),
168+
},
169+
},
170+
{
171+
name: "ipv4 addresses within multiple CIDRs",
172+
args: args{
173+
ips: []netip.Addr{
174+
netip.MustParseAddr("192.168.1.42"),
175+
netip.MustParseAddr("192.168.2.42"),
176+
netip.MustParseAddr("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
177+
},
178+
cidrs: []netip.Prefix{
179+
netip.MustParsePrefix("192.168.1.0/24"),
180+
netip.MustParsePrefix("192.168.0.0/16"),
181+
},
182+
},
183+
want: []netip.Addr{
184+
netip.MustParseAddr("192.168.1.42"),
185+
netip.MustParseAddr("192.168.2.42"),
186+
},
187+
},
188+
{
189+
name: "ipv6 addresses within one CIDR",
190+
args: args{
191+
ips: []netip.Addr{
192+
netip.MustParseAddr("2600:1F13:0837:8500::1"),
193+
netip.MustParseAddr("2600:1F13:0837:8504::1"),
194+
netip.MustParseAddr("192.168.1.42"),
195+
},
196+
cidrs: []netip.Prefix{
197+
netip.MustParsePrefix("2600:1f13:837:8500::/64"),
198+
},
199+
},
200+
want: []netip.Addr{
201+
netip.MustParseAddr("2600:1F13:0837:8500::1"),
202+
},
203+
},
204+
{
205+
name: "ipv6 addresses within multiple CIDRs",
206+
args: args{
207+
ips: []netip.Addr{
208+
netip.MustParseAddr("2600:1F13:0837:8500::1"),
209+
netip.MustParseAddr("2600:1F13:0837:8504::1"),
210+
netip.MustParseAddr("192.168.1.42"),
211+
},
212+
cidrs: []netip.Prefix{
213+
netip.MustParsePrefix("2600:1f13:837:8500::/64"),
214+
netip.MustParsePrefix("2600:1f13:837:8500::/56"),
215+
},
216+
},
217+
want: []netip.Addr{
218+
netip.MustParseAddr("2600:1F13:0837:8500::1"),
219+
netip.MustParseAddr("2600:1F13:0837:8504::1"),
220+
},
221+
},
222+
}
223+
for _, tt := range tests {
224+
t.Run(tt.name, func(t *testing.T) {
225+
got := FilterIPsWithinCIDRs(tt.args.ips, tt.args.cidrs)
226+
assert.Equal(t, tt.want, got)
227+
})
228+
}
229+
}
230+
231+
func TestGetSubnetAssociatedIPv4CIDRs(t *testing.T) {
232+
type args struct {
233+
subnet *ec2sdk.Subnet
234+
}
235+
tests := []struct {
236+
name string
237+
args args
238+
want []netip.Prefix
239+
wantErr error
240+
}{
241+
{
242+
name: "one IPv4 CIDR",
243+
args: args{
244+
subnet: &ec2sdk.Subnet{
245+
CidrBlock: awssdk.String("192.168.1.0/24"),
246+
},
247+
},
248+
want: []netip.Prefix{
249+
netip.MustParsePrefix("192.168.1.0/24"),
250+
},
251+
},
252+
}
253+
for _, tt := range tests {
254+
t.Run(tt.name, func(t *testing.T) {
255+
got, err := GetSubnetAssociatedIPv4CIDRs(tt.args.subnet)
256+
if tt.wantErr != nil {
257+
assert.EqualError(t, err, tt.wantErr.Error())
258+
} else {
259+
assert.Equal(t, tt.want, got)
260+
}
261+
})
262+
}
263+
}
264+
265+
func TestGetSubnetAssociatedIPv6CIDRs(t *testing.T) {
266+
type args struct {
267+
subnet *ec2sdk.Subnet
268+
}
269+
tests := []struct {
270+
name string
271+
args args
272+
want []netip.Prefix
273+
wantErr error
274+
}{
275+
{
276+
name: "one IPv6 CIDR",
277+
args: args{
278+
subnet: &ec2sdk.Subnet{
279+
CidrBlock: awssdk.String("192.168.1.0/24"),
280+
Ipv6CidrBlockAssociationSet: []*ec2sdk.SubnetIpv6CidrBlockAssociation{
281+
{
282+
Ipv6CidrBlock: awssdk.String("2600:1f13:837:8500::/64"),
283+
Ipv6CidrBlockState: &ec2sdk.SubnetCidrBlockState{
284+
State: awssdk.String(ec2sdk.SubnetCidrBlockStateCodeAssociated),
285+
},
286+
},
287+
},
288+
},
289+
},
290+
want: []netip.Prefix{
291+
netip.MustParsePrefix("2600:1f13:837:8500::/64"),
292+
},
293+
},
294+
{
295+
name: "multiple IPv6 CIDR",
296+
args: args{
297+
subnet: &ec2sdk.Subnet{
298+
CidrBlock: awssdk.String("192.168.1.0/24"),
299+
Ipv6CidrBlockAssociationSet: []*ec2sdk.SubnetIpv6CidrBlockAssociation{
300+
{
301+
Ipv6CidrBlock: awssdk.String("2600:1f13:837:8500::/64"),
302+
Ipv6CidrBlockState: &ec2sdk.SubnetCidrBlockState{
303+
State: awssdk.String(ec2sdk.SubnetCidrBlockStateCodeAssociated),
304+
},
305+
},
306+
{
307+
Ipv6CidrBlock: awssdk.String("2600:1f13:837:8504::/64"),
308+
Ipv6CidrBlockState: &ec2sdk.SubnetCidrBlockState{
309+
State: awssdk.String(ec2sdk.SubnetCidrBlockStateCodeAssociated),
310+
},
311+
},
312+
},
313+
},
314+
},
315+
want: []netip.Prefix{
316+
netip.MustParsePrefix("2600:1f13:837:8500::/64"),
317+
netip.MustParsePrefix("2600:1f13:837:8504::/64"),
318+
},
319+
},
320+
{
321+
name: "zero IPv6 CIDR",
322+
args: args{
323+
subnet: &ec2sdk.Subnet{
324+
CidrBlock: awssdk.String("192.168.1.0/24"),
325+
},
326+
},
327+
want: nil,
328+
},
329+
}
330+
for _, tt := range tests {
331+
t.Run(tt.name, func(t *testing.T) {
332+
got, err := GetSubnetAssociatedIPv6CIDRs(tt.args.subnet)
333+
if tt.wantErr != nil {
334+
assert.EqualError(t, err, tt.wantErr.Error())
335+
} else {
336+
assert.Equal(t, tt.want, got)
337+
}
338+
})
339+
}
340+
}

0 commit comments

Comments
 (0)