This works perfectly.
Can you explain a couple of items so that I understand why yours works and mine did not.
I notice the difference is that you chose connection state new and destination port.
What is this rule saying/doing that mine wasn't?
The key is the dst-port. It's what the "input" chain matches, while in "output", it would've been the "src-port", but THAT rule would be pointless... But in more detail:
Let's say we have two devices - "A" and "B".
When "A" connect to "B" via TCP or UDP, it needs to connect to a particular port on "B". In order for "B" to respond back to "A", "A" (kind of) creates a temporary server on itself, at a random port, where it waits for response from "B", until the connection is over for one reason or another. "A" sends that temporary port to the port on "B" from which it wants answers, and similarly, when "B" answers, it puts the original port to identify itself.
So what's happening is that the in-interface receives the following message
"I'm looking for <server IP>, and I'm <client IP>. I'd like to ask <server port> to answer me on <client port>. Can you do that please?"
and if the server decides to respond, that same interface would send back a packet that would mean
"I'm looking for <client IP>, and I'm <server IP>. I want to tell <client port> that I can indeed answer its requests to <server port>."
And here's the tricky part... when a router ("B") sees the first message (AND <server IP> is its IP address on in-interface), it assigns it on the "input" firewall chain, where the "src-address" is the address of the device that looked for it ("A"), the "src-port" is the source where the device wants answers, and "in-interface" is the interface where the packet came from. The router's IP that the client wants is the "dst-address", and the port that it wants answers from is the "dst-port".
If the packet is accepted, the router proceeds to answering, at which point, the "output" firewall chain is triggered, where the src-* and dst-* stuff are reversed, and the "in-interface" is now actually the "out-interface".
As for the connection state... It doesn't really make a difference here
, but in theory, you could for example allow an initial exchange like above, but then forbid the actual data from being given. If you omit the connection state, the router will not bother answering on any event. With connection-state=new, if the client claims to have already been connected previously, the router will check whether that's true, and answer if so. Since new connections are forbidden, it will fail to see that's true, and thus not answer. So... yeah... it won't answer in any event either way.