Skip to content

Commit 2017ede

Browse files
authored
Merge pull request #5265 from trozet/specify_ovn_ephemeral_port_range
Configures ephemeral port range for OVN SNAT'ing
2 parents b31c67a + d31d171 commit 2017ede

File tree

5 files changed

+102
-5
lines changed

5 files changed

+102
-5
lines changed

go-controller/pkg/config/config.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ const DefaultVXLANPort = 4789
3838

3939
const DefaultDBTxnTimeout = time.Second * 100
4040

41+
// DefaultEphemeralPortRange is used for unit testing only
42+
const DefaultEphemeralPortRange = "32768-60999"
43+
4144
// The following are global config parameters that other modules may access directly
4245
var (
4346
// Build information. Populated at build-time.
@@ -494,6 +497,10 @@ type GatewayConfig struct {
494497
DisableForwarding bool `gcfg:"disable-forwarding"`
495498
// AllowNoUplink (disabled by default) controls if the external gateway bridge without an uplink port is allowed in local gateway mode.
496499
AllowNoUplink bool `gcfg:"allow-no-uplink"`
500+
// EphemeralPortRange is the range of ports used by egress SNAT operations in OVN. Specifically for NAT where
501+
// the source IP of the NAT will be a shared Node IP address. If unset, the value will be determined by sysctl lookup
502+
// for the kernel's ephemeral range: net.ipv4.ip_local_port_range. Format is "<min port>-<max port>".
503+
EphemeralPortRange string `gfcg:"ephemeral-port-range"`
497504
}
498505

499506
// OvnAuthConfig holds client authentication and location details for
@@ -664,6 +671,9 @@ func PrepareTestConfig() error {
664671
Kubernetes.DisableRequestedChassis = false
665672
EnableMulticast = false
666673
Default.OVSDBTxnTimeout = 5 * time.Second
674+
if Gateway.Mode != GatewayModeDisabled {
675+
Gateway.EphemeralPortRange = DefaultEphemeralPortRange
676+
}
667677

668678
if err := completeConfig(); err != nil {
669679
return err
@@ -1509,6 +1519,14 @@ var OVNGatewayFlags = []cli.Flag{
15091519
Usage: "Allow the external gateway bridge without an uplink port in local gateway mode",
15101520
Destination: &cliConfig.Gateway.AllowNoUplink,
15111521
},
1522+
&cli.StringFlag{
1523+
Name: "ephemeral-port-range",
1524+
Usage: "The port range in '<min port>-<max port>' format for OVN to use when SNAT'ing to a node IP. " +
1525+
"This range should not collide with the node port range being used in Kubernetes. If not provided, " +
1526+
"the default value will be derived from checking the sysctl value of net.ipv4.ip_local_port_range on the node.",
1527+
Destination: &cliConfig.Gateway.EphemeralPortRange,
1528+
Value: Gateway.EphemeralPortRange,
1529+
},
15121530
// Deprecated CLI options
15131531
&cli.BoolFlag{
15141532
Name: "init-gateways",
@@ -1917,6 +1935,19 @@ func buildGatewayConfig(ctx *cli.Context, cli, file *config) error {
19171935
if !found {
19181936
return fmt.Errorf("invalid gateway mode %q: expect one of %s", string(Gateway.Mode), strings.Join(validModes, ","))
19191937
}
1938+
1939+
if len(Gateway.EphemeralPortRange) > 0 {
1940+
if !isValidEphemeralPortRange(Gateway.EphemeralPortRange) {
1941+
return fmt.Errorf("invalid ephemeral-port-range, should be in the format <min port>-<max port>")
1942+
}
1943+
} else {
1944+
// auto-detect ephermal range
1945+
portRange, err := getKernelEphemeralPortRange()
1946+
if err != nil {
1947+
return fmt.Errorf("unable to auto-detect ephemeral port range to use with OVN")
1948+
}
1949+
Gateway.EphemeralPortRange = portRange
1950+
}
19201951
}
19211952

19221953
// Options are only valid if Mode is not disabled
@@ -1927,6 +1958,9 @@ func buildGatewayConfig(ctx *cli.Context, cli, file *config) error {
19271958
if Gateway.NextHop != "" {
19281959
return fmt.Errorf("gateway next-hop option %q not allowed when gateway is disabled", Gateway.NextHop)
19291960
}
1961+
if len(Gateway.EphemeralPortRange) > 0 {
1962+
return fmt.Errorf("gateway ephemeral port range option not allowed when gateway is disabled")
1963+
}
19301964
}
19311965

19321966
if Gateway.Mode != GatewayModeShared && Gateway.VLANID != 0 {

go-controller/pkg/config/utils.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package config
33
import (
44
"fmt"
55
"net"
6+
"os"
67
"reflect"
8+
"regexp"
79
"strconv"
810
"strings"
911

@@ -328,3 +330,49 @@ func AllocateV6MasqueradeIPs(masqueradeSubnetNetworkAddress net.IP, masqueradeIP
328330
}
329331
return nil
330332
}
333+
334+
func isValidEphemeralPortRange(s string) bool {
335+
// Regex to match "<number>-<number>" with no extra characters
336+
re := regexp.MustCompile(`^(\d{1,5})-(\d{1,5})$`)
337+
matches := re.FindStringSubmatch(s)
338+
if matches == nil {
339+
return false
340+
}
341+
342+
minPort, err1 := strconv.Atoi(matches[1])
343+
maxPort, err2 := strconv.Atoi(matches[2])
344+
if err1 != nil || err2 != nil {
345+
return false
346+
}
347+
348+
// Port numbers must be in the 1-65535 range
349+
if minPort < 1 || minPort > 65535 || maxPort < 0 || maxPort > 65535 {
350+
return false
351+
}
352+
353+
return maxPort > minPort
354+
}
355+
356+
func getKernelEphemeralPortRange() (string, error) {
357+
data, err := os.ReadFile("/proc/sys/net/ipv4/ip_local_port_range")
358+
if err != nil {
359+
return "", fmt.Errorf("failed to read port range: %w", err)
360+
}
361+
362+
parts := strings.Fields(string(data))
363+
if len(parts) != 2 {
364+
return "", fmt.Errorf("unexpected format: %q", string(data))
365+
}
366+
367+
minPort, err := strconv.Atoi(parts[0])
368+
if err != nil {
369+
return "", fmt.Errorf("invalid min port: %w", err)
370+
}
371+
372+
maxPort, err := strconv.Atoi(parts[1])
373+
if err != nil {
374+
return "", fmt.Errorf("invalid max port: %w", err)
375+
}
376+
377+
return fmt.Sprintf("%d-%d", minPort, maxPort), nil
378+
}

go-controller/pkg/libovsdb/ops/router.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,10 @@ func buildNAT(
961961
Match: match,
962962
}
963963

964+
if config.Gateway.Mode != config.GatewayModeDisabled {
965+
nat.ExternalPortRange = config.Gateway.EphemeralPortRange
966+
}
967+
964968
if logicalPort != "" {
965969
nat.LogicalPort = &logicalPort
966970
}
@@ -1061,7 +1065,7 @@ func isEquivalentNAT(existing *nbdb.NAT, searched *nbdb.NAT) bool {
10611065
return false
10621066
}
10631067

1064-
// Compre externalIP if its not empty.
1068+
// Compare externalIP if it's not empty.
10651069
if searched.ExternalIP != "" && searched.ExternalIP != existing.ExternalIP {
10661070
return false
10671071
}

go-controller/pkg/ovn/gateway_test.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,27 +220,35 @@ func generateGatewayInitExpectedNB(testData []libovsdbtest.TestData, expectedOVN
220220
natUUID := fmt.Sprintf("nat-%d-UUID", i)
221221
natUUIDs = append(natUUIDs, natUUID)
222222
physicalIP, _ := util.MatchFirstIPNetFamily(utilnet.IsIPv6CIDR(subnet), l3GatewayConfig.IPAddresses)
223-
testData = append(testData, &nbdb.NAT{
223+
nat := nbdb.NAT{
224224
UUID: natUUID,
225225
ExternalIP: physicalIP.IP.String(),
226226
LogicalIP: subnet.String(),
227227
Options: map[string]string{"stateless": "false"},
228228
Type: nbdb.NATTypeSNAT,
229-
})
229+
}
230+
if config.Gateway.Mode != config.GatewayModeDisabled {
231+
nat.ExternalPortRange = config.DefaultEphemeralPortRange
232+
}
233+
testData = append(testData, &nat)
230234
}
231235
}
232236

233237
for i, physicalIP := range l3GatewayConfig.IPAddresses {
234238
natUUID := fmt.Sprintf("nat-join-%d-UUID", i)
235239
natUUIDs = append(natUUIDs, natUUID)
236240
joinLRPIP, _ := util.MatchFirstIPNetFamily(utilnet.IsIPv6CIDR(physicalIP), joinLRPIPs)
237-
testData = append(testData, &nbdb.NAT{
241+
nat := nbdb.NAT{
238242
UUID: natUUID,
239243
ExternalIP: physicalIP.IP.String(),
240244
LogicalIP: joinLRPIP.IP.String(),
241245
Options: map[string]string{"stateless": "false"},
242246
Type: nbdb.NATTypeSNAT,
243-
})
247+
}
248+
if config.Gateway.Mode != config.GatewayModeDisabled {
249+
nat.ExternalPortRange = config.DefaultEphemeralPortRange
250+
}
251+
testData = append(testData, &nat)
244252
}
245253

246254
testData = append(testData, &nbdb.MeterBand{
@@ -394,6 +402,7 @@ var _ = ginkgo.Describe("Gateway Init Operations", func() {
394402
ginkgo.Context("Gateway Creation Operations Shared Gateway Mode", func() {
395403
ginkgo.BeforeEach(func() {
396404
config.Gateway.Mode = config.GatewayModeShared
405+
config.Gateway.EphemeralPortRange = config.DefaultEphemeralPortRange
397406
})
398407

399408
ginkgo.It("creates an IPv4 gateway in OVN", func() {
@@ -1441,6 +1450,7 @@ var _ = ginkgo.Describe("Gateway Init Operations", func() {
14411450
ginkgo.BeforeEach(func() {
14421451
config.Gateway.Mode = config.GatewayModeLocal
14431452
config.IPv6Mode = false
1453+
config.Gateway.EphemeralPortRange = config.DefaultEphemeralPortRange
14441454
})
14451455

14461456
ginkgo.It("creates a dual-stack gateway in OVN", func() {

go-controller/pkg/ovn/namespace_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ var _ = ginkgo.Describe("OVN Namespace Operations", func() {
238238
ginkgo.It("creates an address set for existing nodes when the host network traffic namespace is created", func() {
239239
config.Gateway.Mode = config.GatewayModeShared
240240
config.Gateway.NodeportEnable = true
241+
config.Gateway.EphemeralPortRange = config.DefaultEphemeralPortRange
241242
var err error
242243
config.Default.ClusterSubnets, err = config.ParseClusterSubnetEntries(clusterCIDR)
243244
gomega.Expect(err).NotTo(gomega.HaveOccurred())

0 commit comments

Comments
 (0)