Open Spindel opened 3 years ago
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.
Yes, traffic should be dropped that doesn't arrive from the proper interface.
Oh yes, absolutely agree. Thanks so much for the well-written issue @Spindel, and sorry for taking a bit to reply.
innernet-server
will be updated to use SO_BINDTODEVICE
to have it listen only on the WireGuard interface.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.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
Awesome, thank you for the help
@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.
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.)
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:
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.
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.
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
.
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
.
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.
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.)
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.
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.
It's not sufficient. rp_filter
ing 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.
FYI there is currently a discussion on HN on how Tailscale prevents IP spoofing which might be of interest here.
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:
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):
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.