There is a known problem when combining stateful rules and NAT: a dynamic rule will store the IP address, but NAT does change that IP address, inversely for outgoing and incoming packets.
So if we use a stateful rule to allow outgoing packets after NAT, that same rule cannot allow incoming packets after NAT, because that same address would then be present only before NAT - and it doesn't make sense to allow the packet before NAT.
Background:
A stateful rule will create a dynamic rule for the specific IP addresses and ports where it did match. When that dynamic rule is later matched, it will repeat the same action as the original stateful rule, AND if that action is not allow or forward, it will jump in the rule list to the place after the original rule and continue processing there.
(There is a bunch of possible rule actions and modifiers which let you decide if at some position in the rule list you want to create dynamic rules, check the dynamic rules, or do both, with or without actually executing the rule's action immediately.)
So, in order to have stateful rules with NAT, we first need to decide if we want to apply the rules to the addresses as present before or after NAT. (It is also possible to do both, by assigning each a different flowname.)
Then, the action for the stateful rule cannot simply be "allow", because that cannot work - on either incoming or outgoing packets.
There are various tricks published on the net about how one can solve this, either by using stateful rules that do only record but not execute an action, or by skipping around in the rule list, or otherwise.
The method I prefer is to use tag in a subroutine, because it is the most versatile and expandable method.
Background:
The action of a rule can be to simply "tag" a packet with a smallint number. The packet will then continue to carry that number while processing the rule list until it gets untagged again, And this tag can be tested for in other rules.
The action of a rule can be to "call" a subroutine. Processing will then jump to the given rule number and continue processing there, until a rule with a "return" action matches. Then it will jump back and continue processing after the "call" rule.
We can define our stateful rules before or after NAT as "tag" rules, and the action that is stored in the created dynamic rules will then be to only tag the packets. In a following non-stateful rule we can then test for the tag and execute any action we like.
An example snippet for NAT and allowing outgoing ssh might then look as such (BUT THIS IS STILL WRONG AND DOES NOT REALLY WORK):
10 skipto 1000 in // separate in- and outgoing packets
110 tag 42 dst-port 22 proto tcp setup keep-state
120 skipto 500 tagged 42 proto all // out: check first, nat later
130 drop proto all
500 nat 1 proto ip4
510 allow proto all
1000 nat 1 proto ip4 // in: nat first, check later
1010 check-state
1020 allow proto all tagged 42
1030 drop proto allSo why does it not work? Our stateful rule 110 does now only tag the matched packets, and the decision on what to do with them is separated to the (non-stateful) rule 120 - in this case it jumps onwards to the NAT and gets subsequently allowed.
Then for the responses coming back in, we jump to rule 1000 and do NAT rightaway, and check-state afterwards. The problem then is: when check-state matches with the dynamic rule, it executes that original action (which is to tag), but then jumps to the position after the original rule 110. Rule 1020 (which is supposed to allow the packet) is not reached!
What happens actually: the incoming response packet gets to rule 120, jumps to NAT in 500, and is then allowed. Apparently everything works as desired - but not as intended!
You may not even notice the problem, until far later when you might decide to change rule 1020 into a "forward" action, and then wonder why that forward doesn't work - or maybe change it into a divert to suricata for protection, and then probably not even notice that your suricata is useless because unreached. (That is why I consider it so important to use tested and repeatable design patterns.)
To fix this, we can put our stateful rules into subroutines:
10 skipto 1000 in // separate in- and outgoing packets
110 call 140 proto all
120 skipto 500 tagged 42 proto all // out: check first, nat later
130 skipto 160 proto all // skip over the subroutine
140 tag 42 dst-port 22 proto tcp setup keep-state
150 return
490 drop proto all // end of processing
500 untag 42 proto all // always clean up afterwards for expandability
510 nat 1 proto ip4
520 allow proto all
1500 nat 1 proto ip4 // in: nat first, check later
1510 call 1540 proto all
1520 allow proto all tagged 42
1530 skipto 1560 proto all // skip over the subroutine
1540 check-state
1550 return
1990 drop proto all // end of processingNow in rule 110, instead of tagging rightaway, we jump to a subroutine at 140, which consists of a single line that does the tagging. Obviousely our main processing now needs to skip over this subroutine, at 130.
Then for the responses we do again jump from 1510 to a subroutine at 1540 which contains only the check-state. On no-match we then return via 1550. On match we again get behind rule 140 - but now there is also a return, so we return nevertheless and continue always at 1520!
Certainly for this simple task of only allowing ssh with NAT, a much shorter ruleset with fewer rules could be created. But this scheme here is expandable in an easy way: you can continue at rule 160 and 1060 with another flow in the very same style. You can also add additional filtering on the other side of NAT (outgoing nat first, check later) at rule 520 and (incoming check first, nat later) rule 1000.
There are 32 sets of rules available. Normally when defining a rule, it goes into set 0, but we can explicitely state a different set, except set 31 which is immutable and contains the default rule.
Normally all sets are active, but we can disable and enable sets individually at any time. This allows to put certain rules into a specific set, and then enable or disable that set on demand (or by software), thereby changing behaviour in an instant and without loading rules.
Also, multiple sets can be enabled and disabled with a single command so that all the behaviour changes at once. (Imagine that you have one set where a certain flow is allowed, and another where it is forwarded elsewhere - then you will want to enable one set and disable the other at the same time.)
Normally when we create a complete new ruleset (and not just change some individual rule), we need to flush and reload the rules. This implies a service interruption (or worse, we might be thrown out of our remote system and not easily get in again).
To avoid this, we can use sets: we load the new rules into another set which is inactive, and then switch sets from the old to tne new. (We could even create a failsafe that switches back to the old set after some time in case we have accidentially boggled our access.)
There is however a delicate problem with dynamic rules (rules that have been created stateful for an ongoing connection). These dynamic rules will only exist for as long as their original rule exists (it does not matter if it is in a disabled set).
So at the first reload these dynamic rules will continue to work, but when we need to reload again (and therefore delete the old and now inactive rules first), they will be removed.
We can solve this by declaring one set as our "waste bin" and move all the old rules into that set instead of deleting them. Obviousely the total number of inactive rules will then grow with subsequent reloads - but the system can hold quite a lot of them, and that number is tunable, and at some time a reboot will be needed for some reason anyway.
The remaining problem is then what these dynamic rules do when they are not simply allow or forward rules: they jump to the rule number after the original stateful rule and continue processing there. After a redesign of the entire rule list, there is no easy way knowing what will be there, and it is most likely not what is desired.
Now comes the advantage of writing stateful rules -other than allow and forward- by tagging in a subroutine (as described in the previous section): the thing we need to find at the respective rule number then is always and only a "return" action.
And we can achieve this, simply by moving our keep-state subroutines into a separate area of the rule list. E.g. by starting them from the top rule number downwards:
10 skipto 1000 in // separate in- and outgoing packets
110 call 65524 proto all
120 skipto 500 tagged 42 proto all // out: check first, nat later
130 <some other flow following next>
...
490 drop proto all // end of processing
500 untag 42 proto all // always clean up afterwards for expandability
510 nat 1 proto ip4
520 allow proto all
1500 nat 1 proto ip4 // in: nat first, check later
1510 call 1540 proto all
1520 allow proto all tagged 42
1530 skipto 1560 proto all // skip over the subroutine
1540 check-state
1550 return
1560 <some other flow>
...
1990 drop proto all // end of processing
65494 return // put a return in front for removed flows
65504 tag 42 <some other flow> keep-state
65514 return
65524 tag 42 dst-port 22 proto tcp setup keep-state
65534 returnNow our check-state have their respective keep-state rules in the area above 65000, so that they will continue processing there and expect a return statement. And they will always find that return statement - it doesn't matter which rule of the new rule list has placed it there. This return will then jump back to the place in the new ruleset where we have invoked a check-state, and we are back in control.
It is advisable to add a distinct flowname to all the keep-state and check-state, so that the match into the dynamic rules is done from the desired check-state handling the flow, and we get back into control at the correct place where we do handle this specific flow.
The action, what we do with this flow, can then also be changed dynamically, because the statefulness is only concerned with tagging.