@@ -248,6 +248,64 @@ func verifySubnetStatusAfterEIPOperation(subnetClient *framework.SubnetClient, s
248248 }
249249}
250250
251+ // checkHairpinSnatRuleExists checks if hairpin SNAT rule exists in the NAT gateway pod
252+ // Returns true if rule exists, false otherwise (including when HAIRPIN_SNAT chain doesn't exist)
253+ // This is used with gomega.Eventually for polling-based verification
254+ func checkHairpinSnatRuleExists (natGwPodName , cidr , eip string ) bool {
255+ cmd := []string {"iptables-save" , "-t" , "nat" }
256+ stdout , _ , err := framework .KubectlExec (framework .KubeOvnNamespace , natGwPodName , cmd ... )
257+ if err != nil {
258+ framework .Logf ("Failed to exec iptables-save in NAT gateway pod %s: %v" , natGwPodName , err )
259+ return false
260+ }
261+
262+ iptablesOutput := string (stdout )
263+
264+ // If HAIRPIN_SNAT chain doesn't exist, rule cannot exist
265+ if ! strings .Contains (iptablesOutput , ":HAIRPIN_SNAT" ) && ! strings .Contains (iptablesOutput , "-N HAIRPIN_SNAT" ) {
266+ return false
267+ }
268+
269+ hairpinRulePattern := fmt .Sprintf ("-A HAIRPIN_SNAT -s %s -d %s -j SNAT --to-source %s" , cidr , cidr , eip )
270+ return strings .Contains (iptablesOutput , hairpinRulePattern )
271+ }
272+
273+ // verifyHairpinSnatRule verifies hairpin SNAT rule exists or not in the NAT gateway pod
274+ // Hairpin SNAT enables internal VMs to access other internal VMs via their FIP/EIP
275+ // The rule format: -A HAIRPIN_SNAT -s <cidr> -d <cidr> -j SNAT --to-source <eip>
276+ // This feature was introduced in v1.15, so the function will skip verification
277+ // if HAIRPIN_SNAT chain does not exist (for backward compatibility)
278+ func verifyHairpinSnatRule (natGwPodName , cidr , eip string , shouldExist bool ) {
279+ ginkgo .GinkgoHelper ()
280+
281+ cmd := []string {"iptables-save" , "-t" , "nat" }
282+ stdout , _ , err := framework .KubectlExec (framework .KubeOvnNamespace , natGwPodName , cmd ... )
283+ framework .ExpectNoError (err , "failed to exec iptables-save in NAT gateway pod %s" , natGwPodName )
284+
285+ iptablesOutput := string (stdout )
286+
287+ // Check if HAIRPIN_SNAT chain exists (feature introduced in v1.15)
288+ // Skip verification if the chain doesn't exist for backward compatibility
289+ if ! strings .Contains (iptablesOutput , ":HAIRPIN_SNAT" ) && ! strings .Contains (iptablesOutput , "-N HAIRPIN_SNAT" ) {
290+ framework .Logf ("HAIRPIN_SNAT chain not found, skipping hairpin SNAT verification (feature requires v1.15+)" )
291+ return
292+ }
293+
294+ // Check for hairpin SNAT rule pattern: -A HAIRPIN_SNAT -s <cidr> -d <cidr> -j SNAT --to-source <eip>
295+ hairpinRulePattern := fmt .Sprintf ("-A HAIRPIN_SNAT -s %s -d %s -j SNAT --to-source %s" , cidr , cidr , eip )
296+ ruleExists := strings .Contains (iptablesOutput , hairpinRulePattern )
297+
298+ if shouldExist {
299+ framework .ExpectTrue (ruleExists ,
300+ "Hairpin SNAT rule should exist: %s\n iptables output:\n %s" , hairpinRulePattern , iptablesOutput )
301+ framework .Logf ("Verified hairpin SNAT rule exists: %s" , hairpinRulePattern )
302+ } else {
303+ framework .ExpectFalse (ruleExists ,
304+ "Hairpin SNAT rule should NOT exist: %s\n iptables output:\n %s" , hairpinRulePattern , iptablesOutput )
305+ framework .Logf ("Verified hairpin SNAT rule does not exist for CIDR %s" , cidr )
306+ }
307+ }
308+
251309var _ = framework .OrderedDescribe ("[group:iptables-vpc-nat-gw]" , func () {
252310 f := framework .NewDefaultFramework ("iptables-vpc-nat-gw" )
253311
@@ -456,6 +514,7 @@ var _ = framework.OrderedDescribe("[group:iptables-vpc-nat-gw]", func() {
456514 })
457515
458516 framework .ConformanceIt ("[2] iptables EIP FIP SNAT DNAT" , func () {
517+ f .SkipVersionPriorTo (1 , 15 , "This feature was introduced in v1.15" )
459518 // Test-specific variables
460519 randomSuffix := framework .RandomSuffix ()
461520 fipVipName := "fip-vip-" + randomSuffix
@@ -528,6 +587,12 @@ var _ = framework.OrderedDescribe("[group:iptables-vpc-nat-gw]", func() {
528587 iptablesSnatRuleClient .DeleteSync (snatName )
529588 })
530589
590+ // Verify hairpin SNAT rule is automatically created for internal CIDR
591+ ginkgo .By ("Verifying hairpin SNAT rule exists in NAT gateway pod" )
592+ vpcNatGwPodName := util .GenNatGwPodName (vpcNatGwName )
593+ snatEip = iptablesEIPClient .Get (snatEipName )
594+ verifyHairpinSnatRule (vpcNatGwPodName , overlaySubnetV4Cidr , snatEip .Status .IP , true )
595+
531596 ginkgo .By ("Creating iptables vip for dnat" )
532597 dnatVip := framework .MakeVip (f .Namespace .Name , dnatVipName , overlaySubnetName , "" , "" , "" )
533598 _ = vipClient .CreateSync (dnatVip )
@@ -603,8 +668,13 @@ var _ = framework.OrderedDescribe("[group:iptables-vpc-nat-gw]", func() {
603668 iptablesSnatRuleClient .DeleteSync (sharedEipSnatName )
604669 })
605670
606- ginkgo .By ("Get share eip" )
671+ // Verify hairpin SNAT rule is created for shared SNAT as well
672+ ginkgo .By ("Getting share eip for hairpin verification" )
607673 shareEip = iptablesEIPClient .Get (sharedEipName )
674+ framework .ExpectNotEmpty (shareEip .Status .IP , "shareEip.Status.IP should not be empty for hairpin verification" )
675+ ginkgo .By ("Verifying hairpin SNAT rule exists for shared snat" )
676+ verifyHairpinSnatRule (vpcNatGwPodName , overlaySubnetV4Cidr , shareEip .Status .IP , true )
677+
608678 ginkgo .By ("Get share dnat" )
609679 shareDnat = iptablesDnatRuleClient .Get (sharedEipDnatName )
610680 ginkgo .By ("Get share snat" )
@@ -628,6 +698,16 @@ var _ = framework.OrderedDescribe("[group:iptables-vpc-nat-gw]", func() {
628698 // make sure eip is shared
629699 nats := []string {util .DnatUsingEip , util .FipUsingEip , util .SnatUsingEip }
630700 framework .ExpectEqual (shareEip .Status .Nat , strings .Join (nats , "," ))
701+
702+ // Verify hairpin SNAT rule cleanup when SNAT is deleted
703+ ginkgo .By ("Deleting snat to verify hairpin SNAT rule cleanup" )
704+ iptablesSnatRuleClient .DeleteSync (snatName )
705+ ginkgo .By ("Verifying hairpin SNAT rule is deleted after snat deletion" )
706+ gomega .Eventually (func () bool {
707+ return checkHairpinSnatRuleExists (vpcNatGwPodName , overlaySubnetV4Cidr , snatEip .Status .IP )
708+ }, 30 * time .Second , 2 * time .Second ).Should (gomega .BeFalse (),
709+ "Hairpin SNAT rule should be deleted after SNAT deletion" )
710+
631711 // All cleanup is handled by DeferCleanup above, no need for manual cleanup
632712 })
633713
0 commit comments