Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more protections against OS configurations that allow IP spoofing #26

Open
Spindel opened this issue Mar 31, 2021 · 13 comments
Open

Comments

@Spindel
Copy link

Spindel commented Mar 31, 2021

In the innernet blog-post there are some claims about authenticated and trust in IP-addresses.

However, innernet does not actually live up to these claims.

Test methodology:

With three devices:

  • one innernet server (that lives on the internet) (10.42.0.0/16)
  • one innernet client ( NAT-ed, on fex. a WiFi) (10.42.0.1/24)
  • One rogue actor ( NAT-ed, on the same net as the client)

Assuming that the client LAN is 192.168.0.1/24.
Assuming that the innernet server is set up with the default cidr of 10.42.0.0/16, we hook up the client and enable the server.

Next, we put a super-seekrit service on the client IP of 10.42.0.1 ( telnet, with admin/admin as password!)

On the "rogue" actor, we then do the following (assuming that eth0 is the ip address):

ip address add 10.42.0.8 dev eth0
ip route add 10.42.0.0/16 dev eth0

Now the "rogue" client can freely access services that are supposed to only be accessible to internal clients on the innernet ip.

Problem

Linux by default responds to traffic directed to it's own IP addresses on any interface. This can be adjusted with a sysctl, but that sysctl should not be configured by software as a security mechanism, as it may break a fair few things. The proper(Er) solution is to add an interface-level filter for traffic.

This means that innernet client should also make sure to drop traffic directed to it's internal IP that arrives on any interface other than the wg interface.

@SolomonSklash
Copy link

My understanding of Wireguard is that it provides this, not innernet itself:

"At the heart of WireGuard is a concept called Cryptokey Routing, which works by associating public keys with a list of tunnel IP addresses that are allowed inside the tunnel. Each network interface has a private key and a list of peers. Each peer has a public key. Public keys are short and simple, and are used by peers to authenticate each other. They can be passed around for use in configuration files by any out-of-band method, similar to how one might send their SSH public key to a friend for access to a shell server."

Innernet is a wrapper around associating IPs and interfaces and would not provide the authentication, Wireguard does.

@DanielJoyce
Copy link

Yes, traffic should be dropped that doesn't arrive from the proper interface.

@mcginty
Copy link
Collaborator

mcginty commented Apr 2, 2021

Oh yes, absolutely agree. Thanks so much for the well-written issue @Spindel, and sorry for taking a bit to reply.

  1. I'm going to edit the blog post to reflect that IP addresses can't be relied on as identifiers unless specific care is taken.
  2. I'll create documentation on setting up services that which to use IPs as identities to not allow rogue IP traffic off the WireGuard interface.
  3. innernet-server will be updated to use SO_BINDTODEVICE to have it listen only on the WireGuard interface.
  4. Will need to look into the best way for the innernet client to try to block traffic from that IP range on other interfaces in a non-destructive way for people's various configurations. Would be interested in any recommendations on that.

@Spindel
Copy link
Author

Spindel commented Apr 3, 2021

Oh yes, absolutely agree. Thanks so much for the well-written issue @Spindel, and sorry for taking a bit to reply.

You're welcome, and the plan ahead seems like a decent one. Regarding how to add the firewalling rules, that's always tricky, as Linux networking is in a bit of a flux to begin with (low level API's like nftables, eBPF, iptables, high level like firewalld, ufw, etc. ) and then adding Windows and macOS all make it a bit tricky.

Even then, address conflicts between local and VPN network ranges can happen, especially on devices that roam (wifi at the office might well be in the same range, etc.) making the service hard to use. ( This was one of the cool features from tailscale, their use of non-routable addresses. )

Sadly, I don't have any good solutions, only problems, and maybe a simple drop-in script might be the better solution? ie, declare an script that will be called with <interface_name> <network_range> when topology changes, and default that script to be something useful?

@kamiller8118
Copy link

kamiller8118 commented Apr 3, 2021 via email

@codethief
Copy link

codethief commented Apr 5, 2021

@SolomonSklash

My` understanding of Wireguard is that it provides this, not innernet itself:

"At the heart of WireGuard is a concept called Cryptokey Routing, which works by associating public keys with a list of tunnel IP addresses that are allowed inside the tunnel. […]"

As far as I understand, the problem is that WireGuard doesn't do the routing (much less authentication) in the case discussed here: The attacker simply sets a different IP address for himself (a "fake IP address" if you will) and starts sending traffic from that IP to the innernet client which will arrive on the client's eth0/wlan0 interface (not wg0) and be forwarded to the super-seekrit service.

So until the issue here is solved, any incoming traffic from innernet adresses will still need to be authenticated separately. I would say this is a major issue because it implies that running services like sshd or web servers strictly "inside" the innernet is actually not possible at the current time (or only feasible with additional manual effort). Put differently: There is no included firewall and services listening on "internal" innernet IP addresses are still accessible from the outside.

@codethief
Copy link

codethief commented Apr 5, 2021

As an addendum to my previous comment, I've got a question regarding the attack vector discussed here (I'm not too familiar with the ins and outs of the Linux network stack): If the attacker disguises as 10.42.0.8 and sends a message to our innernet client at 10.42.0.1, will a response by the client then reach the attacker? After all, the network on the client will be set up such that packets to 10.42.0.0/16 should get routed through the wg0 interface. So how does this work if a connection is initiated by a message whose sender is some IP in the range 10.42.0.0/16 but which doesn't reach the client through the wg0 interface? Does this possibly depend on whether it's a UDP or a TCP connection, i.e. whether the client's "super-seekrit service" was listening for incoming UDP or TCP connections? (I imagine for UDP the client's response shouldn't reach the attacker but for TCP it would but I could be mistaken.)

@mcginty
Copy link
Collaborator

mcginty commented Apr 5, 2021

I clearly wasn't familiar enough either, but it's quite interesting when you dig into it. The security issues with the current model can be broken into two main components, as I currently understand it:

1. Private IP Spoofing via non-strict Reverse Path Filtering (RFC 3704)

Most Linux distros, for example, (with the exception of RHEL) default to what's called "loose" Reverse Path Filtering. Say you have two interfaces: eth0 which routes 192.168.0.0/24, and eth1 which routes 10.0.0.0/8.

One would think that since you send packets to 10.0.0.1 via eth1, the reverse would also be enforced - Linux would restrict incoming packets in the 10.0.0.0/8 range to those arriving over eth1. This is where the issue lies. In Loose mode, Linux will simply look to see if any interface can route that packet, and then will accept incoming packets on any interface. So, if you have an application listening on 10.0.0.2, your local address on eth1, packets can arrive over eth0 that have a source IP of 10.0.0.X and a destination IP of 10.0.0.2, and can successfully establish a connection using any IP of their choosing as long as it's routable by any interface.

As an example though for the innernet server, this is not an open vector if you're running it on a server whose firewall is blocking all traffic except for 51820/udp, which is recommended.

Fixes

On the application level, you can bind a socket to a specific interface, which is the fix I'm currently implementing first for innernet-server.

On the OS level, innernet can check and possibly enforce the correct value for the net.ipv4.conf.all.rp_filter sysctl (antispoof packet filter on BSD-likes including macOS, I believe). This breaks asymmetric network setups, though, so more care is required.

There may also be less-invasive firewall rules that can also be added to simply block traffic from innernet internal IPs on any interface that's not the WireGuard interface. This would possibly be a less controversial change.

2. CSRF-like attacks

There are any number of ways an HTTP request can be made on your behalf without your consent, and peers should be able to prevent random HTTP requests generated from other applications on their machine from being accepted by innernet-server.

Fixes

The server can generate a token when the interface is brought up, and require that the client sends that token along with every request (similar to how most API keys work out there). The client can store that in /etc/innernet/[interface].conf, and that way access is limited to those who can read /etc/innernet/[interface].conf.

Patch release

Fixes are in the works, thanks everyone for being patient and contributing to the discussion. Let this be a reminder that this is experimental software and not to be considered fully secure until much more rigorous review and breaking has occurred.

mcginty added a commit that referenced this issue Apr 5, 2021
This is one many upcoming changes to address IP spoofing
issues.

See #26 for more details.
mcginty added a commit that referenced this issue Apr 5, 2021
This is one many upcoming changes to address IP spoofing
issues.

See #26 for more details.
mcginty added a commit that referenced this issue Apr 5, 2021
This is one many upcoming changes to address IP spoofing
issues.

See #26 for more details.
mcginty added a commit that referenced this issue Apr 5, 2021
This is one many upcoming changes to address IP spoofing
issues.

See #26 for more details.
@codethief
Copy link

codethief commented Apr 6, 2021

On the application level, you can bind a socket to a specific interface

Unfortunately, most applications[0] don't provide config options for this, so short of patching most applications one desires to use, I don't think this is a feasible option.

[0]: In particular, this is also true for Docker which only allows binding to a certain IP address, not interface, leading to the very same issue we're discussing here.

On the OS level, innernet can check and possibly enforce the correct value for the net.ipv4.conf.all.rp_filter sysctl

This and/or it could make sure iptables is set up correctly and to warn the user if necessary. Maybe one could make this a config option? (Or make it the default and add an option to disable it.)

@bschwind bschwind pinned this issue Apr 6, 2021
mcginty added a commit that referenced this issue Apr 6, 2021
This is one many upcoming changes to address IP spoofing
issues.

See #26 for more details.
@mcginty mcginty unpinned this issue Apr 8, 2021
@mcginty
Copy link
Collaborator

mcginty commented Apr 24, 2021

I just added a section on security to the README and am going to rename this issue to better reflect the tasks remaining outside of the CSRF work covered in #38.

@mcginty mcginty changed the title Do not claim that IP traffic is authenticated Add more protections against OS configurations that allow IP spoofing Apr 24, 2021
@ArsenArsen
Copy link

https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt

rp_filter can be enabled per interface. I'll be testing this later today, and sending in a patch if it turns out sufficient.

@ArsenArsen
Copy link

ArsenArsen commented Sep 23, 2021

It's not sufficient. rp_filtering would only apply to packets coming into the interface, not for packets addressed to the interface that it was enabled on. However, the following nftable ruleset works:

flush table inet innernet_testnet;

table inet innernet_testnet {
        chain input {
                type filter hook input priority 0;
                policy accept;

                ip daddr 10.42.0.0/16 iifname != { lo, testnet } drop;
        }
}

... where testnet is the WireGuard interface and innernet that was created. This should be rather nonintrusive but I didn't test it with some more complex/fragile firewall setups, just with libvirt-managed rules. I'm not proficient enough in Rust to implement this, though.

Considering WireGuard is entirely L3, this should be enough to filter all traffic addressed on 10.42.0.0 from reaching other interfaces, and requires no modification to existing software (my test was socat UDP4-RECVFROM:1234,bind=10.42.0.1,fork -).

A similar rule should be added for other addresses assigned to the WireGuard interface, such as any possible IPv6 addresses, and it'd likely be good to extract the two interfaces into a set in that case.

PS: it'd probably be desirable to use iif rather than iifname in generated rules, and still, in complement, enable rp_filter for the interface, too.

EDIT: https://wiki.nftables.org/wiki-nftables/index.php/Atomic_rule_replacement the above is not very atomic, but that should be fine, if this only happens after an interface being added and before any peers or configuration is established.

@codethief
Copy link

codethief commented Apr 14, 2022

FYI there is currently a discussion on HN on how Tailscale prevents IP spoofing which might be of interest here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants