diff --git a/src/yggdrasil/ckr.go b/src/yggdrasil/ckr.go new file mode 100644 index 00000000..2a054711 --- /dev/null +++ b/src/yggdrasil/ckr.go @@ -0,0 +1,256 @@ +package yggdrasil + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "net" + "sort" +) + +// This module implements crypto-key routing, similar to Wireguard, where we +// allow traffic for non-Yggdrasil ranges to be routed over Yggdrasil. + +type cryptokey struct { + core *Core + enabled bool + ipv4routes []cryptokey_route + ipv6routes []cryptokey_route + ipv4cache map[address]cryptokey_route + ipv6cache map[address]cryptokey_route + ipv4sources []net.IPNet + ipv6sources []net.IPNet +} + +type cryptokey_route struct { + subnet net.IPNet + destination []byte +} + +// Initialise crypto-key routing. This must be done before any other CKR calls. +func (c *cryptokey) init(core *Core) { + c.core = core + c.ipv4routes = make([]cryptokey_route, 0) + c.ipv6routes = make([]cryptokey_route, 0) + c.ipv4cache = make(map[address]cryptokey_route, 0) + c.ipv6cache = make(map[address]cryptokey_route, 0) + c.ipv4sources = make([]net.IPNet, 0) + c.ipv6sources = make([]net.IPNet, 0) +} + +// Enable or disable crypto-key routing. +func (c *cryptokey) setEnabled(enabled bool) { + c.enabled = enabled +} + +// Check if crypto-key routing is enabled. +func (c *cryptokey) isEnabled() bool { + return c.enabled +} + +// Check whether the given address (with the address length specified in bytes) +// matches either the current node's address, the node's routed subnet or the +// list of subnets specified in IPv4Sources/IPv6Sources. +func (c *cryptokey) isValidSource(addr address, addrlen int) bool { + ip := net.IP(addr[:addrlen]) + + if addrlen == net.IPv6len { + // Does this match our node's address? + if bytes.Equal(addr[:16], c.core.router.addr[:16]) { + return true + } + + // Does this match our node's subnet? + if bytes.Equal(addr[:8], c.core.router.subnet[:8]) { + return true + } + } + + // Does it match a configured CKR source? + if c.isEnabled() { + // Build our references to the routing sources + var routingsources *[]net.IPNet + + // Check if the prefix is IPv4 or IPv6 + if addrlen == net.IPv6len { + routingsources = &c.ipv6sources + } else if addrlen == net.IPv4len { + routingsources = &c.ipv4sources + } else { + return false + } + + for _, subnet := range *routingsources { + if subnet.Contains(ip) { + return true + } + } + } + + // Doesn't match any of the above + return false +} + +// Adds a source subnet, which allows traffic with these source addresses to +// be tunnelled using crypto-key routing. +func (c *cryptokey) addSourceSubnet(cidr string) error { + // Is the CIDR we've been given valid? + _, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return err + } + + // Get the prefix length and size + _, prefixsize := ipnet.Mask.Size() + + // Build our references to the routing sources + var routingsources *[]net.IPNet + + // Check if the prefix is IPv4 or IPv6 + if prefixsize == net.IPv6len*8 { + routingsources = &c.ipv6sources + } else if prefixsize == net.IPv4len*8 { + routingsources = &c.ipv4sources + } else { + return errors.New("Unexpected prefix size") + } + + // Check if we already have this CIDR + for _, subnet := range *routingsources { + if subnet.String() == ipnet.String() { + return errors.New("Source subnet already configured") + } + } + + // Add the source subnet + *routingsources = append(*routingsources, *ipnet) + c.core.log.Println("Added CKR source subnet", cidr) + return nil +} + +// Adds a destination route for the given CIDR to be tunnelled to the node +// with the given BoxPubKey. +func (c *cryptokey) addRoute(cidr string, dest string) error { + // Is the CIDR we've been given valid? + ipaddr, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return err + } + + // Get the prefix length and size + _, prefixsize := ipnet.Mask.Size() + + // Build our references to the routing table and cache + var routingtable *[]cryptokey_route + var routingcache *map[address]cryptokey_route + + // Check if the prefix is IPv4 or IPv6 + if prefixsize == net.IPv6len*8 { + routingtable = &c.ipv6routes + routingcache = &c.ipv6cache + } else if prefixsize == net.IPv4len*8 { + routingtable = &c.ipv4routes + routingcache = &c.ipv4cache + } else { + return errors.New("Unexpected prefix size") + } + + // Is the route an Yggdrasil destination? + var addr address + var snet subnet + copy(addr[:], ipaddr) + copy(snet[:], ipnet.IP) + if addr.isValid() || snet.isValid() { + return errors.New("Can't specify Yggdrasil destination as crypto-key route") + } + // Do we already have a route for this subnet? + for _, route := range *routingtable { + if route.subnet.String() == ipnet.String() { + return errors.New(fmt.Sprintf("Route already exists for %s", cidr)) + } + } + // Decode the public key + if boxPubKey, err := hex.DecodeString(dest); err != nil { + return err + } else { + // Add the new crypto-key route + *routingtable = append(*routingtable, cryptokey_route{ + subnet: *ipnet, + destination: boxPubKey, + }) + + // Sort so most specific routes are first + sort.Slice(*routingtable, func(i, j int) bool { + im, _ := (*routingtable)[i].subnet.Mask.Size() + jm, _ := (*routingtable)[j].subnet.Mask.Size() + return im > jm + }) + + // Clear the cache as this route might change future routing + // Setting an empty slice keeps the memory whereas nil invokes GC + for k := range *routingcache { + delete(*routingcache, k) + } + + c.core.log.Println("Added CKR destination subnet", cidr) + return nil + } + + return errors.New("Unspecified error") +} + +// Looks up the most specific route for the given address (with the address +// length specified in bytes) from the crypto-key routing table. An error is +// returned if the address is not suitable or no route was found. +func (c *cryptokey) getPublicKeyForAddress(addr address, addrlen int) (boxPubKey, error) { + // Check if the address is a valid Yggdrasil address - if so it + // is exempt from all CKR checking + if addr.isValid() { + return boxPubKey{}, errors.New("Cannot look up CKR for Yggdrasil addresses") + } + + // Build our references to the routing table and cache + var routingtable *[]cryptokey_route + var routingcache *map[address]cryptokey_route + + // Check if the prefix is IPv4 or IPv6 + if addrlen == net.IPv6len { + routingtable = &c.ipv6routes + routingcache = &c.ipv6cache + } else if addrlen == net.IPv4len { + routingtable = &c.ipv4routes + routingcache = &c.ipv4cache + } else { + return boxPubKey{}, errors.New("Unexpected prefix size") + } + + // Check if there's a cache entry for this addr + if route, ok := (*routingcache)[addr]; ok { + var box boxPubKey + copy(box[:boxPubKeyLen], route.destination) + return box, nil + } + + // No cache was found - start by converting the address into a net.IP + ip := make(net.IP, addrlen) + copy(ip[:addrlen], addr[:]) + + // Check if we have a route. At this point c.ipv6routes should be + // pre-sorted so that the most specific routes are first + for _, route := range *routingtable { + // Does this subnet match the given IP? + if route.subnet.Contains(ip) { + // Cache the entry for future packets to get a faster lookup + (*routingcache)[addr] = route + + // Return the boxPubKey + var box boxPubKey + copy(box[:boxPubKeyLen], route.destination) + return box, nil + } + } + + // No route was found if we got to this point + return boxPubKey{}, errors.New(fmt.Sprintf("No route to %s", ip.String())) +} diff --git a/src/yggdrasil/config/config.go b/src/yggdrasil/config/config.go index bcf4f322..a14ece9d 100644 --- a/src/yggdrasil/config/config.go +++ b/src/yggdrasil/config/config.go @@ -4,8 +4,8 @@ package config type NodeConfig struct { Listen string `comment:"Listen address for peer connections. Default is to listen for all\nTCP connections over IPv4 and IPv6 with a random port."` AdminListen string `comment:"Listen address for admin connections Default is to listen for local\nconnections either on TCP/9001 or a UNIX socket depending on your\nplatform. Use this value for yggdrasilctl -endpoint=X."` - Peers []string `comment:"List of connection strings for static peers in URI format, i.e.\ntcp://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j."` - InterfacePeers map[string][]string `comment:"List of connection strings for static peers in URI format, arranged\nby source interface, i.e. { \"eth0\": [ tcp://a.b.c.d:e ] }. Note that\nSOCKS peerings will NOT be affected by this option and should go in\nthe \"Peers\" section instead."` + Peers []string `comment:"List of connection strings for static peers in URI format, e.g.\ntcp://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j."` + InterfacePeers map[string][]string `comment:"List of connection strings for static peers in URI format, arranged\nby source interface, e.g. { \"eth0\": [ tcp://a.b.c.d:e ] }. Note that\nSOCKS peerings will NOT be affected by this option and should go in\nthe \"Peers\" section instead."` ReadTimeout int32 `comment:"Read timeout for connections, specified in milliseconds. If less\nthan 6000 and not negative, 6000 (the default) is used. If negative,\nreads won't time out."` AllowedEncryptionPublicKeys []string `comment:"List of peer encryption public keys to allow or incoming TCP\nconnections from. If left empty/undefined then all connections\nwill be allowed by default."` EncryptionPublicKey string `comment:"Your public encryption key. Your peers may ask you for this to put\ninto their AllowedEncryptionPublicKeys configuration."` @@ -17,6 +17,7 @@ type NodeConfig struct { IfTAPMode bool `comment:"Set local network interface to TAP mode rather than TUN mode if\nsupported by your platform - option will be ignored if not."` IfMTU int `comment:"Maximux Transmission Unit (MTU) size for your local TUN/TAP interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."` SessionFirewall SessionFirewall `comment:"The session firewall controls who can send/receive network traffic\nto/from. This is useful if you want to protect this node without\nresorting to using a real firewall. This does not affect traffic\nbeing routed via this node to somewhere else. Rules are prioritised as\nfollows: blacklist, whitelist, always allow outgoing, direct, remote."` + TunnelRouting TunnelRouting `comment:"Allow tunneling non-Yggdrasil traffic over Yggdrasil. This effectively\nallows you to use Yggdrasil to route to, or to bridge other networks,\nsimilar to a VPN tunnel. Tunnelling works between any two nodes and\ndoes not require them to be directly peered."` //Net NetConfig `comment:"Extended options for connecting to peers over other networks."` } @@ -26,6 +27,7 @@ type NetConfig struct { I2P I2PConfig `comment:"Experimental options for configuring peerings over I2P."` } +// SessionFirewall controls the session firewall configuration type SessionFirewall struct { Enable bool `comment:"Enable or disable the session firewall. If disabled, network traffic\nfrom any node will be allowed. If enabled, the below rules apply."` AllowFromDirect bool `comment:"Allow network traffic from directly connected peers."` @@ -34,3 +36,12 @@ type SessionFirewall struct { WhitelistEncryptionPublicKeys []string `comment:"List of public keys from which network traffic is always accepted,\nregardless of AllowFromDirect or AllowFromRemote."` BlacklistEncryptionPublicKeys []string `comment:"List of public keys from which network traffic is always rejected,\nregardless of the whitelist, AllowFromDirect or AllowFromRemote."` } + +// TunnelRouting contains the crypto-key routing tables for tunneling +type TunnelRouting struct { + Enable bool `comment:"Enable or disable tunnel routing."` + IPv6Destinations map[string]string `comment:"IPv6 CIDR subnets, mapped to the EncryptionPublicKey to which they\nshould be routed, e.g. { \"aaaa:bbbb:cccc::/e\": \"boxpubkey\", ... }"` + IPv6Sources []string `comment:"Optional IPv6 source subnets which are allowed to be tunnelled in\naddition to this node's Yggdrasil address/subnet. If not\nspecified, only traffic originating from this node's Yggdrasil\naddress or subnet will be tunnelled."` + IPv4Destinations map[string]string `comment:"IPv4 CIDR subnets, mapped to the EncryptionPublicKey to which they\nshould be routed, e.g. { \"a.b.c.d/e\": \"boxpubkey\", ... }"` + IPv4Sources []string `comment:"IPv4 source subnets which are allowed to be tunnelled. Unlike for\nIPv6, this option is required for bridging IPv4 traffic. Only\ntraffic with a source matching these subnets will be tunnelled."` +} diff --git a/src/yggdrasil/core.go b/src/yggdrasil/core.go index 015147c4..2f60a1ba 100644 --- a/src/yggdrasil/core.go +++ b/src/yggdrasil/core.go @@ -121,6 +121,31 @@ func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error { return err } + c.router.cryptokey.setEnabled(nc.TunnelRouting.Enable) + if c.router.cryptokey.isEnabled() { + c.log.Println("Crypto-key routing enabled") + for ipv6, pubkey := range nc.TunnelRouting.IPv6Destinations { + if err := c.router.cryptokey.addRoute(ipv6, pubkey); err != nil { + panic(err) + } + } + for _, source := range nc.TunnelRouting.IPv6Sources { + if c.router.cryptokey.addSourceSubnet(source); err != nil { + panic(err) + } + } + for ipv4, pubkey := range nc.TunnelRouting.IPv4Destinations { + if err := c.router.cryptokey.addRoute(ipv4, pubkey); err != nil { + panic(err) + } + } + for _, source := range nc.TunnelRouting.IPv4Sources { + if c.router.cryptokey.addSourceSubnet(source); err != nil { + panic(err) + } + } + } + if err := c.admin.start(); err != nil { c.log.Println("Failed to start admin socket") return err diff --git a/src/yggdrasil/router.go b/src/yggdrasil/router.go index 86eb193c..fdcbb97f 100644 --- a/src/yggdrasil/router.go +++ b/src/yggdrasil/router.go @@ -23,6 +23,7 @@ package yggdrasil // The router then runs some sanity checks before passing it to the tun import ( + "bytes" "time" "golang.org/x/net/icmp" @@ -32,20 +33,23 @@ import ( // The router struct has channels to/from the tun/tap device and a self peer (0), which is how messages are passed between this node and the peers/switch layer. // The router's mainLoop goroutine is responsible for managing all information related to the dht, searches, and crypto sessions. type router struct { - core *Core - addr address - in <-chan []byte // packets we received from the network, link to peer's "out" - out func([]byte) // packets we're sending to the network, link to peer's "in" - recv chan<- []byte // place where the tun pulls received packets from - send <-chan []byte // place where the tun puts outgoing packets - reset chan struct{} // signal that coords changed (re-init sessions/dht) - admin chan func() // pass a lambda for the admin socket to query stuff + core *Core + addr address + subnet subnet + in <-chan []byte // packets we received from the network, link to peer's "out" + out func([]byte) // packets we're sending to the network, link to peer's "in" + recv chan<- []byte // place where the tun pulls received packets from + send <-chan []byte // place where the tun puts outgoing packets + reset chan struct{} // signal that coords changed (re-init sessions/dht) + admin chan func() // pass a lambda for the admin socket to query stuff + cryptokey cryptokey } // Initializes the router struct, which includes setting up channels to/from the tun/tap. func (r *router) init(core *Core) { r.core = core r.addr = *address_addrForNodeID(&r.core.dht.nodeID) + r.subnet = *address_subnetForNodeID(&r.core.dht.nodeID) in := make(chan []byte, 32) // TODO something better than this... p := r.core.peers.newPeer(&r.core.boxPub, &r.core.sigPub, &boxSharedKey{}, "(self)") p.out = func(packet []byte) { @@ -67,6 +71,7 @@ func (r *router) init(core *Core) { r.core.tun.send = send r.reset = make(chan struct{}, 1) r.admin = make(chan func()) + r.cryptokey.init(r.core) // go r.mainLoop() } @@ -117,30 +122,82 @@ func (r *router) mainLoop() { // If the session hasn't responded recently, it triggers a ping or search to keep things alive or deal with broken coords *relatively* quickly. // It also deals with oversized packets if there are MTU issues by calling into icmpv6.go to spoof PacketTooBig traffic, or DestinationUnreachable if the other side has their tun/tap disabled. func (r *router) sendPacket(bs []byte) { - if len(bs) < 40 { - panic("Tried to send a packet shorter than a header...") - } var sourceAddr address - var sourceSubnet subnet - copy(sourceAddr[:], bs[8:]) - copy(sourceSubnet[:], bs[8:]) - if !sourceAddr.isValid() && !sourceSubnet.isValid() { + var destAddr address + var destSnet subnet + var destPubKey *boxPubKey + var destNodeID *NodeID + var addrlen int + if bs[0]&0xf0 == 0x60 { + // Check if we have a fully-sized header + if len(bs) < 40 { + panic("Tried to send a packet shorter than an IPv6 header...") + } + // IPv6 address + addrlen = 16 + copy(sourceAddr[:addrlen], bs[8:]) + copy(destAddr[:addrlen], bs[24:]) + copy(destSnet[:addrlen/2], bs[24:]) + } else if bs[0]&0xf0 == 0x40 { + // Check if we have a fully-sized header + if len(bs) < 20 { + panic("Tried to send a packet shorter than an IPv4 header...") + } + // IPv4 address + addrlen = 4 + copy(sourceAddr[:addrlen], bs[12:]) + copy(destAddr[:addrlen], bs[16:]) + } else { + // Unknown address length return } - var dest address - copy(dest[:], bs[24:]) - var snet subnet - copy(snet[:], bs[24:]) - if !dest.isValid() && !snet.isValid() { + if !r.cryptokey.isValidSource(sourceAddr, addrlen) { + // The packet had a source address that doesn't belong to us or our + // configured crypto-key routing source subnets return } + if !destAddr.isValid() && !destSnet.isValid() { + // The addresses didn't match valid Yggdrasil node addresses so let's see + // whether it matches a crypto-key routing range instead + if key, err := r.cryptokey.getPublicKeyForAddress(destAddr, addrlen); err == nil { + // A public key was found, get the node ID for the search + destPubKey = &key + destNodeID = getNodeID(destPubKey) + // Do a quick check to ensure that the node ID refers to a vaild Yggdrasil + // address or subnet - this might be superfluous + addr := *address_addrForNodeID(destNodeID) + copy(destAddr[:], addr[:]) + copy(destSnet[:], addr[:]) + if !destAddr.isValid() && !destSnet.isValid() { + return + } + } else { + // No public key was found in the CKR table so we've exhausted our options + return + } + } doSearch := func(packet []byte) { var nodeID, mask *NodeID - if dest.isValid() { - nodeID, mask = dest.getNodeIDandMask() - } - if snet.isValid() { - nodeID, mask = snet.getNodeIDandMask() + switch { + case destNodeID != nil: + // We already know the full node ID, probably because it's from a CKR + // route in which the public key is known ahead of time + nodeID = destNodeID + var m NodeID + for i := range m { + m[i] = 0xFF + } + mask = &m + case destAddr.isValid(): + // We don't know the full node ID - try and use the address to generate + // a truncated node ID + nodeID, mask = destAddr.getNodeIDandMask() + case destSnet.isValid(): + // We don't know the full node ID - try and use the subnet to generate + // a truncated node ID + nodeID, mask = destSnet.getNodeIDandMask() + default: + return } sinfo, isIn := r.core.searches.searches[*nodeID] if !isIn { @@ -153,11 +210,11 @@ func (r *router) sendPacket(bs []byte) { } var sinfo *sessionInfo var isIn bool - if dest.isValid() { - sinfo, isIn = r.core.sessions.getByTheirAddr(&dest) + if destAddr.isValid() { + sinfo, isIn = r.core.sessions.getByTheirAddr(&destAddr) } - if snet.isValid() { - sinfo, isIn = r.core.sessions.getByTheirSubnet(&snet) + if destSnet.isValid() { + sinfo, isIn = r.core.sessions.getByTheirSubnet(&destSnet) } switch { case !isIn || !sinfo.init: @@ -186,6 +243,14 @@ func (r *router) sendPacket(bs []byte) { } fallthrough // Also send the packet default: + // If we know the public key ahead of time (i.e. a CKR route) then check + // if the session perm pub key matches before we send the packet to it + if destPubKey != nil { + if !bytes.Equal((*destPubKey)[:], sinfo.theirPermPub[:]) { + return + } + } + // Drop packets if the session MTU is 0 - this means that one or other // side probably has their TUN adapter disabled if sinfo.getMTU() == 0 { @@ -236,29 +301,55 @@ func (r *router) sendPacket(bs []byte) { // Don't continue - drop the packet return } + sinfo.send <- bs } } // Called for incoming traffic by the session worker for that connection. // Checks that the IP address is correct (matches the session) and passes the packet to the tun/tap. -func (r *router) recvPacket(bs []byte, theirAddr *address, theirSubnet *subnet) { +func (r *router) recvPacket(bs []byte, sinfo *sessionInfo) { // Note: called directly by the session worker, not the router goroutine if len(bs) < 24 { util_putBytes(bs) return } - var source address - copy(source[:], bs[8:]) + var sourceAddr address + var dest address var snet subnet - copy(snet[:], bs[8:]) - switch { - case source.isValid() && source == *theirAddr: - case snet.isValid() && snet == *theirSubnet: - default: + var addrlen int + if bs[0]&0xf0 == 0x60 { + // IPv6 address + addrlen = 16 + copy(sourceAddr[:addrlen], bs[8:]) + copy(dest[:addrlen], bs[24:]) + copy(snet[:addrlen/2], bs[24:]) + } else if bs[0]&0xf0 == 0x40 { + // IPv4 address + addrlen = 4 + copy(sourceAddr[:addrlen], bs[12:]) + copy(dest[:addrlen], bs[16:]) + } else { + // Unknown address length + return + } + // Check that the packet is destined for either our Yggdrasil address or + // subnet, or that it matches one of the crypto-key routing source routes + if !r.cryptokey.isValidSource(dest, addrlen) { util_putBytes(bs) return } + // See whether the packet they sent should have originated from this session + switch { + case sourceAddr.isValid() && sourceAddr == sinfo.theirAddr: + case snet.isValid() && snet == sinfo.theirSubnet: + default: + key, err := r.cryptokey.getPublicKeyForAddress(sourceAddr, addrlen) + if err != nil || key != sinfo.theirPermPub { + util_putBytes(bs) + return + } + } //go func() { r.recv<-bs }() r.recv <- bs } diff --git a/src/yggdrasil/session.go b/src/yggdrasil/session.go index 0bc27a12..0e587d52 100644 --- a/src/yggdrasil/session.go +++ b/src/yggdrasil/session.go @@ -589,5 +589,5 @@ func (sinfo *sessionInfo) doRecv(p *wire_trafficPacket) { sinfo.updateNonce(&p.Nonce) sinfo.time = time.Now() sinfo.bytesRecvd += uint64(len(bs)) - sinfo.core.router.recvPacket(bs, &sinfo.theirAddr, &sinfo.theirSubnet) + sinfo.core.router.recvPacket(bs, sinfo) } diff --git a/src/yggdrasil/tun.go b/src/yggdrasil/tun.go index cbbcdea7..1987c2d2 100644 --- a/src/yggdrasil/tun.go +++ b/src/yggdrasil/tun.go @@ -101,10 +101,10 @@ func (tun *tunDevice) read() error { if tun.iface.IsTAP() { o = tun_ETHER_HEADER_LENGTH } - if buf[o]&0xf0 != 0x60 || - n != 256*int(buf[o+4])+int(buf[o+5])+tun_IPv6_HEADER_LENGTH+o { - // Either not an IPv6 packet or not the complete packet for some reason - //panic("Should not happen in testing") + switch { + case buf[o]&0xf0 == 0x60 && n == 256*int(buf[o+4])+int(buf[o+5])+tun_IPv6_HEADER_LENGTH+o: + case buf[o]&0xf0 == 0x40 && n == 256*int(buf[o+2])+int(buf[o+3])+o: + default: continue } if buf[o+6] == 58 {