summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaciej Żenczykowski <maze@google.com>2022-07-12 16:17:33 -0700
committerMaciej Żenczykowski <maze@google.com>2022-07-14 12:28:37 +0000
commitf6ec94ea69dd8b76d977ea2a68c2893a733e2806 (patch)
tree95cc0eb18d29d67f1df593e9de6bbd8aef5a3bc9
parent086c219456b03a8bc1516c3c520beada72e80ab8 (diff)
downloadandroid-clat-f6ec94ea69dd8b76d977ea2a68c2893a733e2806.tar.gz
send out IPv6 DAD packets when starting up clatd for the clat v6 address
Since we're telling the kernel this is an anycast ip [via setsockopt(sock, SOL_IPV6, IPV6_JOIN_ANYCAST)], it apparently doesn't perform DAD for us when adding the ip. From the kernel's perspective this behaviour makes absolute sense: an anycast'ed IP would normally be configured on multiple machines - possibly on the same L2 network segment - and thus DAD may well fail. For the CLAT use case we want to add the IP, so that the kernel handles IPv6 MLD and replies with NA to incoming NS, yet we don't want the kernel to 'own' the IP itself, as that would cause it to send out spurious TCP resets and icmp port unreachable messages. (Not to mention applications could be confused by the IP being configured on an interface: while it is *not* for direct application use) Basically we want something like 'proxy arp (nd)' for the clat ip... We should probably do it manually... but that's hard... As such this isn't a full DAD [duplicate address detection] implementation. We don't actually perform DAD, just like we didn't before this patch... We simply send the packets out to pretend we did. The chance of collision is super low, since the bottom 64-bits are picked randomly with 16 of them chosen to guarantee checksum neutrality. ie. we should have around 48 bits of entropy. That's a few hundred - few thousand in 2.8e14 chance of collision. (depending on how many ips are used on the L2 segment) I'm hoping this will be enough to resolve compatibility problems with some Cisco networking gear based ipv6-only wifi networks, where apparently the CLAT v6 address isn't ever neighbour solicited for by the router. I'm guessing it's some security feature: you didn't do DAD, so it's not your IP... Sample generated packet: b6:7a:01:a5:26:27 (oui Unknown) > 33:33:ff:54:ae:21 (oui Unknown), ethertype IPv6 (0x86dd), length 86: (hlim 255, next-header ICMPv6 (58) payload length: 32) :: > ff02::1:ff54:ae21: [icmp6 sum ok] ICMP6, neighbor solicitation, length 32, who has 2401:fa00:480:13d:2a65:4e6a:7554:ae21 unknown option (14), length 8 (1): 0x0000: 891b eaef 1263 0x0000: 6000 0000 0020 3aff 0000 0000 0000 0000 `.....:......... 0x0010: 0000 0000 0000 0000 ff02 0000 0000 0000 ................ 0x0020: 0000 0001 ff54 ae21 8700 77b6 0000 0000 .....T.!..w..... 0x0030: 2401 fa00 0480 013d 2a65 4e6a 7554 ae21 $......=*eNjuT.! 0x0040: 0e01 891b 434c 4154 ....CLAT Bug: 238387793 Test: TreeHugger, manually on bramble with ipv6-only wifi network Signed-off-by: Maciej Żenczykowski <maze@google.com> Change-Id: I16141bc427467e935da55f4eac6943659d8a65ad
-rw-r--r--clatd.c91
1 files changed, 91 insertions, 0 deletions
diff --git a/clatd.c b/clatd.c
index f72f431..4ce94f7 100644
--- a/clatd.c
+++ b/clatd.c
@@ -40,6 +40,7 @@
#include <sys/uio.h>
#include "clatd.h"
+#include "checksum.h"
#include "config.h"
#include "dump.h"
#include "getaddr.h"
@@ -123,11 +124,101 @@ void read_packet(int read_fd, int write_fd, int to_ipv6) {
translate_packet(write_fd, 1 /* to_ipv6 */, packet, readlen);
}
+// IPv6 DAD packet format:
+// Ethernet header (if needed) will be added by the kernel:
+// u8[6] src_mac; u8[6] dst_mac '33:33:ff:XX:XX:XX'; be16 ethertype '0x86DD'
+// IPv6 header:
+// be32 0x60000000 - ipv6, tclass 0, flowlabel 0
+// be16 payload_length '32'; u8 nxt_hdr ICMPv6 '58'; u8 hop limit '255'
+// u128 src_ip6 '::'
+// u128 dst_ip6 'ff02::1:ffXX:XXXX'
+// ICMPv6 header:
+// u8 type '135'; u8 code '0'; u16 icmp6 checksum; u32 reserved '0'
+// ICMPv6 neighbour solicitation payload:
+// u128 tgt_ip6
+// ICMPv6 ND options:
+// u8 opt nr '14'; u8 length '1'; u8[6] nonce '6 random bytes'
+void send_dad(int fd, const struct in6_addr* tgt, int count) {
+ struct {
+ struct ip6_hdr ip6h;
+ struct nd_neighbor_solicit ns;
+ uint8_t ns_opt_nr;
+ uint8_t ns_opt_len;
+ uint8_t ns_opt_nonce[6];
+ } dad_pkt = {
+ .ip6h = {
+ .ip6_flow = htonl(6 << 28), // v6, 0 tclass, 0 flowlabel
+ .ip6_plen = htons(sizeof(dad_pkt) - sizeof(struct ip6_hdr)), // payload length, ie. 32
+ .ip6_nxt = IPPROTO_ICMPV6, // 58
+ .ip6_hlim = 255,
+ .ip6_src = {}, // ::
+ .ip6_dst.s6_addr = {
+ 0xFF, 0x02, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 1,
+ 0xFF, tgt->s6_addr[13], tgt->s6_addr[14], tgt->s6_addr[15],
+ }, // ff02::1:ffXX:XXXX - multicast group address derived from bottom 24-bits of tgt
+ },
+ .ns = {
+ .nd_ns_type = ND_NEIGHBOR_SOLICIT, // 135
+ .nd_ns_code = 0,
+ .nd_ns_cksum = 0, // will be calculated later
+ .nd_ns_reserved = 0,
+ .nd_ns_target = *tgt,
+ },
+ .ns_opt_nr = 14, // icmp6 option 'nonce' from RFC3971
+ .ns_opt_len = 1, // in units of 8 bytes, including option nr and len
+ .ns_opt_nonce = { // 'opt_len' * 8 - [size of u8: opt nr] - [size of u8: opt len] = 6 bytes
+ 0, 0, 'C', 'L', 'A', 'T',
+ }, // first 2 bytes filled in below with random bytes, CLAT left to ease id in tcpdump
+ };
+ arc4random_buf(&dad_pkt.ns_opt_nonce, 2);
+
+ // 40 byte IPv6 header + 8 byte ICMPv6 header + 16 byte ipv6 target address + 8 byte nonce option
+ _Static_assert(sizeof(dad_pkt) == 40 + 8 + 16 + 8, "sizeof dad packet != 72");
+
+ // IPv6 header checksum is standard negated 16-bit one's complement sum over the icmpv6 pseudo
+ // header (which includes payload length, nextheader, and src/dst ip) and the icmpv6 payload.
+ //
+ // Src/dst ip immediately prefix the icmpv6 header itself, so can be handled along
+ // with the payload. We thus only need to manually account for payload len & next header.
+ //
+ // The magic '8' is simply the offset of the ip6_src field in the ipv6 header,
+ // ie. we're skipping over the ipv6 version, tclass, flowlabel, payload length, next header
+ // and hop limit fields, because they're not quite where we want them to be.
+ //
+ // ip6_plen is already in network order, while ip6_nxt is a single byte and thus needs htons().
+ uint32_t csum = dad_pkt.ip6h.ip6_plen + htons(dad_pkt.ip6h.ip6_nxt);
+ csum = ip_checksum_add(csum, &dad_pkt.ip6h.ip6_src, sizeof(dad_pkt) - 8);
+ dad_pkt.ns.nd_ns_cksum = ip_checksum_finish(csum);
+
+ const struct sockaddr_in6 dst = {
+ .sin6_family = AF_INET6,
+ .sin6_addr = dad_pkt.ip6h.ip6_dst,
+ .sin6_scope_id = if_nametoindex(Global_Clatd_Config.native_ipv6_interface),
+ };
+
+ while (count--) {
+ sendto(fd, &dad_pkt, sizeof(dad_pkt), 0 /*flags*/, (const struct sockaddr *)&dst, sizeof(dst));
+ }
+}
+
/* function: event_loop
* reads packets from the tun network interface and passes them down the stack
* tunnel - tun device data
*/
void event_loop(struct tun_data *tunnel) {
+ // Apparently some network gear will refuse to perform NS for IPs that aren't DAD'ed,
+ // this would then result in an ipv6-only network with working native ipv6, working
+ // IPv4 via DNS64, but non-functioning IPv4 via CLAT (ie. IPv4 literals + IPv4 only apps).
+ // The kernel itself doesn't do DAD for anycast ips (but does handle IPV6 MLD and handle ND).
+ // So we'll spoof dad here, and yeah, we really should check for a response and in
+ // case of failure pick a different IP. Seeing as 48-bits of the IP are utterly random
+ // (with the other 16 chosen to guarantee checksum neutrality) this seems like a remote
+ // concern...
+ // TODO: actually perform true DAD
+ send_dad(tunnel->write_fd6, &Global_Clatd_Config.ipv6_local_subnet, 2);
+
time_t last_interface_poll;
struct pollfd wait_fd[] = {
{ tunnel->read_fd6, POLLIN, 0 },