Skip to content

Commit 9e5f67a

Browse files
committed
Support SNAT Fixed Port Ranges
1 parent acc76bb commit 9e5f67a

File tree

9 files changed

+274
-9
lines changed

9 files changed

+274
-9
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,16 @@ Default: empty
267267
Specify a comma-separated list of IPv4 CIDRs to exclude from SNAT. For every item in the list an `iptables` rule and off\-VPC
268268
IP rule will be applied. If an item is not a valid ipv4 range it will be skipped. This should be used when `AWS_VPC_K8S_CNI_EXTERNALSNAT=false`.
269269

270+
#### `AWS_VPC_K8S_CNI_SNAT_FIXED_PORTS`
271+
272+
Type: String
273+
274+
Default: empty
275+
276+
Specify a comma-separated list of ports or port ranges that should be excluded from port randomization when SNAT is applied. Format should be individual ports or port ranges, for example: "80,443,8080-8090". This takes effect when `AWS_VPC_K8S_CNI_EXTERNALSNAT=false` and `AWS_VPC_K8S_CNI_RANDOMIZESNAT` is set to either `hashrandom` or `prng`. The specified ports will still be SNATed but will maintain their original source port values instead of being randomized.
277+
278+
*Note*: This is useful when you have applications that require consistent source ports for outbound connections, or when you need to ensure specific source ports are used for outbound traffic. The ports specified here will be excluded from the random port allocation mechanism while still being subject to SNAT rules.
279+
270280
#### `POD_MTU` (v1.16.4+)
271281

272282
Type: Integer as a String

cmd/aws-vpc-cni/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ const (
104104
envEnIPv6Egress = "ENABLE_V6_EGRESS"
105105
envEnIPv4Egress = "ENABLE_V4_EGRESS"
106106
envRandomizeSNAT = "AWS_VPC_K8S_CNI_RANDOMIZESNAT"
107+
envSNATFixedPorts = "AWS_VPC_K8S_CNI_SNAT_FIXED_PORTS"
107108
envIPCooldownPeriod = "IP_COOLDOWN_PERIOD"
108109
envDisablePodV6 = "DISABLE_POD_V6"
109110
)
@@ -145,6 +146,8 @@ type NetConf struct {
145146

146147
RandomizeSNAT string `json:"randomizeSNAT,omitempty"`
147148

149+
SNATFixedPorts string `json:"snatFixedPorts,omitempty"`
150+
148151
// MTU for eth0
149152
MTU string `json:"mtu,omitempty"`
150153

@@ -266,6 +269,7 @@ func generateJSON(jsonFile string, outFile string, getPrimaryIP func(ipv4 bool)
266269
pluginLogFile := utils.GetEnv(envPluginLogFile, defaultPluginLogFile)
267270
pluginLogLevel := utils.GetEnv(envPluginLogLevel, defaultPluginLogLevel)
268271
randomizeSNAT := utils.GetEnv(envRandomizeSNAT, defaultRandomizeSNAT)
272+
snatFixedPorts := utils.GetEnv(envSNATFixedPorts, "")
269273

270274
netconf := string(byteValue)
271275
netconf = strings.Replace(netconf, "__VETHPREFIX__", vethPrefix, -1)
@@ -279,6 +283,7 @@ func generateJSON(jsonFile string, outFile string, getPrimaryIP func(ipv4 bool)
279283
netconf = strings.Replace(netconf, "__EGRESSPLUGINIPAMDST__", egressIPAMDst, -1)
280284
netconf = strings.Replace(netconf, "__EGRESSPLUGINIPAMDATADIR__", egressIPAMDataDir, -1)
281285
netconf = strings.Replace(netconf, "__RANDOMIZESNAT__", randomizeSNAT, -1)
286+
netconf = strings.Replace(netconf, "__SNATFIXEDPORTS__", snatFixedPorts, -1)
282287
netconf = strings.Replace(netconf, "__NODEIP__", nodeIP, -1)
283288

284289
byteValue = []byte(netconf)

cmd/egress-cni-plugin/egressContext.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@ func (ec *egressContext) cmdAddEgressV4() (err error) {
267267
for _, ipc := range ec.TmpResult.IPs {
268268
if ipc.Address.IP.To4() != nil {
269269
// add SNAT chain/rules necessary for the container IPv6 egress traffic
270-
if err = snat.Add(ec.IPTablesIface, ec.NetConf.NodeIP, ipc.Address.IP, ipv4MulticastRange, ec.SnatChain, ec.SnatComment, ec.NetConf.RandomizeSNAT); err != nil {
270+
if err = snat.Add(ec.IPTablesIface, ec.NetConf.NodeIP, ipc.Address.IP, ipv4MulticastRange, ec.SnatChain, ec.SnatComment,
271+
ec.NetConf.RandomizeSNAT, ec.NetConf.SNATFixedPorts); err != nil {
271272
return err
272273
}
273274
}
@@ -392,7 +393,14 @@ func (ec *egressContext) cmdAddEgressV6() (err error) {
392393

393394
// set up SNAT in host for container IPv6 egress traffic
394395
// following line adds an ip6tables entries to NAT for IPv6 traffic between container v6if0 and node primary ENI (eth0)
395-
err = snat.Add(ec.IPTablesIface, ec.NetConf.NodeIP, containerIPv6, ipv6MulticastRange, ec.SnatChain, ec.SnatComment, ec.NetConf.RandomizeSNAT)
396+
err = snat.Add(ec.IPTablesIface,
397+
ec.NetConf.NodeIP,
398+
containerIPv6,
399+
ipv6MulticastRange,
400+
ec.SnatChain,
401+
ec.SnatComment,
402+
ec.NetConf.RandomizeSNAT,
403+
ec.NetConf.SNATFixedPorts)
396404
if err != nil {
397405
ec.Log.Errorf("setup host snat failed: %v", err)
398406
return err

cmd/egress-cni-plugin/netconf.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ type NetConf struct {
4646

4747
RandomizeSNAT string `json:"randomizeSNAT"`
4848

49+
SNATFixedPorts string `json:"snatFixedPorts"`
50+
4951
// IP to use as SNAT target
5052
NodeIP net.IP `json:"nodeIP"`
5153

cmd/egress-cni-plugin/snat/snat.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,36 @@
1414
package snat
1515

1616
import (
17+
"fmt"
1718
"net"
1819

1920
"github.com/aws/amazon-vpc-cni-k8s/pkg/iptableswrapper"
2021
"github.com/aws/amazon-vpc-cni-k8s/pkg/utils/cniutils"
2122
)
2223

23-
func iptRules(target, src net.IP, multicastRange, chain, comment string, useRandomFully, useHashRandom bool) [][]string {
24+
func iptRules(target, src net.IP, multicastRange, chain, comment string, useRandomFully, useHashRandom bool, fixedPorts string) [][]string {
2425
var rules [][]string
2526

2627
// Accept/ignore multicast (just because we can)
2728
rules = append(rules, []string{chain, "-d", multicastRange, "-j", "ACCEPT", "-m", "comment", "--comment", comment})
2829

29-
// SNAT
30+
if fixedPorts != "" && (useRandomFully || useHashRandom) {
31+
// Add protocol-specific SNAT rules for fixed ports
32+
for _, proto := range []string{"tcp", "udp", "sctp", "dccp"} {
33+
args := []string{
34+
chain,
35+
"-p", proto,
36+
"-m", "multiport",
37+
"--sports", fixedPorts,
38+
"-j", "SNAT",
39+
"--to-source", target.String(),
40+
"-m", "comment", "--comment", fmt.Sprintf("%s (fixed ports %s)", comment, proto),
41+
}
42+
rules = append(rules, args)
43+
}
44+
}
45+
46+
// SNAT rule for remaining traffic (protocol-agnostic)
3047
args := []string{
3148
chain,
3249
"-j", "SNAT",
@@ -47,7 +64,7 @@ func iptRules(target, src net.IP, multicastRange, chain, comment string, useRand
4764
}
4865

4966
// Add NAT entries to iptables for POD egress IPv6/IPv4 traffic
50-
func Add(ipt iptableswrapper.IPTablesIface, nodeIP, src net.IP, multicastRange, chain, comment, rndSNAT string) error {
67+
func Add(ipt iptableswrapper.IPTablesIface, nodeIP, src net.IP, multicastRange, chain, comment, rndSNAT, fixedPorts string) error {
5168
//Defaults to `random-fully` unless a different option is explicitly set via
5269
//`AWS_VPC_K8S_CNI_RANDOMIZESNAT`. If the underlying iptables version doesn't support
5370
//'random-fully`, we will fall back to `random`.
@@ -58,7 +75,7 @@ func Add(ipt iptableswrapper.IPTablesIface, nodeIP, src net.IP, multicastRange,
5875
useHashRandom, useRandomFully = true, false
5976
}
6077

61-
rules := iptRules(nodeIP, src, multicastRange, chain, comment, useRandomFully, useHashRandom)
78+
rules := iptRules(nodeIP, src, multicastRange, chain, comment, useRandomFully, useHashRandom, fixedPorts)
6279

6380
chains, err := ipt.ListChains("nat")
6481
if err != nil {

cmd/egress-cni-plugin/snat/snat_test.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,52 @@ func TestAddV4(t *testing.T) {
5757

5858
setupAddExpect(ipt, &actualChain, &actualRule)
5959

60-
err := Add(ipt, nodeIPv4, containerIPv4, ipv4MulticastRange, chainV4, comment, rndSNAT)
60+
err := Add(ipt, nodeIPv4, containerIPv4, ipv4MulticastRange, chainV4, comment, rndSNAT, "")
6161
assert.Nil(t, err)
6262

6363
assert.EqualValuesf(t, expectChain, actualChain, "iptables chain is expected to be created")
6464

6565
assert.EqualValuesf(t, expectRule, actualRule, "iptables rules are expected to be created")
6666
}
6767

68+
func TestAddV4WithExcludedPorts(t *testing.T) {
69+
ipt := mock_iptables.NewMockIPTablesIface(gomock.NewController(t))
70+
71+
expectChain := []string{chainV4}
72+
actualChain := []string{}
73+
74+
excludedPorts := "1024:2048,8080:8090"
75+
76+
expectRule := []string{
77+
fmt.Sprintf("nat %s -d %s -j ACCEPT -m comment --comment %s",
78+
chainV4, ipv4MulticastRange, comment),
79+
}
80+
81+
for _, proto := range []string{"tcp", "udp", "sctp", "dccp"} {
82+
expectRule = append(expectRule,
83+
fmt.Sprintf("nat %s -p %s -m multiport --sports %s -j SNAT --to-source %s -m comment --comment %s (fixed ports %s)",
84+
chainV4, proto, excludedPorts, nodeIPv4.String(), comment, proto))
85+
}
86+
87+
expectRule = append(expectRule,
88+
fmt.Sprintf("nat %s -j SNAT --to-source %s -m comment --comment %s --random",
89+
chainV4, nodeIPv4.String(), comment))
90+
91+
expectRule = append(expectRule,
92+
fmt.Sprintf("nat POSTROUTING -s %s -j %s -m comment --comment %s",
93+
containerIPv4.String(), chainV4, comment))
94+
95+
actualRule := []string{}
96+
97+
setupAddExpect(ipt, &actualChain, &actualRule)
98+
99+
err := Add(ipt, nodeIPv4, containerIPv4, ipv4MulticastRange, chainV4, comment, rndSNAT, excludedPorts)
100+
assert.Nil(t, err)
101+
102+
assert.EqualValuesf(t, expectChain, actualChain, "iptables chain is expected to be created")
103+
assert.EqualValuesf(t, expectRule, actualRule, "iptables rules are expected to be created")
104+
}
105+
68106
func TestDelV4(t *testing.T) {
69107
ipt := mock_iptables.NewMockIPTablesIface(gomock.NewController(t))
70108

misc/10-aws.conflist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"mtu": "__MTU__",
1919
"enabled": "__EGRESSPLUGINENABLED__",
2020
"randomizeSNAT": "__RANDOMIZESNAT__",
21+
"snatFixedPorts": "__SNATFIXEDPORTS__",
2122
"nodeIP": "__NODEIP__",
2223
"ipam": {
2324
"type": "host-local",

pkg/networkutils/network.go

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ const (
9797
// Default is "prng".
9898
envRandomizeSNAT = "AWS_VPC_K8S_CNI_RANDOMIZESNAT"
9999

100+
// This environment is used to specify port ranges which will be excluded from SNAT randomization.
101+
// This is useful if you still want the benefit of randomization but need to exclude certain ports
102+
// for compatibility with protocols such as STUN and TURN.
103+
// Defaults to empty.
104+
envSNATFixedPorts = "AWS_VPC_K8S_CNI_SNAT_FIXED_PORTS"
105+
100106
// envNodePortSupport is the name of environment variable that configures whether we implement support for
101107
// NodePorts on the primary ENI. This requires that we add additional iptables rules and loosen the kernel's
102108
// RPF check as described below. Defaults to true.
@@ -166,6 +172,7 @@ type linuxNetwork struct {
166172
useExternalSNAT bool
167173
ipv6EgressEnabled bool
168174
excludeSNATCIDRs []string
175+
snatFixedPorts []string
169176
externalServiceCIDRs []string
170177
typeOfSNAT snatType
171178
nodePortSupportEnabled bool
@@ -193,6 +200,7 @@ func New() NetworkAPIs {
193200
useExternalSNAT: useExternalSNAT(),
194201
ipv6EgressEnabled: ipV6EgressEnabled(),
195202
excludeSNATCIDRs: parseCIDRString(envExcludeSNATCIDRs),
203+
snatFixedPorts: parsePortRangeString(envSNATFixedPorts),
196204
externalServiceCIDRs: parseCIDRString(envExternalServiceCIDRs),
197205
typeOfSNAT: typeOfSNAT(),
198206
nodePortSupportEnabled: nodePortSupportEnabled(),
@@ -516,11 +524,36 @@ func (n *linuxNetwork) buildIptablesSNATRules(vpcCIDRs []string, primaryAddr *ne
516524
}})
517525
}
518526

527+
if len(n.snatFixedPorts) > 0 {
528+
for _, proto := range []string{"tcp", "udp", "sctp", "dccp"} {
529+
fixedPortRule := []string{
530+
"!", "-o", "vlan+",
531+
"-p", proto,
532+
"-m", "multiport",
533+
"-m", "comment", "--comment", fmt.Sprintf("AWS, SNAT (fixed ports %s)", proto),
534+
"-m", "addrtype", "!", "--dst-type", "LOCAL",
535+
"--sports", strings.Join(n.snatFixedPorts, ","),
536+
"-j", "SNAT", "--to-source", primaryAddr.String(),
537+
}
538+
539+
iptableRules = append(iptableRules, iptablesRule{
540+
name: "SNAT rule for fixed ports",
541+
shouldExist: !n.useExternalSNAT,
542+
table: "nat",
543+
chain: chain,
544+
rule: fixedPortRule,
545+
})
546+
}
547+
}
548+
519549
// Prepare the Desired Rule for SNAT Rule for non-pod ENIs
520-
snatRule := []string{"!", "-o", "vlan+",
550+
snatRule := []string{
551+
"!", "-o", "vlan+",
521552
"-m", "comment", "--comment", "AWS, SNAT",
522553
"-m", "addrtype", "!", "--dst-type", "LOCAL",
523-
"-j", "SNAT", "--to-source", primaryAddr.String()}
554+
"-j", "SNAT", "--to-source", primaryAddr.String(),
555+
}
556+
524557
if n.typeOfSNAT == randomHashSNAT {
525558
snatRule = append(snatRule, "--random")
526559
}
@@ -852,6 +885,7 @@ func GetConfigForDebug() map[string]interface{} {
852885
envVethPrefix: getVethPrefixName(),
853886
envNodePortSupport: nodePortSupportEnabled(),
854887
envRandomizeSNAT: typeOfSNAT(),
888+
envSNATFixedPorts: parsePortRangeString(envSNATFixedPorts),
855889
}
856890
}
857891

@@ -888,6 +922,57 @@ func (n *linuxNetwork) GetExternalServiceCIDRs() []string {
888922
return parseCIDRString(envExternalServiceCIDRs)
889923
}
890924

925+
// parsePortRangeString parses port ranges from the env Port ranges can be
926+
// comma separated singles ports "80,8080" or ranges "8070-8080". Returns iptables
927+
// compatible format with : for ranges.
928+
func parsePortRangeString(envVar string) []string {
929+
portRangeString := os.Getenv(envVar)
930+
if portRangeString == "" {
931+
return nil
932+
}
933+
934+
var ports []string
935+
for _, p := range strings.Split(portRangeString, ",") {
936+
p = strings.TrimSpace(p)
937+
if strings.Contains(p, "-") {
938+
rangeParts := strings.Split(p, "-")
939+
if len(rangeParts) != 2 {
940+
log.Errorf("%v is not a valid port range", p)
941+
continue
942+
}
943+
944+
start := validatePort(rangeParts[0], p)
945+
if start == -1 {
946+
continue
947+
}
948+
949+
end := validatePort(rangeParts[1], p)
950+
if end == -1 || end < start {
951+
log.Errorf("Invalid end port in range %v", p)
952+
continue
953+
}
954+
955+
ports = append(ports, fmt.Sprintf("%d:%d", start, end))
956+
} else {
957+
port := validatePort(p, p)
958+
if port == -1 {
959+
continue
960+
}
961+
ports = append(ports, strconv.Itoa(port))
962+
}
963+
}
964+
return ports
965+
}
966+
967+
func validatePort(portStr string, originalInput string) int {
968+
port, err := strconv.Atoi(strings.TrimSpace(portStr))
969+
if err != nil || port < 1 || port > 65535 {
970+
log.Errorf("Invalid port in %v", originalInput)
971+
return -1
972+
}
973+
return port
974+
}
975+
891976
func parseCIDRString(envVar string) []string {
892977
cidrString := os.Getenv(envVar)
893978
if cidrString == "" {

0 commit comments

Comments
 (0)