Each chain hooks into Netfilter: as long as a packet exists, Netfilter will call all chains hooking in the current phase (eg: input).
What this means is that each chain provides a chance to get the packet dropped. Once the packet is dropped, it doesn't exist anymore: remaining chains won't be called for traversal since there's nothing to be used for traversal: the packet already disappeared.
So whatever the priority order of the two chains, when each chain has its own rule to accept or drop a packet, independently from the other chain, the final outcome is a logical AND between each outcome, where drop means false and accept means true:
drop AND drop = drop
drop AND accept = drop
accept AND drop = drop
accept AND accept = accept
This is reminded in nft(8)
in the VERDICT STATEMENT
section:
accept
Terminate ruleset evaluation and accept the packet. The packet can
still be dropped later by another hook, for instance accept in the
forward hook still allows one to drop the packet later in the
postrouting hook, or another forward base chain that has a higher
priority number and is evaluated afterwards in the processing
pipeline.
drop
Terminate ruleset evaluation and drop the packet. The drop occurs
instantly, no further chains or hooks are evaluated. It is not
possible to accept the packet in a later chain again, as those are not
evaluated anymore for the packet.
If you want to avoid this, then the evaluation must not be independent. For example you can pass a message between the first chain and the 2nd chain (in order of hook and hook's priority) using a firewall mark to be interpreted as "decision already made, accept this packet". This requires cooperation from both sides, so can't be slapped on an unaware tool (eg: can't add this to a ruleset handled by firewalld without modifying firewalld to account for this, either as the message creator or as the message consumer.
For your case, you could replace your ruleset with a variation of this:
table inet filter {
chain markandaccept {
meta mark set 0x900d accept
}
chain input {
type filter hook input priority filter + 1; policy accept;
meta mark 0x900d accept
iif "lo" accept
ct state established,related accept
tcp dport 299 ip saddr 3x.xx.xx.xx accept
icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, echo-reply, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, 148, 149 } accept
ip6 saddr fe80::/10 icmpv6 type { mld-listener-query, mld-listener-report, mld-listener-done, mld2-listener-report, 151, 152, 153 } accept
counter drop
}
chain dyn {
type filter hook input priority filter; policy accept;
iif "lo" goto markandaccept
ct state established,related goto markandaccept
ip saddr 2x.xx.xx.xx udp dport 8999 log prefix "dyn" goto markandaccept
ip6 saddr xxx:xxxx:xxxx:xxxx::9999 udp dport 8999 log prefix "dyn" goto markandaccept
ip saddr 2x.xx.xx.xx tcp dport 7999 log prefix "dyn" goto markandaccept
ip6 saddr xxx:xxxx:xxxx:xxxx::9999 tcp dport 7999 log prefix "dyn" goto markandaccept
ip saddr 2x.xx.xx.xx icmp type echo-request log prefix "dyn" goto markandaccept
ip6 saddr xxx:xxxx:xxxx:xxxx::9999 icmp type echo-request log prefix "dyn"
ip saddr 2x.xx.xx.xx tcp dport 6999 log prefix "dyn" goto markandaccept
ip6 saddr xxx:xxxx:xxxx:xxxx::aaaa tcp dport 6999 log prefix "dyn" goto markandaccept
}
}
This is an example made without any simplification, I'm sure better can be done.
So whenever the dyn chain deems a packet should stay accepted, the packet is tagged (internally, in the current network stack only) with a mark. The first test in the altered input chain is to check for this mark: if present, the packet is immediately accepted without further processing: the decision made in the first chain is honored in the 2nd chain.