I am doing outbound filtering on some interfaces. And I seem to have struck a similar issue to this person (https://forum.opnsense.org/index.php?topic=41311.msg202609#msg202609).
Eg. I have this rule on an interface:
root@opnsense-test:~ # pfctl -vvs rules | grep -A 3 "drop out log quick on ixl0"
@195 block drop out log quick on ixl0 reply-to (ixl0 192.0.2.1) inet all label "83448bc8-ca97-401d-86f2-57d67fb16fb2"
[ Evaluations: 211 Packets: 211 Bytes: 22618 States: 0 ]
[ Inserted: uid 0 pid 90386 State Creations: 0 ]
[ Last Active Time: Wed Aug 26 05:53:29 2026 ]
I can allow traffic out of that interface with the following rule:
root@opnsense-test:~ # pfctl -vvs rules | grep -A 3 5443b2cb-c3ca-4e0c-81cf-88c04c85f00f
@120 pass out log quick inet from (self:10) to any flags S/SA keep state label "5443b2cb-c3ca-4e0c-81cf-88c04c85f00f"
[ Evaluations: 11 Packets: 18 Bytes: 1946 States: 4 ]
[ Inserted: uid 0 pid 24 State Creations: 11 ]
[ Last Active Time: Wed Aug 26 05:45:02 2026 ]
And then I can establish a TCP connection:
root@opnsense-test:~ # telnet 192.0.2.49 8443
Trying 192.0.2.49...
Connected to 192.0.2.49.
Escape character is '^]'.
^]
telnet> Connection closed.
And I can see the states in PF:
root@opnsense-test:~ # pfctl -vvs states | grep -A3 192.0.2.49
all tcp 192.0.2.47:45666 -> 192.0.2.49:8443 ESTABLISHED:ESTABLISHED
[3811272169 + 16711936] wscale 7 [181588979 + 65792] wscale 7
age 00:00:02, expires in 23:59:58, 2:1 pkts, 112:60 bytes, rule 120
id: bddd8c6a00000000 creatorid: 3610e121
However, if I replace the above floating rule with one that matches the interface:
@148 pass out quick on ixl0 reply-to (ixl0 192.0.2.1) inet all flags S/SA keep state label "ed13d8d2-e403-4cf7-ba41-6b698f76643e"
[ Evaluations: 36 Packets: 33 Bytes: 3751 States: 22 ]
[ Inserted: uid 0 pid 70434 State Creations: 22 ]
[ Last Active Time: Wed Aug 26 06:14:36 2026
I get responses, but the local firewall never completes the TCP handshake:
root@opnsense-test:~ # telnet 192.0.2.49 8443
Trying 192.0.2.49...
^C
root@opnsense-test:~ # pfctl -vvs states | grep -A 3 192.0.2.49
all tcp 192.0.2.47:59825 -> 192.0.2.49:8443 ESTABLISHED:SYN_SENT
[3105104415 + 4294902016] wscale 7 [4173125645 + 4294902016] wscale 7
age 00:00:10, expires in 00:00:28, 2:5 pkts, 120:300 bytes, rule 148
id: 07de8c6a00000000 creatorid: 3610e121 reply-to: 192.0.2.1@ixl0
root@opnsense-test:~ # tcpdump -nevi ixl0 host 192.0.2.49
tcpdump: listening on ixl0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
06:17:38.604709 38:68:dd:66:ac:e8 > 38:68:dd:66:7d:b0, ethertype IPv4 (0x0800), length 74: (tos 0x10, ttl
64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.0.2.47.50028 > 192.0.2.49.8443: Flags [S], cksum 0xff66 (correct), seq 4278843326, win 652
28, options [mss 1460,nop,wscale 7,sackOK,TS val 2155498607 ecr 0], length 0
06:17:38.604815 38:68:dd:66:7d:b0 > 38:68:dd:66:ac:e8, ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 6
4, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.0.2.49.8443 > 192.0.2.47.50028: Flags [S.], cksum 0x5bb6 (correct), seq 1043354308, ack 42
78843327, win 65228, options [mss 1460,nop,wscale 7,sackOK,TS val 2956288629 ecr 2155498607], length 0
06:17:39.628574 38:68:dd:66:ac:e8 > 38:68:dd:66:7d:b0, ethertype IPv4 (0x0800), length 74: (tos 0x10, ttl
64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.0.2.47.50028 > 192.0.2.49.8443: Flags [S], cksum 0xfb66 (correct), seq 4278843326, win 652
28, options [mss 1460,nop,wscale 7,sackOK,TS val 2155499631 ecr 0], length 0
06:17:39.628651 38:68:dd:66:7d:b0 > 38:68:dd:66:ac:e8, ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 6
4, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.0.2.49.8443 > 192.0.2.47.50028: Flags [S.], cksum 0x57b6 (correct), seq 1043354308, ack 4$
78843327, win 65228, options [mss 1460,nop,wscale 7,sackOK,TS val 2956289653 ecr 2155498607], length 0
06:17:39.628762 38:68:dd:66:7d:b0 > 38:68:dd:66:ac:e8, ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 6
4, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.0.2.49.8443 > 192.0.2.47.50028: Flags [S.], cksum 0x53b6 (correct), seq 1043354308, ack 42
78843327, win 65228, options [mss 1460,nop,wscale 7,sackOK,TS val 2956289653 ecr 2155499631], length 0
06:17:40.690948 38:68:dd:66:7d:b0 > 38:68:dd:66:ac:e8, ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 6
4, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.0.2.49.8443 > 192.0.2.47.50028: Flags [S.], cksum 0x4f90 (correct), seq 1043354308, ack 42
78843327, win 65228, options [mss 1460,nop,wscale 7,sackOK,TS val 2956290715 ecr 2155499631], length 0
This is even worse when attempting to connect to something not on the same subnet as my device, where I never even see the Syn packet:
[Edit] I successfully recreated this now.
root@opnsense-test:~ # telnet 198.51.100.1 8443
Trying 198.51.100.1...
^C
root@opnsense-test:~ # pfctl -vvs states | grep -A 3 198.51
No ALTQ support in kernel
ALTQ related functions disabled
all tcp 192.0.2.47:18469 -> 198.51.100.1:8443 ESTABLISHED:SYN_SENT
[1406893479 + 4294705408] wscale 7 [2561759781 + 4294902016] wscale 7
age 00:01:11, expires in 00:00:23, 7:12 pkts, 420:720 bytes, rule 146
id: 8de98c6a00000000 creatorid: 3610e121 reply-to: 192.0.2.1@ixl0
root@opnsense-test:~ # tcpdump -nevi ixl0 tcp port 8443
tcpdump: listening on ixl0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
21:55:52.902296 38:68:dd:66:ac:e8 > 00:00:5e:00:01:01, ethertype IPv4 (0x0800), length 74: (tos 0x10, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.0.2.47.61030 > 198.51.100.1.8443: Flags [S], cksum 0xef3a (correct), seq 2484399761, win 65228, options [mss 1460,nop,wscale 7,sackOK,TS val 416987146 ecr 0], length 0
21:55:52.912149 e2:39:b3:4e:bc:47 > 38:68:dd:66:ac:e8, ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 59, id 0, offset 0, flags [DF], proto TCP (6), length 60)
198.51.100.1.8443 > 192.0.2.47.61030: Flags [S.], cksum 0xd0d9 (correct), seq 3967036082, ack 2484399762, win 64492, options [mss 1414,sackOK,TS val 3376431350 ecr 416987146,nop,wscale 7], length 0
21:55:53.902416 38:68:dd:66:ac:e8 > 00:00:5e:00:01:01, ethertype IPv4 (0x0800), length 74: (tos 0x10, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.0.2.47.61030 > 198.51.100.1.8443: Flags [S], cksum 0xeb52 (correct), seq 2484399761, win 65228, options [mss 1460,nop,wscale 7,sackOK,TS val 416988146 ecr 0], length 0
21:55:53.912051 e2:39:b3:4e:bc:47 > 38:68:dd:66:ac:e8, ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 59, id 0, offset 0, flags [DF], proto TCP (6), length 60)
198.51.100.1.8443 > 192.0.2.47.61030: Flags [S.], cksum 0xccf1 (correct), seq 3967036082, ack 2484399762, win 64492, options [mss 1414,sackOK,TS val 3376432350 ecr 416987146,nop,wscale 7], length 0
21:55:54.940966 e2:39:b3:4e:bc:47 > 38:68:dd:66:ac:e8, ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 59, id 0, offset 0, flags [DF], proto TCP (6), length 60)
198.51.100.1.8443 > 192.0.2.47.61030: Flags [S.], cksum 0xc8ec (correct), seq 3967036082, ack 2484399762, win 64492, options [mss 1414,sackOK,TS val 3376433379 ecr 416987146,nop,wscale 7], length 0
^C Though, I have sometimes seen the same behaviour as with local addresses (Syns and Syn-Acks but nothing more), I just haven't been able to recreate it for the sake of this post.
The issue appears to be that any rule on an interface with a gateway configured (inbound or outbound) will have a reply-to value created automatically by OPNsense. You can disable this with the Disable reply-to check-box in the Edit-rule pop-up, which fixes the problem.
But from my reading of man PF:
reply-to
The reply-to option is similar to route-to, but routes packets that
pass in the opposite direction (replies) to the specified interface.
Opposite direction is only defined in the context of a state entry,
and reply-to is useful only in rules that create state. It can be
used on systems with multiple external connections to route all out-
going packets of a connection through the interface the incoming con-
nection arrived through (symmetric routing enforcement).It seems to me that the reply-to field on an outbound rule doesn't make sense. Shouldn't replies always be coming back to the firewall itself for an outbound rule? Or am I misunderstanding the purpose of the reply-to field here?
For reference:
Type opnsense-business
Version 26.4.1p2
Architecture amd64