diff --git a/sys/src/cmd/ip/ayiya.c b/sys/src/cmd/ip/ayiya.c new file mode 100644 index 000000000..052d0640f --- /dev/null +++ b/sys/src/cmd/ip/ayiya.c @@ -0,0 +1,717 @@ +/* + * ayiya - tunnel client. + */ + +#include +#include +#include +#include +#include + +/* + * IPv6 and related IP protocols & their numbers: + * + * ipv6 41 IPv6 # Internet Protocol, version 6 + * ipv6-route 43 IPv6-Route # Routing Header for IPv6 + * ipv6-frag 44 IPv6-Frag # Fragment Header for IPv6 + * esp 50 ESP # Encapsulating Security Payload + * ah 51 AH # Authentication Header + * ipv6-icmp 58 IPv6-ICMP icmp6 # ICMP version 6 + * ipv6-nonxt 59 IPv6-NoNxt # No Next Header for IPv6 + * ipv6-opts 60 IPv6-Opts # Destination Options for IPv6 + */ +enum { + IP_IPV6PROTO = 41, /* IPv4 protocol number for IPv6 */ + IP_ESPPROTO = 50, /* IP v4 and v6 protocol number */ + IP_AHPROTO = 51, /* IP v4 and v6 protocol number */ + IP_ICMPV6PROTO = 58, + V6to4pfx = 0x2002, + + IP_MAXPAY = 2*1024, +}; + +enum { + AYIYAMAXID = 1<<15, + AYIYAMAXSIG = 15*4, + + AYIYAMAXHDR = 8+AYIYAMAXID+AYIYAMAXSIG, + + IdNone = 0, + IdInteger, + IdString, + + HashNone = 0, + HashMD5, + HashSHA1, + + AuthNone = 0, + AuthSharedKey, + AuthPubKey, + + OpNone = 0, + OpForward, + OpEchoRequest, + OpEchoRequestAndForward, + OpEchoResponse, + OpMOTD, + OpQueryRequest, + OpQueryResponse, +}; + +typedef struct AYIYA AYIYA; +struct AYIYA +{ + uint idlen; + uint idtype; + uint siglen; + uint hashmeth; + uint authmeth; + uint opcode; + uint nexthdr; + uint epochtime; + + uchar *identity; + uchar *signature; +}; + +AYIYA conf; + +int gateway; +int debug; + +uchar local6[IPaddrlen]; +uchar remote6[IPaddrlen]; +uchar localmask[IPaddrlen]; +uchar localnet[IPaddrlen]; + +uchar nullsig[AYIYAMAXSIG]; + +static char *secret = nil; + +static char *outside = nil; /* dial string of tunnel server */ +static char *inside = "/net"; + +static int badipv4(uchar*); +static int badipv6(uchar*); +static void ip2tunnel(int, int); +static void tunnel2ip(int, int); + +static void +ayiyadump(AYIYA *a) +{ + int i; + + fprint(2, "idlen=%ud idtype=%ux siglen=%ud hashmeth=%ud authmeth=%ud opcode=%ux nexthdr=%ux epochtime=%ux\n", + a->idlen, a->idtype, a->siglen, a->hashmeth, a->authmeth, a->opcode, a->nexthdr, a->epochtime); + fprint(2, "identity=[ "); + for(i=0; iidlen; i++) + fprint(2, "%.2ux ", a->identity[i]); + fprint(2, "] "); + fprint(2, "signature=[ "); + for(i=0; isiglen; i++) + fprint(2, "%.2ux ", a->signature[i]); + fprint(2, "]\n"); + +} + +static uint +lg2(uint a) +{ + uint n; + + for(n = 0; (a >>= 1) != 0; n++) + ; + return n; +} + +static int +ayiyapack(AYIYA *a, uchar *pay, int paylen) +{ + uchar *pkt; + + pkt = pay; + if(a->siglen > 0){ + pkt -= a->siglen; + memmove(pkt, a->signature, a->siglen); + } + if(a->idlen > 0){ + pkt -= a->idlen; + memmove(pkt, a->identity, a->idlen); + } + + pkt -= 4; + pkt[0] = a->epochtime>>24; + pkt[1] = a->epochtime>>16; + pkt[2] = a->epochtime>>8; + pkt[3] = a->epochtime; + + pkt -= 4; + pkt[0] = (lg2(a->idlen)<<4) | a->idtype; + pkt[1] = ((a->siglen/4)<<4) | a->hashmeth; + pkt[2] = (a->authmeth<<4) | a->opcode; + pkt[3] = a->nexthdr; + + USED(paylen); + + return pay - pkt; +} + +static int +ayiyaunpack(AYIYA *a, uchar *pkt, int pktlen) +{ + int hdrlen; + + if(pktlen < 8) + return -1; + + a->idlen = 1<<(pkt[0] >> 4); + a->idtype = pkt[0] & 15; + a->siglen = (pkt[1] >> 4) * 4; + a->hashmeth = pkt[1] & 15; + a->authmeth = pkt[2] >> 4; + a->opcode = pkt[2] & 15; + a->nexthdr = pkt[3]; + a->epochtime = pkt[7] | pkt[6]<<8 | pkt[5]<<16 | pkt[4]<<24; + + hdrlen = 8 + a->idlen + a->siglen; + if(hdrlen > pktlen) + return -1; + + a->identity = nil; + if(a->idlen > 0) + a->identity = pkt + 8; + + a->signature = nil; + if(a->siglen > 0) + a->signature = pkt + 8 + a->idlen; + + return hdrlen; +} + +static int +ayiyahash(uint meth, uchar *pkt, int pktlen, uchar *dig) +{ + switch(meth){ + case HashMD5: + if(dig != nil) + md5(pkt, pktlen, dig, nil); + return MD5dlen; + case HashSHA1: + if(dig != nil) + sha1(pkt, pktlen, dig, nil); + return SHA1dlen; + } + return 0; +} + +static void +ayiyasign(AYIYA *a, uchar *pkt, int pktlen) +{ + uchar dig[AYIYAMAXSIG], *pktsig; + + if(a->hashmeth == HashNone && a->siglen == 0) + return; + + assert(a->siglen <= sizeof(dig)); + assert(a->siglen <= pktlen - a->idlen - 8); + pktsig = pkt + 8 + a->idlen; + + if(ayiyahash(a->hashmeth, pkt, pktlen, dig) != a->siglen){ + memset(pktsig, 0, a->siglen); + return; + } + + memmove(pktsig, dig, a->siglen); +} + +static int +ayiyaverify(AYIYA *a, uchar *pkt, int pktlen) +{ + uchar dig[AYIYAMAXSIG], sig[AYIYAMAXSIG]; + + if(conf.hashmeth == HashNone && a->siglen == 0) + return 0; + if(a->hashmeth != conf.hashmeth || a->authmeth != conf.authmeth || a->siglen != conf.siglen) + return -1; + memmove(sig, a->signature, a->siglen); + memmove(a->signature, conf.signature, a->siglen); + if(ayiyahash(a->hashmeth, pkt, pktlen, dig) != a->siglen) + return -1; + return memcmp(sig, dig, a->siglen) != 0; +} + +static int +ayiyaout(int fd, AYIYA *a, uchar *p, int n) +{ + int m; + + a->idlen = conf.idlen; + a->siglen = conf.siglen; + a->idtype = conf.idtype; + a->hashmeth = conf.hashmeth; + a->authmeth = conf.authmeth; + a->identity = conf.identity; + a->signature = conf.signature; + + a->epochtime = time(nil); + + if (debug > 1) { + fprint(2, "send: "); + ayiyadump(a); + } + + m = ayiyapack(a, p, n); + n += m, p -= m; + + ayiyasign(a, p, n); + + if (write(fd, p, n) != n) { + syslog(0, "ayiya", "error writing to tunnel (%r), giving up"); + return -1; + } + return 0; +} + +static int +ayiyarquery(char *q) +{ + fprint(2, "ayiyarquery: %s\n", q); + *q = '\0'; + return 0; +} + +static void +usage(void) +{ + fprint(2, "usage: %s [-g] [-x mtpt] [-k secret] local6[/mask] remote4 remote6\n", + argv0); + exits("Usage"); +} + +/* process non-option arguments */ +static void +procargs(int argc, char **argv) +{ + char *p, *loc6; + + if (argc < 3) + usage(); + + loc6 = *argv++; + argc--; + + /* local v6 address (mask defaults to /128) */ + memcpy(localmask, IPallbits, sizeof localmask); + p = strchr(loc6, '/'); + if (p != nil) { + parseipmask(localmask, p); + *p = 0; + } + if (parseip(local6, loc6) == -1) + sysfatal("bad local v6 address %s", loc6); + if (isv4(local6)) + usage(); + if (argc >= 1 && argv[0][0] == '/') { + parseipmask(localmask, *argv++); + argc--; + } + if (debug) + fprint(2, "local6 %I %M\n", local6, localmask); + + outside = netmkaddr(*argv++, "udp", "5072"); + argc--; + if(outside == nil) + usage(); + outside = strdup(outside); + if (debug) + fprint(2, "outside %s\n", outside); + + /* remote v6 address */ + if (parseip(remote6, *argv++) == -1) + sysfatal("bad remote v6 address %s", argv[-1]); + argc--; + if (argc != 0) + usage(); + + maskip(local6, localmask, localnet); + if (debug) + fprint(2, "localnet %I remote6 %I\n", localnet, remote6); +} + +static void +setup(int *v6net) +{ + int n, cfd; + char *cl, *ir; + char buf[128], path[64]; + + /* + * open local IPv6 interface (as a packet interface) + */ + + cl = smprint("%s/ipifc/clone", inside); + cfd = open(cl, ORDWR); /* allocate a conversation */ + n = 0; + if (cfd < 0 || (n = read(cfd, buf, sizeof buf - 1)) <= 0) + sysfatal("can't make packet interface %s: %r", cl); + if (debug) + fprint(2, "cloned %s as local v6 interface\n", cl); + free(cl); + buf[n] = 0; + + snprint(path, sizeof path, "%s/ipifc/%s/data", inside, buf); + *v6net = open(path, ORDWR); + if (*v6net < 0 || fprint(cfd, "bind pkt") < 0) + sysfatal("can't bind packet interface: %r"); + /* 1280 is MTU, apparently from rfc2460 */ + if (fprint(cfd, "add %I %M %I 1280", local6, localmask, remote6) <= 0) + sysfatal("can't set local ipv6 address: %r"); + close(cfd); + if (debug) + fprint(2, "opened & bound %s as local v6 interface\n", path); + + if (gateway) { + /* route global addresses through the tunnel to remote6 */ + ir = smprint("%s/iproute", inside); + cfd = open(ir, OWRITE); + if (cfd >= 0 && debug) + fprint(2, "injected 2000::/3 %I into %s\n", remote6, ir); + free(ir); + if (cfd < 0 || fprint(cfd, "add 2000:: /3 %I", remote6) <= 0) + sysfatal("can't set default global route: %r"); + } +} + +static void +runtunnel(int v6net, int tunnel) +{ + /* run the tunnel copying in the background */ + switch (rfork(RFPROC|RFNOWAIT|RFMEM|RFNOTEG)) { + case -1: + sysfatal("rfork"); + default: + exits(nil); + case 0: + break; + } + + switch (rfork(RFPROC|RFNOWAIT|RFMEM)) { + case -1: + sysfatal("rfork"); + default: + tunnel2ip(tunnel, v6net); + break; + case 0: + ip2tunnel(v6net, tunnel); + break; + } + exits("tunnel gone"); +} + +void +main(int argc, char **argv) +{ + int tunnel, v6net; + + fmtinstall('I', eipfmt); + fmtinstall('V', eipfmt); + fmtinstall('M', eipfmt); + + ARGBEGIN { + case 'd': + debug++; + break; + case 'g': + gateway++; + break; + case 'x': + inside = EARGF(usage()); + break; + case 'k': + secret = EARGF(usage()); + break; + default: + usage(); + } ARGEND + + procargs(argc, argv); + + conf.idtype = IdInteger; + conf.idlen = sizeof(local6); + conf.identity = local6; + + conf.authmeth = AuthNone; + conf.hashmeth = HashSHA1; + conf.siglen = ayiyahash(conf.hashmeth, nil, 0, nil); + conf.signature = nullsig; + + if(secret != nil){ + conf.authmeth = AuthSharedKey; + conf.signature = malloc(conf.siglen); + ayiyahash(conf.hashmeth, (uchar*)secret, strlen(secret), conf.signature); + memset(secret, 0, strlen(secret)); /* prevent accidents */ + } + + tunnel = dial(outside, nil, nil, nil); + if (tunnel < 0) + sysfatal("can't dial tunnel: %r"); + + setup(&v6net); + runtunnel(v6net, tunnel); + exits(0); +} + +/* + * based on libthread's threadsetname, but drags in less library code. + * actually just sets the arguments displayed. + */ +void +procsetname(char *fmt, ...) +{ + int fd; + char *cmdname; + char buf[128]; + va_list arg; + + va_start(arg, fmt); + cmdname = vsmprint(fmt, arg); + va_end(arg); + if (cmdname == nil) + return; + snprint(buf, sizeof buf, "#p/%d/args", getpid()); + if((fd = open(buf, OWRITE)) >= 0){ + write(fd, cmdname, strlen(cmdname)+1); + close(fd); + } + free(cmdname); +} + +static int alarmed; + +static void +catcher(void*, char *msg) +{ + if(strstr(msg, "alarm") != nil){ + alarmed = 1; + noted(NCONT); + } + noted(NDFLT); +} + +/* + * encapsulate v6 packets from the packet interface + * and send them into the tunnel. + */ +static void +ip2tunnel(int in, int out) +{ + uchar buf[AYIYAMAXHDR + IP_MAXPAY], *p; + Ip6hdr *ip; + AYIYA y[1]; + int n, m; + + procsetname("v6 %I -> tunnel %s %I", local6, outside, remote6); + + notify(catcher); + + /* get a V6 packet destined for the tunnel */ + for(;;) { + alarmed = 0; + alarm(60*1000); + + p = buf + AYIYAMAXHDR; + if ((n = read(in, p, IP_MAXPAY)) <= 0) { + if(!alarmed) + break; + + /* send heartbeat */ + y->nexthdr = 59; + y->opcode = OpNone; + if(ayiyaout(out, y, p, 0) < 0) + break; + + continue; + } + + ip = (Ip6hdr*)p; + + /* if not IPV6, drop it */ + if ((ip->vcf[0] & 0xF0) != IP_VER6) + continue; + + /* check length: drop if too short, trim if too long */ + m = nhgets(ip->ploadlen) + IPV6HDR_LEN; + if (m > n) + continue; + if (m < n) + n = m; + + /* drop if v6 source or destination address is naughty */ + if (badipv6(ip->src)) { + syslog(0, "ayiya", "egress filtered %I -> %I; bad src", + ip->src, ip->dst); + continue; + } + if ((!equivip6(ip->dst, remote6) && badipv6(ip->dst))) { + syslog(0, "ayiya", "egress filtered %I -> %I; " + "bad dst not remote", ip->src, ip->dst); + continue; + } + + if (debug > 1) + fprint(2, "v6 to tunnel %I -> %I\n", ip->src, ip->dst); + + /* pass packet to the other end of the tunnel */ + y->nexthdr = IP_IPV6PROTO; + y->opcode = OpForward; + if(ayiyaout(out, y, p, n) < 0 && !alarmed) + break; + } + + alarm(0); +} + +/* + * decapsulate v6 packets from the tunnel + * and forward them to the packet interface + */ +static void +tunnel2ip(int in, int out) +{ + uchar buf[2*AYIYAMAXHDR + IP_MAXPAY + 5], *p; + uchar a[IPaddrlen]; + Ip6hdr *op; + AYIYA y[1]; + int n, m; + + procsetname("tunnel %s %I -> v6 %I", outside, remote6, local6); + + for (;;) { + p = buf + AYIYAMAXHDR; /* space for reply header */ + + /* get a packet from the tunnel */ + if ((n = read(in, p, AYIYAMAXHDR + IP_MAXPAY)) <= 0) + break; + + /* zero slackspace */ + memset(p+n, 0, 5); + + m = ayiyaunpack(y, p, n); + if (m <= 0 || m > n) + continue; + + if (debug > 1) { + fprint(2, "recv: "); + ayiyadump(y); + } + + if (ayiyaverify(y, p, n) != 0) { + fprint(2, "ayiya bad packet signature\n"); + continue; + } + n -= m, p += m; + + switch(y->opcode){ + case OpForward: + case OpEchoRequest: + case OpEchoRequestAndForward: + break; + case OpMOTD: + fprint(2, "ayiya motd: %s\n", (char*)p); + continue; + case OpQueryRequest: + if(n < 4) + continue; + if (ayiyarquery((char*)p + 4) < 0) + continue; + n = 4 + strlen((char*)p + 4); + y->opcode = OpQueryResponse; + if (ayiyaout(in, y, p, n) < 0) + return; + continue; + case OpNone: + case OpEchoResponse: + case OpQueryResponse: + continue; + default: + fprint(2, "ayiya unknown opcode: %x\n", y->opcode); + continue; + } + + switch(y->opcode){ + case OpForward: + case OpEchoRequestAndForward: + /* if not IPv6 nor ICMPv6, drop it */ + if (y->nexthdr != IP_IPV6PROTO && y->nexthdr != IP_ICMPV6PROTO) { + syslog(0, "ayiya", + "dropping pkt from tunnel with inner proto %d", + y->nexthdr); + break; + } + + op = (Ip6hdr*)p; + if(n < IPV6HDR_LEN) + break; + + /* + * don't relay: just accept packets for local host/subnet + * (this blocks link-local and multicast addresses as well) + */ + maskip(op->dst, localmask, a); + if (!equivip6(a, localnet)) { + syslog(0, "ayiya", "ingress filtered %I -> %I; " + "dst not on local net", op->src, op->dst); + break; + } + if (debug > 1) + fprint(2, "tunnel to v6 %I -> %I\n", op->src, op->dst); + + /* pass V6 packet to the interface */ + if (write(out, p, n) != n) { + syslog(0, "ayiya", "error writing to packet interface (%r), giving up"); + return; + } + break; + } + + switch(y->opcode){ + case OpEchoRequest: + case OpEchoRequestAndForward: + y->opcode = OpEchoResponse; + if (ayiyaout(in, y, p, n) < 0) + return; + } + } +} + +static int +badipv4(uchar *a) +{ + switch (a[0]) { + case 0: /* unassigned */ + case 10: /* private */ + case 127: /* loopback */ + return 1; + case 172: + return a[1] >= 16; /* 172.16.0.0/12 private */ + case 192: + return a[1] == 168; /* 192.168.0.0/16 private */ + case 169: + return a[1] == 254; /* 169.254.0.0/16 DHCP link-local */ + } + /* 224.0.0.0/4 multicast, 240.0.0.0/4 reserved, broadcast */ + return a[0] >= 240; +} + +/* + * 0x0000/16 prefix = v4 compatible, v4 mapped, loopback, unspecified... + * site-local is now deprecated, rfc3879 + */ +static int +badipv6(uchar *a) +{ + int h = a[0]<<8 | a[1]; + + return h == 0 || ISIPV6MCAST(a) || ISIPV6LINKLOCAL(a) || + h == V6to4pfx && badipv4(a+2); +} diff --git a/sys/src/cmd/ip/mkfile b/sys/src/cmd/ip/mkfile index 8aaee46fa..71c4c6fbc 100644 --- a/sys/src/cmd/ip/mkfile +++ b/sys/src/cmd/ip/mkfile @@ -1,6 +1,7 @@