2

The system has two interfaces 1.1.1.1 and 1.1.2.1. The sysctl net.ipv4.ip_forward is either disabled or enabled but iptables -P FORWARD DROP is set.

Now setting 1.1.1.1 as default route on another 1.1.1.2 computer, I am able to ping and connect to 1.1.2.1 interface ip. How to prevent this easily and in a most elegant way?

The solution

I am now using this magic one liner, thanks to @A.B:

nft add rule inet filter input fib daddr . iif type != { local, broadcast, multicast } drop

Explanation:

nft stands for nftables, a supposed successor to iptables

input is a chain from table inet filter, this table is created by default (you can query it with nft list table inet filter command) but if you have a custom nft setup you should change it accordingly. inet type classifier means it will work for both ip4 and ip6 protocols.

fib daddr . iif type is a query to Forwarding Information Base, you basically ask what type does destination address daddr have on the input interface iif

If the address is not local nor broadcast nor multicast to the interface packet came from then drop it.

@A.B also made some points how to block (or how not to block) packets from following DNAT rules, check his answer for details.

  • It seams to be a different situation, I have already handled this weak gateway discover algorithm through multiple routing tables and ip rule and still have problems with incoming connections. One of the answer there is partially covering my problem though. – ZAB Jun 11 at 22:56
1

Since 1.1.2.1 is an other IP address belonging to the same system, there is no forwarding involved. The system received a packet and handled it for itself, as an incoming packet for local delivery rather than a packet to be routed to a 3rd party. This goes along with the general idea that on Linux IPs should be considered as part of a pool rather than belonging to an interface.

To prevent such packet to be accepted using iptables, you have to filter it in the INPUT chain rather than the FORWARD chain. If the two networks involved are /24, this would be for example:

iptables -I INPUT -s 1.1.1.0/24 -d 1.1.2.0/24 -j DROP

As this would also prevent the system to connect to itself in some cases (because this example doesn't use interface names in the rule), it should probably at least have this rule prepended to it:

iptables -I INPUT -i lo -j ACCEPT

Stating the interface name is sometimes helpful to organize rules and for security in some corner cases (not as often as thought, considering reverse path filtering is activated by default on most distributions).

UPDATE: as mentioned by OP, of course this example rule can be better written by using -s 1.1.1.0/24 -d ! 1.1.1.0/24 to only have one rule per LAN, rather than LAN^2 rules if all LANs should be "isolated" likewise.


UPDATE: as further requested, here's a much more elegant method using nftables with a fib (forwarding information base) expression (even documented as example in the man page): drop a destination address that is not local (nor broadcast nor multicast) to the interface where it was received. Since this expression doesn't require to state explicitly any IP (nor any interface), it can even work both for IPv4 and IPv6 at the same time, by using an inet address family table rather than an ip address family table.

# nft add table inet filter
# nft add chain inet filter prerouting '{ type filter hook prerouting priority -150; policy accept; }
# nft add rule inet filter prerouting fib daddr . iif type != { local, broadcast, multicast } counter drop

This alone will handle any number of LANs/interfaces (as long as you don't play and put multiple LANs on the same interface, which would probably defeat the rule above between those multiple LANs). counter is optional, just to see hits using for example nft list ruleset.

However, this rule, as is, allows DNAT to an other local IP defined on an other interface since it happens before any DNAT (with iptables it's registered at priority NF_IP_PRI_NAT_DST (=-100)) but at the same time prevents routing/forwarding (if OP chose to enable it back) or forwarding DNAT: either they are dropped because not local (for normal forwarding), or the return packets will hit this rule before "reverse DNAT" happens and will be dropped . If added in the hook input chain instead, the reverse will happen: no local DNAT to an other IP allowed but routed/forwarded DNAT works because it goes through the forward hook rather than input. This would be more useful, and for other rare cases, exceptions can be added. So in the end this should be used instead of above:

# nft add table inet filter
# nft add chain inet filter input '{ type filter hook input priority 0; policy accept; }
# nft add rule inet filter input fib daddr . iif type != { local, broadcast, multicast } counter drop

Also iptables and nftables can coexist peacefully (with the exception of nat where only one should attempt to register, not both). So these few nftables lines can play along any existing iptables rules.

The rule above can be written in script format (mostly output of nft list ruleset) instead for initialization at boot, like:

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy accept;
        fib daddr . iif type != { local, broadcast, multicast } counter drop
    }
}

To be nitpicking, the only thing to check is when using the newer iptables over nftables API, which is for example default on Debian buster, flush ruleset would also flush iptables rules since they're actually stored using nftables, and since for now you can't instead flush only a non-existing chain "in advance" without an error. So this script should be run before any iptables script if this is the case.


Also note that a client in the 1.1.1.0/24 network while not able to connect to 1.1.2.1 is still able to infer its existance using ARP. Something like:

arping -I eth0 1.1.2.1

would get replies from 1.1.2.1 with the system's interface on the 1.1.1.0/24 network, even if 1.1.2.1 "belongs" to an other interface in an other network, with a different MAC address.

That's because, again, Linux by default, for better or for worse, will answer ARP requests received on any of its interfaces for any of its IPs. Details described this time there: arp_filter.

To avoid this, either mingle with arp_filter + ip rule or arp_announce + arp_ignore, or use arptables (or nftables with arp family) to filter ARP requests (might require one of the previous settings anyway).

  • I see a better way would be to use ! -d option and automatically issue commands of this kind for each interface ip iptables -I INPUT -s 1.1.1.0/24 ! -d 1.1.1.0/24 -j DROP. Yet it is still ugly as hell – ZAB Jun 11 at 23:03
  • For more than 2 interfaces the number of rules growth quadratically without the ! option. Even better would be to somehow issue one line command that test identity of source and destination interfaces without the need of particular names and ip's. – ZAB Jun 11 at 23:14
  • I've tested this cool solution and it works, but it should be tied to input hook to let DNAT pass and putting it in prerouting with priority -150 blocks DNAT instead – ZAB Jun 15 at 12:46
  • mhh ok I got it: prerouting would allow dnat to an other local IP configured on an other interface, input would allow dnat to a routed destination (since the packet goes through the forward chain and never hit the filter). It seems both can't work at the same time (without some specific exclusion rule). So yes input seems more useful. Will edit answer. – A.B Jun 15 at 14:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.