diff --git a/cmd/yggdrasilctl/main.go b/cmd/yggdrasilctl/main.go index b37c2561..f09e5d86 100644 --- a/cmd/yggdrasilctl/main.go +++ b/cmd/yggdrasilctl/main.go @@ -256,6 +256,13 @@ func main() { if buildversion, ok := v.(map[string]interface{})["build_version"].(string); ok && buildversion != "unknown" { fmt.Println("Build version:", buildversion) } + if friendlyname, ok := v.(map[string]interface{})["friendly_name"].(string); ok { + if friendlyname == "" { + fmt.Println("Friendly name: (none)") + } else { + fmt.Println("Friendly name:", friendlyname) + } + } fmt.Println("IPv6 address:", k) if subnet, ok := v.(map[string]interface{})["subnet"].(string); ok { fmt.Println("IPv6 subnet:", subnet) diff --git a/src/config/config.go b/src/config/config.go index 9671eca3..18950a4f 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -2,6 +2,7 @@ package config // NodeConfig defines all configuration values needed to run a signle yggdrasil node type NodeConfig struct { + Metadata Metadata `comment:"Optional node metadata. Entirely optional but visible to all\npeers and nodes with open sessions."` 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. To disable\nthe admin socket, use the value \"none\" 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."` @@ -51,3 +52,10 @@ type TunnelRouting struct { type SwitchOptions struct { MaxTotalQueueSize uint64 `comment:"Maximum size of all switch queues combined (in bytes)."` } + +// Optional metadata - format subject to change +type Metadata struct { + Name string + Location string + Contact string +} diff --git a/src/yggdrasil/admin.go b/src/yggdrasil/admin.go index 266d5a89..332fa1ce 100644 --- a/src/yggdrasil/admin.go +++ b/src/yggdrasil/admin.go @@ -574,6 +574,9 @@ func (a *admin) getData_getSelf() *admin_nodeInfo { {"ip", a.core.GetAddress().String()}, {"subnet", a.core.GetSubnet().String()}, {"coords", fmt.Sprint(coords)}, + {"name", a.core.metadata.name}, + {"location", a.core.metadata.location}, + {"contact", a.core.metadata.contact}, } if name := GetBuildName(); name != "unknown" { self = append(self, admin_pair{"build_name", name}) diff --git a/src/yggdrasil/core.go b/src/yggdrasil/core.go index 5ab91a74..06a3cb31 100644 --- a/src/yggdrasil/core.go +++ b/src/yggdrasil/core.go @@ -23,6 +23,7 @@ type Core struct { boxPriv boxPrivKey sigPub sigPubKey sigPriv sigPrivKey + metadata metadata switchTable switchTable peers peers sessions sessions @@ -40,7 +41,8 @@ type Core struct { func (c *Core) init(bpub *boxPubKey, bpriv *boxPrivKey, spub *sigPubKey, - spriv *sigPrivKey) { + spriv *sigPrivKey, + metadata metadata) { // TODO separate init and start functions // Init sets up structs // Start launches goroutines that depend on structs being set up @@ -51,6 +53,7 @@ func (c *Core) init(bpub *boxPubKey, } c.boxPub, c.boxPriv = *bpub, *bpriv c.sigPub, c.sigPriv = *spub, *spriv + c.metadata = metadata c.admin.core = c c.searches.init(c) c.dht.init(c) @@ -80,6 +83,11 @@ func GetBuildVersion() string { return buildVersion } +// Gets the friendly name of this node, as specified in the NodeConfig. +func (c *Core) GetMeta() metadata { + return c.metadata +} + // Starts up Yggdrasil using the provided NodeConfig, and outputs debug logging // through the provided log.Logger. The started stack will include TCP and UDP // sockets, a multicast discovery socket, an admin socket, router, switch and @@ -121,7 +129,13 @@ func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error { copy(sigPub[:], sigPubHex) copy(sigPriv[:], sigPrivHex) - c.init(&boxPub, &boxPriv, &sigPub, &sigPriv) + meta := metadata{ + name: nc.Metadata.Name, + location: nc.Metadata.Location, + contact: nc.Metadata.Contact, + } + + c.init(&boxPub, &boxPriv, &sigPub, &sigPriv, meta) c.admin.init(c, nc.AdminListen) if err := c.tcp.init(c, nc.Listen, nc.ReadTimeout); err != nil { diff --git a/src/yggdrasil/debug.go b/src/yggdrasil/debug.go index e463518d..9a56e280 100644 --- a/src/yggdrasil/debug.go +++ b/src/yggdrasil/debug.go @@ -50,7 +50,7 @@ func StartProfiler(log *log.Logger) error { func (c *Core) Init() { bpub, bpriv := newBoxKeys() spub, spriv := newSigKeys() - c.init(bpub, bpriv, spub, spriv) + c.init(bpub, bpriv, spub, spriv, metadata{}) c.switchTable.start() c.router.start() } @@ -84,7 +84,7 @@ func (c *Core) DEBUG_getPeers() *peers { func (ps *peers) DEBUG_newPeer(box boxPubKey, sig sigPubKey, link boxSharedKey) *peer { //in <-chan []byte, //out chan<- []byte) *peer { - return ps.newPeer(&box, &sig, &link, "(simulator)") //, in, out) + return ps.newPeer(&box, &sig, &link, "(simulator)", metadata{}) //, in, out) } /* @@ -356,7 +356,7 @@ func (c *Core) DEBUG_init(bpub []byte, copy(boxPriv[:], bpriv) copy(sigPub[:], spub) copy(sigPriv[:], spriv) - c.init(&boxPub, &boxPriv, &sigPub, &sigPriv) + c.init(&boxPub, &boxPriv, &sigPub, &sigPriv, metadata{}) if err := c.router.start(); err != nil { panic(err) diff --git a/src/yggdrasil/metadata.go b/src/yggdrasil/metadata.go new file mode 100644 index 00000000..c5243087 --- /dev/null +++ b/src/yggdrasil/metadata.go @@ -0,0 +1,7 @@ +package yggdrasil + +type metadata struct { + name string + location string + contact string +} diff --git a/src/yggdrasil/peer.go b/src/yggdrasil/peer.go index 67aa805a..4a81cb83 100644 --- a/src/yggdrasil/peer.go +++ b/src/yggdrasil/peer.go @@ -79,30 +79,31 @@ type peer struct { bytesSent uint64 // To track bandwidth usage for getPeers bytesRecvd uint64 // To track bandwidth usage for getPeers // BUG: sync/atomic, 32 bit platforms need the above to be the first element - core *Core - port switchPort - box boxPubKey - sig sigPubKey - shared boxSharedKey - linkShared boxSharedKey - endpoint string - friendlyName string - firstSeen time.Time // To track uptime for getPeers - linkOut (chan []byte) // used for protocol traffic (to bypass queues) - doSend (chan struct{}) // tell the linkLoop to send a switchMsg - dinfo *dhtInfo // used to keep the DHT working - out func([]byte) // Set up by whatever created the peers struct, used to send packets to other nodes - close func() // Called when a peer is removed, to close the underlying connection, or via admin api + core *Core + port switchPort + box boxPubKey + sig sigPubKey + shared boxSharedKey + linkShared boxSharedKey + endpoint string + metadata metadata + firstSeen time.Time // To track uptime for getPeers + linkOut (chan []byte) // used for protocol traffic (to bypass queues) + doSend (chan struct{}) // tell the linkLoop to send a switchMsg + dinfo *dhtInfo // used to keep the DHT working + out func([]byte) // Set up by whatever created the peers struct, used to send packets to other nodes + close func() // Called when a peer is removed, to close the underlying connection, or via admin api } // Creates a new peer with the specified box, sig, and linkShared keys, using the lowest unocupied port number. -func (ps *peers) newPeer(box *boxPubKey, sig *sigPubKey, linkShared *boxSharedKey, endpoint string) *peer { +func (ps *peers) newPeer(box *boxPubKey, sig *sigPubKey, linkShared *boxSharedKey, endpoint string, metadata metadata) *peer { now := time.Now() p := peer{box: *box, sig: *sig, shared: *getSharedKey(&ps.core.boxPriv, box), linkShared: *linkShared, endpoint: endpoint, + metadata: metadata, firstSeen: now, doSend: make(chan struct{}, 1), core: ps.core} diff --git a/src/yggdrasil/router.go b/src/yggdrasil/router.go index b4824767..2df359df 100644 --- a/src/yggdrasil/router.go +++ b/src/yggdrasil/router.go @@ -58,7 +58,7 @@ func (r *router) init(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 := r.core.peers.newPeer(&r.core.boxPub, &r.core.sigPub, &boxSharedKey{}, "(self)", r.core.metadata) p.out = func(packet []byte) { // This is to make very sure it never blocks select { @@ -428,6 +428,10 @@ func (r *router) handleProto(packet []byte) { r.handlePing(bs, &p.FromKey) case wire_SessionPong: r.handlePong(bs, &p.FromKey) + case wire_SessionMetaRequest: + fallthrough + case wire_SessionMetaResponse: + r.handleMeta(bs, &p.FromKey) case wire_DHTLookupRequest: r.handleDHTReq(bs, &p.FromKey) case wire_DHTLookupResponse: @@ -472,6 +476,17 @@ func (r *router) handleDHTRes(bs []byte, fromKey *boxPubKey) { r.core.dht.handleRes(&res) } +// Decodes meta request +func (r *router) handleMeta(bs []byte, fromKey *boxPubKey) { + req := sessionMeta{} + if !req.decode(bs) { + return + } + req.SendPermPub = *fromKey + r.core.log.Printf("handleMeta: %+v\n", req) + r.core.sessions.handleMeta(&req) +} + // Passed a function to call. // This will send the function to r.admin and block until it finishes. // It's used by the admin socket to ask the router mainLoop goroutine about information in the session or dht structs, which cannot be read safely from outside that goroutine. diff --git a/src/yggdrasil/session.go b/src/yggdrasil/session.go index 92ae262b..e088726e 100644 --- a/src/yggdrasil/session.go +++ b/src/yggdrasil/session.go @@ -13,34 +13,37 @@ import ( // All the information we know about an active session. // This includes coords, permanent and ephemeral keys, handles and nonces, various sorts of timing information for timeout and maintenance, and some metadata for the admin API. type sessionInfo struct { - core *Core - theirAddr address - theirSubnet subnet - theirPermPub boxPubKey - theirSesPub boxPubKey - mySesPub boxPubKey - mySesPriv boxPrivKey - sharedSesKey boxSharedKey // derived from session keys - theirHandle handle - myHandle handle - theirNonce boxNonce - myNonce boxNonce - theirMTU uint16 - myMTU uint16 - wasMTUFixed bool // Was the MTU fixed by a receive error? - time time.Time // Time we last received a packet - coords []byte // coords of destination - packet []byte // a buffered packet, sent immediately on ping/pong - init bool // Reset if coords change - send chan []byte - recv chan *wire_trafficPacket - nonceMask uint64 - tstamp int64 // tstamp from their last session ping, replay attack mitigation - mtuTime time.Time // time myMTU was last changed - pingTime time.Time // time the first ping was sent since the last received packet - pingSend time.Time // time the last ping was sent - bytesSent uint64 // Bytes of real traffic sent in this session - bytesRecvd uint64 // Bytes of real traffic received in this session + core *Core + theirAddr address + theirSubnet subnet + theirPermPub boxPubKey + theirSesPub boxPubKey + mySesPub boxPubKey + mySesPriv boxPrivKey + sharedSesKey boxSharedKey // derived from session keys + theirHandle handle + myHandle handle + theirNonce boxNonce + myNonce boxNonce + metaReqTime time.Time + metaResTime time.Time + theirMetadata metadata + theirMTU uint16 + myMTU uint16 + wasMTUFixed bool // Was the MTU fixed by a receive error? + time time.Time // Time we last received a packet + coords []byte // coords of destination + packet []byte // a buffered packet, sent immediately on ping/pong + init bool // Reset if coords change + send chan []byte + recv chan *wire_trafficPacket + nonceMask uint64 + tstamp int64 // tstamp from their last session ping, replay attack mitigation + mtuTime time.Time // time myMTU was last changed + pingTime time.Time // time the first ping was sent since the last received packet + pingSend time.Time // time the last ping was sent + bytesSent uint64 // Bytes of real traffic sent in this session + bytesRecvd uint64 // Bytes of real traffic received in this session } // Represents a session ping/pong packet, andincludes information like public keys, a session handle, coords, a timestamp to prevent replays, and the tun/tap MTU. @@ -54,6 +57,13 @@ type sessionPing struct { MTU uint16 } +// Represents a session metadata packet. +type sessionMeta struct { + SendPermPub boxPubKey // Sender's permanent key + IsResponse bool + Metadata metadata +} + // Updates session info in response to a ping, after checking that the ping is OK. // Returns true if the session was updated, or false otherwise. func (s *sessionInfo) update(p *sessionPing) bool { @@ -466,6 +476,66 @@ func (ss *sessions) handlePing(ping *sessionPing) { bs, sinfo.packet = sinfo.packet, nil ss.core.router.sendPacket(bs) } + if time.Since(sinfo.metaResTime).Minutes() > 15 { + if time.Since(sinfo.metaReqTime).Minutes() > 1 { + ss.sendMeta(sinfo, false) + } + } +} + +func (ss *sessions) sendMeta(sinfo *sessionInfo, isResponse bool) { + meta := sessionMeta{ + IsResponse: isResponse, + Metadata: metadata{ + name: "some.name.com", //[]byte(ss.core.friendlyName)[0:len(ss.core.friendlyName):32], + location: "Some Place", + contact: "someone@somewhere.com", + }, + } + bs := meta.encode() + shared := ss.getSharedKey(&ss.core.boxPriv, &sinfo.theirPermPub) + payload, nonce := boxSeal(shared, bs, nil) + p := wire_protoTrafficPacket{ + Coords: sinfo.coords, + ToKey: sinfo.theirPermPub, + FromKey: ss.core.boxPub, + Nonce: *nonce, + Payload: payload, + } + packet := p.encode() + ss.core.router.out(packet) + if isResponse { + ss.core.log.Println("Sent meta response to", sinfo.theirAddr) + } else { + ss.core.log.Println("Sent meta request to", sinfo.theirAddr) + sinfo.metaReqTime = time.Now() + } +} + +// Handles a meta request/response. +func (ss *sessions) handleMeta(meta *sessionMeta) { + // Get the corresponding session (or create a new session) + sinfo, isIn := ss.getByTheirPerm(&meta.SendPermPub) + // Check the session firewall + if !isIn && ss.sessionFirewallEnabled { + if !ss.isSessionAllowed(&meta.SendPermPub, false) { + return + } + } + if !isIn || sinfo.timedout() { + return + } + if meta.IsResponse { + ss.core.log.Println("Received meta response", string(meta.Metadata.name), "from", sinfo.theirAddr) + sinfo.theirMetadata = meta.Metadata + sinfo.metaResTime = time.Now() + ss.core.log.Println("- name:", meta.Metadata.name) + ss.core.log.Println("- contact:", meta.Metadata.contact) + ss.core.log.Println("- location:", meta.Metadata.location) + } else { + ss.core.log.Println("Received meta request", string(meta.Metadata.name), "from", sinfo.theirAddr) + ss.sendMeta(sinfo, true) + } } // Used to subtract one nonce from another, staying in the range +- 64. diff --git a/src/yggdrasil/tcp.go b/src/yggdrasil/tcp.go index 5ca66304..dc1e2b16 100644 --- a/src/yggdrasil/tcp.go +++ b/src/yggdrasil/tcp.go @@ -287,7 +287,7 @@ func (iface *tcpInterface) handler(sock net.Conn, incoming bool) { }() // Note that multiple connections to the same node are allowed // E.g. over different interfaces - p := iface.core.peers.newPeer(&info.box, &info.sig, getSharedKey(myLinkPriv, &meta.link), sock.RemoteAddr().String()) + p := iface.core.peers.newPeer(&info.box, &info.sig, getSharedKey(myLinkPriv, &meta.link), sock.RemoteAddr().String(), metadata{}) p.linkOut = make(chan []byte, 1) in := func(bs []byte) { p.handlePacket(bs) diff --git a/src/yggdrasil/wire.go b/src/yggdrasil/wire.go index d05624e2..fd898a6c 100644 --- a/src/yggdrasil/wire.go +++ b/src/yggdrasil/wire.go @@ -16,6 +16,8 @@ const ( wire_SessionPong // inside protocol traffic header wire_DHTLookupRequest // inside protocol traffic header wire_DHTLookupResponse // inside protocol traffic header + wire_SessionMetaRequest // inside protocol traffic header + wire_SessionMetaResponse // inside protocol traffic header ) // Calls wire_put_uint64 on a nil slice. @@ -353,6 +355,48 @@ func (p *sessionPing) decode(bs []byte) bool { //////////////////////////////////////////////////////////////////////////////// +// Encodes a sessionPing into its wire format. +func (p *sessionMeta) encode() []byte { + var pTypeVal uint64 + if p.IsResponse { + pTypeVal = wire_SessionMetaResponse + } else { + pTypeVal = wire_SessionMetaRequest + } + bs := wire_encode_uint64(pTypeVal) + if p.IsResponse { + bs = append(bs, p.Metadata.name...) + bs = append(bs, p.Metadata.location...) + bs = append(bs, p.Metadata.contact...) + } + return bs +} + +// Decodes an encoded sessionPing into the struct, returning true if successful. +func (p *sessionMeta) decode(bs []byte) bool { + var pType uint64 + switch { + case !wire_chop_uint64(&pType, &bs): + return false + case pType != wire_SessionMetaRequest && pType != wire_SessionMetaResponse: + return false + } + p.IsResponse = pType == wire_SessionMetaResponse + if p.IsResponse { + switch { + case !wire_chop_slice([]byte(p.Metadata.name), &bs): + return false + case !wire_chop_slice([]byte(p.Metadata.location), &bs): + return false + case !wire_chop_slice([]byte(p.Metadata.contact), &bs): + return false + } + } + return true +} + +//////////////////////////////////////////////////////////////////////////////// + // Encodes a dhtReq into its wire format. func (r *dhtReq) encode() []byte { coords := wire_encode_coords(r.Coords)