rpthms / nft-geo-filter

Allow/deny traffic in nftables using country specific IP blocks
MIT License
97 stars 24 forks source link

Allow outgoing traffic to blocked IP ranges #5

Closed peeterm007 closed 4 years ago

peeterm007 commented 4 years ago

Hello

Thank you for your work on this application. I have a question concerning allowing outgoing traffic. Taking MC as an example, suppose I allow incoming traffic only from within MC

nft-geo-filter --allow MC

this entails that no incoming or outgoing traffic is allowed to non-MC addresses. But suppose I would still like to allow outgoing traffic to the rest of the world (ie packets originating from my machine). How can I achieve this? I tried adding

ct state established,related accept

to the chain 'filter-chain' (assuming default names in this example), but it did not make a difference.

Is there a way to make this work? It would be great to have an option for this in the program.

Thank you

rpthms commented 4 years ago

Yeah, with the way the script currently works it will block all packets coming from non-MC IP addresses, whether they are new connections or part of an established connection. Adding ct state established,related accept to the top of the filter chain should work though. Just make sure you're not using the netdev table, since the ct rules will not work in it. (conntrack only starts working in the prerouting hooks).

So, run nft-geo-filter --allow MC and get the following table:

table inet geo-filter {
        set filter-v4 {
                type ipv4_addr
                flags interval
                auto-merge
                elements = { 37.44.224.0/22, 80.94.96.0/20,
                             82.113.0.0/19, 87.238.104.0/21,
                             87.254.224.0/19, 88.209.64.0/18,
                             91.199.109.0/24, 176.114.96.0/20,
                             185.47.116.0/22, 185.162.120.0/22,
                             185.250.4.0/22, 188.191.136.0/21,
                             194.9.12.0/23, 195.20.192.0/23,
                             195.78.0.0/19, 213.133.72.0/21,
                             213.137.128.0/19 }
        }

        set filter-v6 {
                type ipv6_addr
                flags interval
                auto-merge
                elements = { 2a01:8fe0::/32,
                             2a07:9080::/29,
                             2a0b:8000::/29 }
        }

        chain filter-chain {
                type filter hook prerouting priority -200; policy drop;
                ip6 saddr { ::1, fe80::/10 } accept
                ip saddr { 10.0.0.0/8, 127.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } accept
                ip saddr @filter-v4 accept
                ip6 saddr @filter-v6 accept
        }
}

and then add the ct rule to the top of the chain by running: nft insert rule inet geo-filter filter-chain ct state established,related accept. Which results in:

table inet geo-filter {
        set filter-v4 {
                type ipv4_addr
                flags interval
                auto-merge
                elements = { 37.44.224.0/22, 80.94.96.0/20,
                             82.113.0.0/19, 87.238.104.0/21,
                             87.254.224.0/19, 88.209.64.0/18,
                             91.199.109.0/24, 176.114.96.0/20,
                             185.47.116.0/22, 185.162.120.0/22,
                             185.250.4.0/22, 188.191.136.0/21,
                             194.9.12.0/23, 195.20.192.0/23,
                             195.78.0.0/19, 213.133.72.0/21,
                             213.137.128.0/19 }
        }

        set filter-v6 {
                type ipv6_addr
                flags interval
                auto-merge
                elements = { 2a01:8fe0::/32,
                             2a07:9080::/29,
                             2a0b:8000::/29 }
        }

        chain filter-chain {
                type filter hook prerouting priority -200; policy drop;
                ct state established,related accept
                ip6 saddr { ::1, fe80::/10 } accept
                ip saddr { 10.0.0.0/8, 127.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } accept
                ip saddr @filter-v4 accept
                ip6 saddr @filter-v6 accept
        }
}
rpthms commented 4 years ago

I guess adding a flag for allowing established connections might be useful for some people. Ok, I'll let you know when I update the script with the new flag.

peeterm007 commented 4 years ago

Hmm, this does not work. I'm using an inet table and I get exactly the filter chain you describe with

ct state established,related accept

inserted but even a simple ping to a non-MC address

% ping -v www.google.com

does not work whereas ping to an MC address works fine.

I now notice the filter-chain is with hook prerouting. Maybe there's a way to do this by splitting the filter chain into an input and output chain?

rpthms commented 4 years ago

That's really weird. Adding the ct rule at the top of the filter chain definitely works for me.

With this ruleset:

table inet geo-filter {
        set filter-v4 {
                type ipv4_addr
                flags interval
                auto-merge
                elements = { 37.44.224.0/22, 80.94.96.0/20,
                             82.113.0.0/19, 87.238.104.0/21,
                             87.254.224.0/19, 88.209.64.0/18,
                             91.199.109.0/24, 176.114.96.0/20,
                             185.47.116.0/22, 185.162.120.0/22,
                             185.250.4.0/22, 188.191.136.0/21,
                             194.9.12.0/23, 195.20.192.0/23,
                             195.78.0.0/19, 213.133.72.0/21,
                             213.137.128.0/19 }
        }

        set filter-v6 {
                type ipv6_addr
                flags interval
                auto-merge
                elements = { 2a01:8fe0::/32,
                             2a07:9080::/29,
                             2a0b:8000::/29 }
        }

        chain filter-chain {
                type filter hook prerouting priority -200; policy drop;
                ct state established,related accept
                ip6 saddr { ::1, fe80::/10 } accept
                ip saddr { 10.0.0.0/8, 127.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } accept
                ip saddr @filter-v4 accept
                ip6 saddr @filter-v6 accept
        }
}

I am able to ping IP addresses outside the filter set:

$ ping 1.1.1.1 -c 4
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=59 time=7.74 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=59 time=7.38 ms
64 bytes from 1.1.1.1: icmp_seq=3 ttl=59 time=7.15 ms
64 bytes from 1.1.1.1: icmp_seq=4 ttl=59 time=14.8 ms

--- 1.1.1.1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3005ms
rtt min/avg/max/mdev = 7.149/9.256/14.753/3.180 ms

$ ping 8.8.8.8 -c 4
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=118 time=18.0 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=118 time=11.4 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=118 time=13.7 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=118 time=8.93 ms

--- 8.8.8.8 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 8.934/12.989/17.978/3.330 ms

I'm also able to ping Google:

$ ping google.com -c 4
PING google.com (172.217.167.46) 56(84) bytes of data.
64 bytes from del03s16-in-f14.1e100.net (172.217.167.46): icmp_seq=1 ttl=118 time=9.21 ms
64 bytes from del03s16-in-f14.1e100.net (172.217.167.46): icmp_seq=2 ttl=118 time=14.5 ms
64 bytes from del03s16-in-f14.1e100.net (172.217.167.46): icmp_seq=3 ttl=118 time=11.4 ms
64 bytes from del03s16-in-f14.1e100.net (172.217.167.46): icmp_seq=4 ttl=118 time=13.1 ms

--- google.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 9.207/12.058/14.503/1.980 ms

And I'm posting this comment on GitHub right now with the above ruleset!

Do you have any other rules outside the geo-filter table that could be blocking packets? Have you tried flushing the ruleset and just running the nft-geo-filter script, so that other rules don't have any affect?

peeterm007 commented 4 years ago

You are right, the main table somehow interferes. I have a fairly minimalistic setup, followed arch wiki to get this:

table inet my_table {
    chain my_input {
        type filter hook input priority 0; policy drop;
        iif "lo" accept comment "Accept any localhost traffic"
        ct state invalid drop comment "Drop invalid connections"
        ct state established,related accept comment "Accept traffic originated from us"
        icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, mld-listener-query, mld-listener-report, mld-listener-done, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert, mld2-listener-report } accept comment "Accept ICMPv6"
        icmp type { destination-unreachable, router-advertisement, router-solicitation, time-exceeded, parameter-problem } accept comment "Accept ICMP"
        tcp dport 22 accept comment "Accept SSH on port 22"
        tcp dport { 80, 443 } accept comment "Accept HTTP (ports 80, 443)"
    }

    chain my_forward {
        type filter hook forward priority 0; policy drop;
    }

    chain my_output {
        type filter hook output priority 0; policy accept;
    }
}

When I delete the inet my_table, leaving only geo-filter, then ping 8.8.8.8 works fine. I wonder if there's a way to make my_table work with the geo-filter added on top?

On second thoughts, maybe I need to integrate the geo-filter into the main table. That would be fine for me.

Thanks

rpthms commented 4 years ago

Hmm, interesting. I tried running adding your rules on my system and run nft-geo-filter after that (along with adding the ct rule on the top of the filter-chain) and I was also not able to ping any of the IPs outside the filter set. I'm not exactly sure why this is happening. I will have to dig into it a bit more.

b0rZ84 commented 4 years ago

Hmm, interesting. I tried running adding your rules on my system and run nft-geo-filter after that (along with adding the ct rule on the top of the filter-chain) and I was also not able to ping any of the IPs outside the filter set. I'm not exactly sure why this is happening. I will have to dig into it a bit more.

I have the same issue as @peeterm007 minimal ip filter table before geo-filter, inserted established,related as adviced but still no incoming packets when initiating connection. By the way, your work is awesome. Looking forward for updated version.

rpthms commented 4 years ago

@b0rZ84 @peeterm007 I'm sorry for the lack of response on this issue. I've just been a bit busy the past few weeks. I still don't have an answer to this problem. I'll try to send a mail to the netfilter guys and see what they have to say about this. Will post an update if I find something worthwhile.

rpthms commented 4 years ago

Ok, figured it out. The reason the outgoing connections were not working was because connection state tracking operations happen using the priority -200 (https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains#Base_chain_priority). Since this script's filter-chain is also using the priority -200, there was no guarantee that the connection tracking would happen before the filter-chain's rule matching began (which could be the reason why outgoing connections would only work if no other table existed other than the geo-filter table). I reduced the filter-chain's priority to -190 so that conntrack operations would always happen before traversing the filter-chain rules and now everything is working the way it is supposed to.

Here's @peeterm007's firewall setup along with a geo-filter table to only allow packets from Monaco (along with the ct state rule).

table inet my_table {
        chain my_input {
                type filter hook input priority filter; policy drop;
                iif "lo" accept comment "Accept any localhost traffic"
                ct state invalid drop comment "Drop invalid connections"
                ct state established,related accept comment "Accept traffic originated from us"
                icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, mld-listener-query, mld-listener-report, mld-listener-done, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert, mld2-listener-report } accept comment "Accept ICMPv6"
                icmp type { destination-unreachable, router-advertisement, router-solicitation, time-exceeded, parameter-problem } accept comment "Accept ICMP"
                tcp dport 22 accept comment "Accept SSH on port 22"
                tcp dport { 80, 443 } accept comment "Accept HTTP (ports 80, 443)"
        }

        chain my_forward {
                type filter hook forward priority filter; policy drop;
        }

        chain my_output {
                type filter hook output priority filter; policy accept;
        }
}
table inet geo-filter {
        set filter-v4 {
                type ipv4_addr
                flags interval
                auto-merge
                elements = { 37.44.224.0/22, 80.94.96.0/20,
                             82.113.0.0/19, 87.238.104.0/21,
                             87.254.224.0/19, 88.209.64.0/18,
                             91.199.109.0/24, 176.114.96.0/20,
                             185.47.116.0/22, 185.162.120.0/22,
                             185.250.4.0/22, 188.191.136.0/21,
                             194.9.12.0/23, 195.20.192.0/23,
                             195.78.0.0/19, 213.133.72.0/21,
                             213.137.128.0/19 }
        }

        set filter-v6 {
                type ipv6_addr
                flags interval
                auto-merge
                elements = { 2a01:8fe0::/32,
                             2a07:9080::/29,
                             2a0b:8000::/29 }
        }

        chain filter-chain {
                type filter hook prerouting priority -190; policy drop;
                ct state established,related accept
                ip6 saddr { ::1, fe80::/10 } accept
                ip saddr { 10.0.0.0/8, 127.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } accept
                ip saddr @filter-v4 accept
                ip6 saddr @filter-v6 accept
        }
}

I can successfully ping addresses outside the filter-set.

$ ping 1.1.1.1 -c 4
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=59 time=9.13 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=59 time=8.46 ms
64 bytes from 1.1.1.1: icmp_seq=3 ttl=59 time=9.53 ms
64 bytes from 1.1.1.1: icmp_seq=4 ttl=59 time=9.35 ms

--- 1.1.1.1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 8.455/9.114/9.527/0.406 ms

$ ping google.com -c 4
PING google.com (216.58.196.110) 56(84) bytes of data.
64 bytes from del11s05-in-f14.1e100.net (216.58.196.110): icmp_seq=2 ttl=118 time=11.6 ms
64 bytes from maa03s19-in-f110.1e100.net (216.58.196.110): icmp_seq=3 ttl=118 time=12.3 ms
64 bytes from maa03s19-in-f110.1e100.net (216.58.196.110): icmp_seq=4 ttl=118 time=9.99 ms

--- google.com ping statistics ---
4 packets transmitted, 3 received, 25% packet loss, time 3020ms
rtt min/avg/max/mdev = 9.994/11.309/12.311/0.971 ms

@peeterm007 @b0rZ84 : Please download the script again and let me know if it works for you. You'll have to add the ct state rule manually for now using nft insert rule inet geo-filter filter-chain ct state established,related accept. I'll update the script soon so that it adds the ct state rule automatically as well.

b0rZ84 commented 4 years ago

@peeterm007 @b0rZ84 : Please download the script again and let me know if it works for you. You'll have to add the ct state rule manually for now using nft insert rule inet geo-filter filter-chain ct state established,related accept. I'll update the script soon so that it adds the ct state rule automatically as well.

It's working perfectly now. Thanks for the quick fix!

rpthms commented 4 years ago

That's great to know! I'll add the ct state rule option to the script before closing this issue.

rpthms commented 4 years ago

@peeterm007 @b0rZ84 You can now add the ct state rule via the script itself by using the --allow-established flag.

Copying from the example section in the README.md:

Feel free to reopen the issue if you come across a bug.

peeterm007 commented 4 years ago

Cool, it works now. Many thanks for fixing it!