You're disabling replies for the local case, preventing a successful communication. Let's check both cases.
From remote (working case):
Query packet
source: 192.168.122.2 port 12345
destination 192.168.122.107 port 8080
The filter/INPUT rule matches: packet accepted
Reply packet
source: 192.168.122.107 port 8080
destination: 192.168.122.2 port 12345
The filter/OUTPUT rule accepts anything: packet accepted
Communication works both ways
From local system to itself (doesn't work, replacing OP's localhost with 192.168.122.107, it's the same):
Reply packet
source: 192.168.122.107 port 8080
destination: 192.168.122.107 port 12345
- emitted first: filter/OUTPUT accepts anything: packet accepted
- packet is looped back through the lo interface
- received back: filter/INPUT doesn't match destination port 12345: packet dropped
Communication doesn't work.
You need a stateful firewall to always accept established flows whenever the default policy is to drop them, because the randomly chosen source port, which becomes the destination in the reply, can't be guessed in advance.
On Linux's Netfilter framework, this is handled by conntrack and the iptables match modules that will query it. Here this can be done with iptables' conntrack
match module:
iptables -I INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
With this any packet from a flow tracked as being already established is automatically accepted. For OP's loopback case this will allow the server to reply to itself successfully without knowing in advance the dynamically chosen source port of the initial query. The RELATED
part also allows to accepted related errors, for example ICMP Destination unreachable (even for TCP it's useful for example with Path MTU Discovery).
More simply, most setups, including those with stateful firewall rules anyway accept local communication. You could also use instead (if for some reason you don't want to activate the conntrack subsystem), or in addition:
iptables -A INPUT -i lo -j ACCEPT