Skip to content

Commit d31d171

Browse files
committed
Configures ephemeral port range for OVN SNAT'ing
There was a previous bug where when an egress packet would be SNAT'ed to the node IP, using a nodeport source port, it would cause reply traffic to get DNAT'ed to the nodeport load balancer. This happened because the egress connections were not conntracked correctly. This was fixed via: https://issues.redhat.com/browse/OCPBUGS-25889 https://issues.redhat.com/browse/FDP-291 However, that fix was not hardware offloadable. The ideal fix here would be to always commit to conntrack and have it be HW offloadable. Until we have a better solution, we can configure the port range for OVN to use on its SNAT. This applies to all SNATs for traffic that enters the local host or leaves the host. The new config option --ephemeral-port-range "<minPort>-<maxPort>" can be used to specify the port range to use with OVN. If not provided, this value will be automatically derived from the ephemeral port range in /proc/sys/net/ipv4/ip_local_port_range, which is typically set already to avoid nodeport range conflicts. Signed-off-by: Tim Rozet <[email protected]>
1 parent 75fe04c commit d31d171

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)