Browse Source

A bunch of tweaks around CLI parameters and string formats of things.

Adam Ierymenko 5 years ago
parent
commit
5dac2e82a7

+ 56 - 53
go/cmd/zerotier/cli/help.go

@@ -26,64 +26,67 @@ Licensed under the ZeroTier BSL (see LICENSE.txt)
 Usage: zerotier [-options] <command> [command args]
 
 Global Options:
-  -j                                    Output raw JSON where applicable
-  -p <path>                             Use alternate base path
-  -t <path>                             Load secret auth token from a file
-  -T <token>                            Set secret auth token on command line
+  -j                                     Output raw JSON where applicable
+  -p <path>                              Use alternate base path
+  -t <path>                              Load secret auth token from a file
+  -T <token>                             Set secret auth token on command line
 
 Commands:
-  help                                  Show this help
-  version                               Print version
-  service                               Start as service
-  status                                Show node status, identity, and config
-  peers                                 List all VL1 peers
-  join <network> [fingerprint]          Join a virtual network
-  leave <network>                       Leave a virtual network
-  networks                              List VL2 virtual networks
-  network <network>                     Show verbose network info
-  set <network> [option] [value]        Get or set a network config option
-    manageips <boolean>                 Is IP management allowed?
-    manageroutes <boolean>              Is route management allowed?
-    globalips <boolean>                 Allow assignment of global IPs?
-    globalroutes <boolean>              Can global IP space routes be set?
-    defaultroute <boolean>              Can default route be overridden?
-  set [option] [value]                  Get or set a service config option
-    port <port>                         Primary P2P port
-    secondaryport <port/0>              Secondary P2P port (0 to disable)
-    blacklist cidr <IP/bits> <boolean>  Toggle physical path blacklisting
-    blacklist if <prefix> <boolean>     Toggle interface prefix blacklisting
-    portmap <boolean>                   Toggle use of uPnP or NAT-PMP
-  controller <command> [option]         Local controller management commands
-    networks                            List networks run by local controller
-    new                                 Create a new network
-    set <network> [setting] [value]     Show or modify network settings
-    members <network>                   List members of a network
-    member <network> [setting] [value]  Show or modify member level settings
-    auth <address|fingerprint>          Authorize a peer
-    deauth <address|fingerprint>        Deauthorize a peer
-  identity <command> [args]             Identity management commands
-    new [c25519|p384]                   Create identity pair (default: c25519)
-    getpublic <identity>                Extract only public part of identity
-    validate <identity>                 Locally validate an identity
-    sign <identity> <file>              Sign a file with an identity's key
-    verify <identity> <file> <sig>      Verify a signature
-  locator <command> [args]              Locator management commands
-    new <identity> <address> [...]      Create and sign a new locator
-    show <locator> [identity]           Show locator information
-  root [command]                        Root management commands
-    list                                List root peers (same as no command)
-    add <identity> <endpoint|locator>   Designate a peer as a root
-    remove <address>                    Un-designate a peer as a root
+  help                                   Show this help
+  version                                Print version
+  service                                Start as service
+  status                                 Show node status and configuration
+  join <network> [fingerprint]           Join a virtual network
+  leave <network>                        Leave a virtual network
+  networks                               List VL2 virtual networks
+  network <network> [command] [option] - Network management commands
+    show                                 Show network details (default)
+    set [option] [value]               - Get or set network options
+      manageips <boolean>                Is IP management allowed?
+      manageroutes <boolean>             Is route management allowed?
+      globalips <boolean>                Allow assignment of global IPs?
+      globalroutes <boolean>             Can global IP space routes be set?
+      defaultroute <boolean>             Can default route be overridden?
+  peers                                  List VL1 peers
+  peer <address> [command] [option]    - Peer management commands
+    show                                 Show peer details (default)
+    try <endpoint> [...]                 Try peer at explicit endpoint
+  roots                                  List root peers
+  root [command]                       - Root management commands
+    add <identity> [endpoint]            Designate a peer as a root
+    remove <address>                     Un-designate a peer as a root
+  set [option] [value]                 - Get or set a core config option
+    port <port>                          Primary P2P port
+    secondaryport <port/0>               Secondary P2P port (0 to disable)
+    blacklist cidr <IP/bits> <boolean>   Toggle physical path blacklisting
+    blacklist if <prefix> <boolean>      Toggle interface prefix blacklisting
+    portmap <boolean>                    Toggle use of uPnP or NAT-PMP
+  controller <command> [option]        - Local controller management commands
+    networks                             List networks run by local controller
+    new                                  Create a new network
+    set <network> [setting] [value]      Show or modify network settings
+    members <network>                    List members of a network
+    member <network> [setting] [value]   Show or modify member level settings
+    auth <address>                       Authorize a peer
+    deauth <address>                     Deauthorize a peer
+  identity <command> [args]            - Identity management commands
+    new [c25519|p384]                    Create identity (default: c25519)
+    getpublic <identity>                 Extract only public part of identity
+    fingerprint <identity>               Get an identity's fingerprint
+    validate <identity>                  Locally validate an identity
+    sign <identity> <file>               Sign a file with an identity's key
+    verify <identity> <file> <sig>       Verify a signature
 
 The 'service' command does not exit until the service receives a signal.
-This is typically run from launchd (Mac), systemd or init (Linux), a Windows
-service harness (Windows), etc.
 
-If 'set' is followed by a 16-digit hex number it will get/set network config
-options. Otherwise it will get/set local options that pertain to the entire
-node.
+An <address> may be specified as a 10-digit short ZeroTier address, a
+fingerprint containing both an address and a SHA384 hash, or an identity.
+The latter two options are equivalent in terms of specificity and may be
+used if stronger security guarantees are desired than those provided by
+the basic ZeroTier addressing system. Fields of type <identity> must be
+full identities and may be specified either verbatim or as a path to a file.
 
-Identities can be specified verbatim on the command line or as a path to
-a file. This is detected automatically.
+An <endpoint> is a place where a peer may be reached. Currently these are
+just 'IP/port' format addresses but other types may be added in the future.
 `,zerotier.CoreVersionMajor, zerotier.CoreVersionMinor, zerotier.CoreVersionRevision)
 }

+ 9 - 2
go/cmd/zerotier/cli/identity.go

@@ -35,8 +35,9 @@ func Identity(args []string) {
 					os.Exit(1)
 				}
 				switch args[1] {
-				case "c25519":
-				case "p384":
+				case "c25519", "C25519", "0":
+					idType = zerotier.IdentityTypeC25519
+				case "p384", "P384", "1":
 					idType = zerotier.IdentityTypeP384
 				default:
 					Help()
@@ -57,6 +58,12 @@ func Identity(args []string) {
 				os.Exit(0)
 			}
 
+		case "fingerprint":
+			if len(args) == 2 {
+				fmt.Println(readIdentity(args[1]).Fingerprint().String())
+				os.Exit(0)
+			}
+
 		case "validate":
 			if len(args) == 2 {
 				if readIdentity(args[1]).LocallyValidate() {

+ 14 - 4
go/cmd/zerotier/cli/join.go

@@ -17,6 +17,7 @@ import (
 	"fmt"
 	"os"
 	"strconv"
+	"strings"
 
 	"zerotier/pkg/zerotier"
 )
@@ -41,10 +42,19 @@ func Join(basePath, authToken string, args []string) {
 
 	var fp *zerotier.Fingerprint
 	if len(args) == 2 {
-		fp, err = zerotier.NewFingerprintFromString(args[1])
-		if err != nil {
-			fmt.Printf("ERROR: invalid network controller fingerprint: %s\n", args[1])
-			os.Exit(1)
+		if strings.ContainsRune(args[1], '-') {
+			fp, err = zerotier.NewFingerprintFromString(args[1])
+			if err != nil {
+				fmt.Printf("ERROR: invalid network controller fingerprint: %s\n", args[1])
+				os.Exit(1)
+			}
+		} else {
+			id, err := zerotier.NewIdentityFromString(args[1])
+			if err != nil {
+				fmt.Printf("ERROR: invalid network controller identity: %s\n", args[1])
+				os.Exit(1)
+			}
+			fp = id.Fingerprint()
 		}
 	}
 

+ 0 - 83
go/cmd/zerotier/cli/locator.go

@@ -1,83 +0,0 @@
-/*
- * Copyright (c)2013-2020 ZeroTier, Inc.
- *
- * Use of this software is governed by the Business Source License included
- * in the LICENSE.TXT file in the project's root directory.
- *
- * Change Date: 2024-01-01
- *
- * On the date above, in accordance with the Business Source License, use
- * of this software will be governed by version 2.0 of the Apache License.
- */
-/****/
-
-package cli
-
-import (
-	"fmt"
-	"os"
-	"time"
-	"zerotier/pkg/zerotier"
-)
-
-func Locator(args []string) {
-	if len(args) > 0 {
-		switch args[0] {
-
-		case "new":
-			if len(args) >= 3 {
-				id := readIdentity(args[1])
-				if !id.HasPrivate() {
-					fmt.Println("ERROR: identity is missing private key and can't be used to sign a locator.")
-					os.Exit(1)
-				}
-				var eps []zerotier.Endpoint
-				for i:=2;i<len(args);i++ {
-					ep, _ := zerotier.NewEndpointFromString(args[i])
-					if ep != nil {
-						eps = append(eps, *ep)
-					}
-				}
-				loc, err := zerotier.NewLocator(zerotier.TimeMs(),eps,id)
-				if err != nil {
-					fmt.Printf("ERROR: unable to create or sign locator: %s\n",err.Error())
-					os.Exit(1)
-				}
-				fmt.Println(loc.String())
-				os.Exit(0)
-			}
-
-		case "show":
-			if len(args) > 1 && len(args) < 4 {
-				loc := readLocator(args[1])
-				var id *zerotier.Identity
-				if len(args) == 3 {
-					id = readIdentity(args[2])
-				}
-				ts, fp, eps, valid, _ := loc.GetInfo(id)
-				fmt.Printf("%s\n  Timestamp: %s (%d)\n  Validity: ",fp.String(),time.Unix(ts / 1000,ts * 1000).String(),ts)
-				if id == nil {
-					fmt.Printf("(no identity provided)\n")
-				} else {
-					if valid {
-						fmt.Printf("SIGNATURE VERIFIED\n")
-					} else {
-						fmt.Printf("! INVALID SIGNATURE\n")
-					}
-				}
-				fmt.Print("  Endpoints: ")
-				for i := range eps {
-					if i > 0 {
-						fmt.Print(" ")
-					}
-					fmt.Print(eps[i].String())
-				}
-				fmt.Printf("\n")
-			}
-
-		}
-
-	}
-	Help()
-	os.Exit(1)
-}

+ 9 - 0
go/cmd/zerotier/cli/root.go

@@ -14,4 +14,13 @@
 package cli
 
 func Root(basePath, authToken string, args []string, jsonOutput bool) {
+	if len(args) > 0 {
+		switch args[0] {
+
+		case "add":
+
+		case "remove":
+
+		}
+	}
 }

+ 0 - 2
go/cmd/zerotier/zerotier.go

@@ -144,8 +144,6 @@ func main() {
 		cli.Set(basePath, authToken, cmdArgs)
 	case "identity":
 		cli.Identity(cmdArgs)
-	case "locator":
-		cli.Locator(cmdArgs)
 	case "root":
 		authTokenRequired(authToken)
 		cli.Root(basePath, authToken, cmdArgs, *jflag)

+ 2 - 2
go/pkg/zerotier/endpoint.go

@@ -21,7 +21,7 @@ const (
 	EndpointTypeIp         = C.ZT_ENDPOINT_TYPE_IP
 	EndpointTypeIpUdp      = C.ZT_ENDPOINT_TYPE_IP_UDP
 	EndpointTypeIpTcp      = C.ZT_ENDPOINT_TYPE_IP_TCP
-	EndpointTypeIpHttp2    = C.ZT_ENDPOINT_TYPE_IP_HTTP2
+	EndpointTypeIpHttp     = C.ZT_ENDPOINT_TYPE_IP_HTTP
 )
 
 type Endpoint struct {
@@ -65,7 +65,7 @@ func (ep *Endpoint) Type() int {
 // InetAddress gets this Endpoint as an InetAddress or nil if its type is not addressed by one.
 func (ep *Endpoint) InetAddress() *InetAddress {
 	switch ep.cep._type {
-	case EndpointTypeIp, EndpointTypeIpUdp, EndpointTypeIpTcp, EndpointTypeIpHttp2:
+	case EndpointTypeIp, EndpointTypeIpUdp, EndpointTypeIpTcp, EndpointTypeIpHttp:
 		ua := sockaddrStorageToUDPAddr(C._getSS(&ep.cep))
 		return &InetAddress{IP: ua.IP, Port: ua.Port}
 	}

+ 6 - 0
go/pkg/zerotier/identity.go

@@ -166,6 +166,12 @@ func (id *Identity) Address() Address { return id.address }
 // HasPrivate returns true if this identity has its own private portion.
 func (id *Identity) HasPrivate() bool { return len(id.privateKey) > 0 }
 
+// Fingerprint gets this identity's address plus hash of public key(s).
+func (id *Identity) Fingerprint() *Fingerprint {
+	id.initCIdentityPtr()
+	return newFingerprintFromCFingerprint(C.ZT_Identity_fingerprint(id.cid))
+}
+
 // PrivateKeyString returns the full identity.secret if the private key is set, or an empty string if no private key is set.
 func (id *Identity) PrivateKeyString() string {
 	switch id.idtype {

+ 3 - 6
include/ZeroTierCore.h

@@ -305,14 +305,9 @@ enum ZT_EndpointType
 	ZT_ENDPOINT_TYPE_IP =            5,  // Naked IP (protocol 193)
 	ZT_ENDPOINT_TYPE_IP_UDP =        6,  // IP/UDP
 	ZT_ENDPOINT_TYPE_IP_TCP =        7,  // IP/TCP
-	ZT_ENDPOINT_TYPE_IP_HTTP2 =      8   // IP/HTTP2 encapsulation
+	ZT_ENDPOINT_TYPE_IP_HTTP =       8   // IP/HTTP encapsulation
 };
 
-/**
- * A string that contains endpoint type IDs indexed by endpoint type (can be used as a lookup array)
- */
-#define ZT_ENDPOINT_TYPE_CHAR_INDEX "012345678"
-
 /**
  * Full identity fingerprint with address and 384-bit hash of public key(s)
  */
@@ -2277,6 +2272,8 @@ ZT_SDK_API void ZT_version(
 	int *revision,
 	int *build);
 
+/* ---------------------------------------------------------------------------------------------------------------- */
+
 #ifdef __cplusplus
 }
 #endif

+ 10 - 0
node/Constants.hpp

@@ -139,6 +139,11 @@
  */
 #define ZT_NAT_T_PORT_SCAN_MAX 16
 
+/**
+ * Minimum interval between attempts to reach a given physical endpoint
+ */
+#define ZT_PATH_MIN_TRY_INTERVAL ZT_PATH_KEEPALIVE_PERIOD
+
 /**
  * Delay between calls to the pulse() method in Peer for each peer
  */
@@ -168,6 +173,11 @@
  */
 #define ZT_PEER_PRIORITIZE_PATHS_INTERVAL 5000
 
+/**
+ * Number of previous endpoints to cache for root-less re-establishment
+ */
+#define ZT_PEER_ENDPOINT_CACHE_SIZE 8
+
 /**
  * Delay between requests for updated network autoconf information
  *

+ 13 - 14
node/Endpoint.cpp

@@ -18,34 +18,34 @@ namespace ZeroTier {
 
 char *Endpoint::toString(char s[ZT_ENDPOINT_STRING_SIZE_MAX]) const noexcept
 {
-	static const char *const s_endpointTypeChars = ZT_ENDPOINT_TYPE_CHAR_INDEX;
+	static const char *const s_endpointTypeChars = "0123456789";
 
 	static_assert(ZT_ENDPOINT_STRING_SIZE_MAX > (ZT_INETADDRESS_STRING_SIZE_MAX + 4), "overflow");
 	static_assert(ZT_ENDPOINT_STRING_SIZE_MAX > (ZT_FINGERPRINT_STRING_SIZE_MAX + 4), "overflow");
 
 	switch (this->type) {
-		default:
+		default: // ZT_ENDPOINT_TYPE_NIL
 			s[0] = s_endpointTypeChars[ZT_ENDPOINT_TYPE_NIL];
 			s[1] = 0;
 			break;
 		case ZT_ENDPOINT_TYPE_ZEROTIER:
 			s[0] = s_endpointTypeChars[ZT_ENDPOINT_TYPE_ZEROTIER];
-			s[1] = '-';
+			s[1] = '=';
 			zt().toString(s + 2);
 			break;
 		case ZT_ENDPOINT_TYPE_ETHERNET:
 		case ZT_ENDPOINT_TYPE_WIFI_DIRECT:
 		case ZT_ENDPOINT_TYPE_BLUETOOTH:
 			s[0] = s_endpointTypeChars[this->type];
-			s[1] = '-';
+			s[1] = '=';
 			eth().toString(s + 2);
 			break;
 		case ZT_ENDPOINT_TYPE_IP:
 		case ZT_ENDPOINT_TYPE_IP_UDP:
 		case ZT_ENDPOINT_TYPE_IP_TCP:
-		case ZT_ENDPOINT_TYPE_IP_HTTP2:
+		case ZT_ENDPOINT_TYPE_IP_HTTP:
 			s[0] = s_endpointTypeChars[this->type];
-			s[1] = '-';
+			s[1] = '=';
 			ip().toString(s + 2);
 			break;
 	}
@@ -59,7 +59,7 @@ bool Endpoint::fromString(const char *s) noexcept
 	if ((!s) || (!*s))
 		return true;
 
-	const char *start = strchr(s, '-');
+	const char *start = strchr(s, '=');
 	if (start++ != nullptr) {
 		// Parse a fully qualified type-address format Endpoint.
 		char tmp[16];
@@ -93,7 +93,7 @@ bool Endpoint::fromString(const char *s) noexcept
 			case ZT_ENDPOINT_TYPE_IP:
 			case ZT_ENDPOINT_TYPE_IP_UDP:
 			case ZT_ENDPOINT_TYPE_IP_TCP:
-			case ZT_ENDPOINT_TYPE_IP_HTTP2:
+			case ZT_ENDPOINT_TYPE_IP_HTTP:
 				if (!asInetAddress(this->value.ss).fromString(start))
 					return false;
 			default:
@@ -116,8 +116,7 @@ bool Endpoint::fromString(const char *s) noexcept
 int Endpoint::marshal(uint8_t data[ZT_ENDPOINT_MARSHAL_SIZE_MAX]) const noexcept
 {
 	switch (this->type) {
-		//case ZT_ENDPOINT_TYPE_NIL:
-		default:
+		default: // ZT_ENDPOINT_TYPE_NIL
 			// NIL endpoints get serialized like NIL InetAddress instances.
 			data[0] = ZT_ENDPOINT_TYPE_NIL;
 			return 1;
@@ -141,7 +140,7 @@ int Endpoint::marshal(uint8_t data[ZT_ENDPOINT_MARSHAL_SIZE_MAX]) const noexcept
 
 		case ZT_ENDPOINT_TYPE_IP:
 		case ZT_ENDPOINT_TYPE_IP_TCP:
-		case ZT_ENDPOINT_TYPE_IP_HTTP2:
+		case ZT_ENDPOINT_TYPE_IP_HTTP:
 			// Other IP types get serialized as new version Endpoint instances with type.
 			data[0] = 16 + (uint8_t)this->type;
 			return 1 + asInetAddress(this->value.ss).marshal(data + 1);
@@ -197,7 +196,7 @@ int Endpoint::unmarshal(const uint8_t *restrict data, int len) noexcept
 		case ZT_ENDPOINT_TYPE_IP:
 		case ZT_ENDPOINT_TYPE_IP_UDP:
 		case ZT_ENDPOINT_TYPE_IP_TCP:
-		case ZT_ENDPOINT_TYPE_IP_HTTP2:
+		case ZT_ENDPOINT_TYPE_IP_HTTP:
 			return asInetAddress(this->value.ss).unmarshal(data + 1, len - 1);
 
 		default:
@@ -227,7 +226,7 @@ bool Endpoint::operator==(const Endpoint &ep) const noexcept
 			case ZT_ENDPOINT_TYPE_IP:
 			case ZT_ENDPOINT_TYPE_IP_UDP:
 			case ZT_ENDPOINT_TYPE_IP_TCP:
-			case ZT_ENDPOINT_TYPE_IP_HTTP2:
+			case ZT_ENDPOINT_TYPE_IP_HTTP:
 				return ip() == ep.ip();
 			default:
 				return true;
@@ -249,7 +248,7 @@ bool Endpoint::operator<(const Endpoint &ep) const noexcept
 			case ZT_ENDPOINT_TYPE_IP:
 			case ZT_ENDPOINT_TYPE_IP_UDP:
 			case ZT_ENDPOINT_TYPE_IP_TCP:
-			case ZT_ENDPOINT_TYPE_IP_HTTP2:
+			case ZT_ENDPOINT_TYPE_IP_HTTP:
 				return ip() < ep.ip();
 			default:
 				return true;

+ 2 - 2
node/Endpoint.hpp

@@ -138,12 +138,12 @@ public:
 			case ZT_ENDPOINT_TYPE_IP:
 			case ZT_ENDPOINT_TYPE_IP_UDP:
 			case ZT_ENDPOINT_TYPE_IP_TCP:
-			case ZT_ENDPOINT_TYPE_IP_HTTP2:
+			case ZT_ENDPOINT_TYPE_IP_HTTP:
 				switch(ep.type) {
 					case ZT_ENDPOINT_TYPE_IP:
 					case ZT_ENDPOINT_TYPE_IP_UDP:
 					case ZT_ENDPOINT_TYPE_IP_TCP:
-					case ZT_ENDPOINT_TYPE_IP_HTTP2:
+					case ZT_ENDPOINT_TYPE_IP_HTTP:
 						return ip().ipsEqual(ep.ip());
 					default:
 						break;

+ 4 - 3
node/Expect.hpp

@@ -39,7 +39,8 @@ namespace ZeroTier {
 class Expect
 {
 public:
-	ZT_INLINE Expect() {}
+	ZT_INLINE Expect()
+	{}
 
 	/**
 	 * Called by other code when something is sending a packet that could potentially receive an OK response
@@ -47,7 +48,7 @@ public:
 	 * @param packetId Packet ID of packet being sent (be sure it's post-armor())
 	 * @param now Current time
 	 */
-	ZT_INLINE void sending(const uint64_t packetId,const int64_t now) noexcept
+	ZT_INLINE void sending(const uint64_t packetId, const int64_t now) noexcept
 	{
 		m_packetIdSent[Utils::hash64(packetId ^ Utils::s_mapNonce) % ZT_EXPECT_BUCKETS].store((uint32_t)(now / ZT_EXPECT_TTL));
 	}
@@ -62,7 +63,7 @@ public:
 	 * @param now Current time
 	 * @return True if we're expecting a reply (and a reset occurred)
 	 */
-	ZT_INLINE bool expecting(const uint64_t inRePacketId,const int64_t now) noexcept
+	ZT_INLINE bool expecting(const uint64_t inRePacketId, const int64_t now) noexcept
 	{
 		return (((now / ZT_EXPECT_TTL) - (int64_t)m_packetIdSent[(unsigned long)Utils::hash64(inRePacketId ^ Utils::s_mapNonce) % ZT_EXPECT_BUCKETS].exchange(0)) <= 1);
 	}

+ 48 - 9
node/Peer.cpp

@@ -243,25 +243,51 @@ void Peer::pulse(void *const tPtr, const int64_t now, const bool isRoot)
 			if (m_locator) {
 				for (Vector<Endpoint>::const_iterator ep(m_locator->endpoints().begin());ep != m_locator->endpoints().end();++ep) {
 					if (ep->type == ZT_ENDPOINT_TYPE_IP_UDP) {
-						RR->t->tryingNewPath(tPtr, 0x84b22322, m_id, ep->ip(), InetAddress::NIL, 0, 0, Identity::NIL);
-						sent(now, m_sendProbe(tPtr, -1, ep->ip(), nullptr, 0, now));
+						if (RR->node->shouldUsePathForZeroTierTraffic(tPtr, m_id, -1, ep->ip())) {
+							int64_t &lt = m_lastTried[*ep];
+							if ((now - lt) > ZT_PATH_MIN_TRY_INTERVAL) {
+								lt = now;
+								RR->t->tryingNewPath(tPtr, 0x84b22322, m_id, ep->ip(), InetAddress::NIL, 0, 0, Identity::NIL);
+								sent(now, m_sendProbe(tPtr, -1, ep->ip(), nullptr, 0, now));
+							}
+						}
+					}
+				}
+			}
+
+			for(unsigned int i=0;i<ZT_PEER_ENDPOINT_CACHE_SIZE;++i) {
+				if ((m_endpointCache[i].firstSeen > 0) && (m_endpointCache[i].target.type == ZT_ENDPOINT_TYPE_IP_UDP)) {
+					if (RR->node->shouldUsePathForZeroTierTraffic(tPtr, m_id, -1, m_endpointCache[i].target.ip())) {
+						int64_t &lt = m_lastTried[m_endpointCache[i].target];
+						if ((now - lt) > ZT_PATH_MIN_TRY_INTERVAL) {
+							lt = now;
+							RR->t->tryingNewPath(tPtr, 0x84b22343, m_id, m_endpointCache[i].target.ip(), InetAddress::NIL, 0, 0, Identity::NIL);
+							sent(now, m_sendProbe(tPtr, -1, m_endpointCache[i].target.ip(), nullptr, 0, now));
+						}
 					}
 				}
 			}
 
 			InetAddress addr;
 			if (RR->node->externalPathLookup(tPtr, m_id, -1, addr)) {
-				if ((addr) && (RR->node->shouldUsePathForZeroTierTraffic(tPtr, m_id, -1, addr))) {
-					RR->t->tryingNewPath(tPtr, 0x84a10000, m_id, addr, InetAddress::NIL, 0, 0, Identity::NIL);
-					sent(now, m_sendProbe(tPtr, -1, addr, nullptr, 0, now));
+				if ((addr) && RR->node->shouldUsePathForZeroTierTraffic(tPtr, m_id, -1, addr)) {
+					int64_t &lt = m_lastTried[Endpoint(addr)];
+					if ((now - lt) > ZT_PATH_MIN_TRY_INTERVAL) {
+						lt = now;
+						RR->t->tryingNewPath(tPtr, 0x84a10000, m_id, addr, InetAddress::NIL, 0, 0, Identity::NIL);
+						sent(now, m_sendProbe(tPtr, -1, addr, nullptr, 0, now));
+					}
 				}
 			}
 		}
 	} else {
 		// Attempt up to ZT_NAT_T_MAX_QUEUED_ATTEMPTS_PER_PULSE queued addresses.
 
+		// Note that m_lastTried is checked when contact() is called and something
+		// is added to the try queue, not here.
+
 		unsigned int attempts = 0;
-		for(;;) {
+		for (;;) {
 			p_TryQueueItem &qi = m_tryQueue.front();
 
 			if (qi.target.isInetAddr()) {
@@ -326,15 +352,15 @@ void Peer::pulse(void *const tPtr, const int64_t now, const bool isRoot)
 			// Discard front item unless the code skips to requeue_item.
 			discard_queue_item:
 			m_tryQueue.pop_front();
-			if ((m_tryQueue.empty()) || (attempts >= ZT_NAT_T_PORT_SCAN_MAX))
+			if (attempts >= std::min((unsigned int)m_tryQueue.size(),(unsigned int)ZT_NAT_T_PORT_SCAN_MAX))
 				break;
 			else continue;
 
 			// If the code skips here the front item is instead moved to the back.
 			requeue_item:
-			if (m_tryQueue.size() > 1)
+			if (m_tryQueue.size() > 1) // no point in doing this splice if there's only one item
 				m_tryQueue.splice(m_tryQueue.end(), m_tryQueue, m_tryQueue.begin());
-			if (attempts >= ZT_NAT_T_PORT_SCAN_MAX)
+			if (attempts >= std::min((unsigned int)m_tryQueue.size(),(unsigned int)ZT_NAT_T_PORT_SCAN_MAX))
 				break;
 			else continue;
 		}
@@ -370,6 +396,13 @@ void Peer::pulse(void *const tPtr, const int64_t now, const bool isRoot)
 			}
 		}
 	}
+
+	// Clean m_lastTried
+	for (Map<Endpoint,int64_t>::iterator i(m_lastTried.begin());i!=m_lastTried.end();) {
+		if ((now - i->second) > (ZT_PATH_MIN_TRY_INTERVAL * 4))
+			m_lastTried.erase(i++);
+		else ++i;
+	}
 }
 
 void Peer::contact(void *tPtr, const int64_t now, const Endpoint &ep, int tries)
@@ -387,6 +420,12 @@ void Peer::contact(void *tPtr, const int64_t now, const Endpoint &ep, int tries)
 		}
 	}
 
+	// Check underlying path attempt rate limit.
+	int64_t &lt = m_lastTried[ep];
+	if ((now - lt) < ZT_PATH_MIN_TRY_INTERVAL)
+		return;
+	lt = now;
+
 	// For IPv4 addresses we send a tiny packet with a low TTL, which helps to
 	// traverse some NAT types. It has no effect otherwise.
 	if (ep.isInetAddr() && ep.ip().isV4()) {

+ 28 - 22
node/Peer.hpp

@@ -153,9 +153,7 @@ public:
 	 * @param bytes Number of bytes relayed
 	 */
 	ZT_INLINE void relayed(const int64_t now, const unsigned int bytes) noexcept
-	{
-		m_relayedMeter.log(now, bytes);
-	}
+	{ m_relayedMeter.log(now, bytes); }
 
 	/**
 	 * Get the current best direct path or NULL if none
@@ -288,25 +286,19 @@ public:
 	 * @return The permanent shared key for this peer computed by simple identity agreement
 	 */
 	ZT_INLINE SharedPtr<SymmetricKey> identityKey() noexcept
-	{
-		return m_identityKey;
-	}
+	{ return m_identityKey; }
 
 	/**
 	 * @return AES instance for HELLO dictionary / encrypted section encryption/decryption
 	 */
 	ZT_INLINE const AES &identityHelloDictionaryEncryptionCipher() noexcept
-	{
-		return m_helloCipher;
-	}
+	{ return m_helloCipher; }
 
 	/**
 	 * @return Key for HMAC on HELLOs
 	 */
 	ZT_INLINE const uint8_t *identityHelloHmacKey() noexcept
-	{
-		return m_helloMacKey;
-	}
+	{ return m_helloMacKey; }
 
 	/**
 	 * @return Raw identity key bytes
@@ -336,9 +328,7 @@ public:
 	 * @return True if this key is ephemeral, false if it's the long-lived identity key
 	 */
 	ZT_INLINE bool isEphemeral(const SharedPtr<SymmetricKey> &k) const noexcept
-	{
-		return (m_identityKey != k);
-	}
+	{ return m_identityKey != k; }
 
 	/**
 	 * Set the currently known remote version of this peer's client
@@ -350,10 +340,10 @@ public:
 	 */
 	ZT_INLINE void setRemoteVersion(unsigned int vproto, unsigned int vmaj, unsigned int vmin, unsigned int vrev) noexcept
 	{
-		m_vProto = (uint16_t) vproto;
-		m_vMajor = (uint16_t) vmaj;
-		m_vMinor = (uint16_t) vmin;
-		m_vRevision = (uint16_t) vrev;
+		m_vProto = (uint16_t)vproto;
+		m_vMajor = (uint16_t)vmaj;
+		m_vMinor = (uint16_t)vmin;
+		m_vRevision = (uint16_t)vrev;
 	}
 
 	ZT_INLINE unsigned int remoteVersionProtocol() const noexcept
@@ -369,7 +359,7 @@ public:
 	{ return m_vRevision; }
 
 	ZT_INLINE bool remoteVersionKnown() const noexcept
-	{ return ((m_vMajor > 0) || (m_vMinor > 0) || (m_vRevision > 0)); }
+	{ return (m_vMajor > 0) || (m_vMinor > 0) || (m_vRevision > 0); }
 
 	/**
 	 * @return True if there is at least one alive direct path
@@ -445,7 +435,7 @@ public:
 	ZT_INLINE bool deduplicateIncomingPacket(const uint64_t packetId) noexcept
 	{
 		// TODO: should take instance ID into account too, but this isn't fully wired.
-		return m_dedup[Utils::hash32((uint32_t) packetId) & ZT_PEER_DEDUP_BUFFER_MASK].exchange(packetId) == packetId;
+		return m_dedup[Utils::hash32((uint32_t)packetId) & ZT_PEER_DEDUP_BUFFER_MASK].exchange(packetId) == packetId;
 	}
 
 private:
@@ -521,7 +511,22 @@ private:
 	// For SharedPtr<>
 	std::atomic<int> __refCount;
 
-	// Addresses recieved via PUSH_DIRECT_PATHS etc. that we are scheduled to try.
+	struct p_EndpointCacheItem
+	{
+		Endpoint target;
+		uint64_t timesSeen;
+		int64_t firstSeen;
+
+		ZT_INLINE bool operator<(const p_EndpointCacheItem &ci) const noexcept
+		{ return (ci.timesSeen < timesSeen) || ((ci.timesSeen == timesSeen) && (ci.firstSeen < firstSeen)); }
+
+		ZT_INLINE p_EndpointCacheItem() noexcept : target(), timesSeen(0), firstSeen(0)
+		{}
+	};
+
+	// Endpoint cache sorted in ascending order of times seen followed by first seen time.
+	p_EndpointCacheItem m_endpointCache[ZT_PEER_ENDPOINT_CACHE_SIZE];
+
 	struct p_TryQueueItem
 	{
 		ZT_INLINE p_TryQueueItem() :
@@ -539,6 +544,7 @@ private:
 	};
 
 	List<p_TryQueueItem> m_tryQueue;
+	Map<Endpoint,int64_t> m_lastTried;
 
 	uint16_t m_vProto;
 	uint16_t m_vMajor;

+ 11 - 9
node/Topology.cpp

@@ -28,7 +28,7 @@ Topology::Topology(const RuntimeEnvironment *renv, void *tPtr) :
 		for (;;) {
 			Identity id;
 			int l = id.unmarshal(dptr, drem);
-			if ((l > 0)&&(id)) {
+			if ((l > 0) && (id)) {
 				if ((drem -= l) <= 0)
 					break;
 				Locator *const loc = new Locator();
@@ -87,9 +87,9 @@ bool Topology::addRoot(void *const tPtr, const Identity &id, const SharedPtr<con
 bool Topology::removeRoot(void *const tPtr, Address address)
 {
 	RWMutex::Lock l1(m_peers_l);
-	for(Vector< SharedPtr<Peer> >::const_iterator r(m_rootPeers.begin());r!=m_rootPeers.end();++r) {
+	for (Vector<SharedPtr<Peer> >::const_iterator r(m_rootPeers.begin());r != m_rootPeers.end();++r) {
 		if ((*r)->address() == address) {
-			Map< Identity,SharedPtr<const Locator> >::iterator rr(m_roots.find((*r)->identity()));
+			Map<Identity, SharedPtr<const Locator> >::iterator rr(m_roots.find((*r)->identity()));
 			if (rr != m_roots.end()) {
 				m_roots.erase(rr);
 				m_updateRootPeers(tPtr);
@@ -113,6 +113,8 @@ void Topology::doPeriodicTasks(void *tPtr, const int64_t now)
 	{
 		RWMutex::Lock l1(m_peers_l);
 		for (Map<Address, SharedPtr<Peer> >::iterator i(m_peers.begin());i != m_peers.end();) {
+			// TODO: also delete if the peer has not exchanged meaningful communication in a while, such as
+			// a network frame or non-trivial control packet.
 			if (((now - i->second->lastReceive()) > ZT_PEER_ALIVE_TIMEOUT) && (m_roots.count(i->second->identity()) == 0)) {
 				i->second->save(tPtr);
 				m_peers.erase(i++);
@@ -147,9 +149,9 @@ void Topology::m_loadCached(void *tPtr, const Address &zta, SharedPtr<Peer> &pee
 		Vector<uint8_t> data(RR->node->stateObjectGet(tPtr, ZT_STATE_OBJECT_PEER, id));
 		if (data.size() > 8) {
 			const uint8_t *d = data.data();
-			int dl = (int) data.size();
+			int dl = (int)data.size();
 
-			const int64_t ts = (int64_t) Utils::loadBigEndian<uint64_t>(d);
+			const int64_t ts = (int64_t)Utils::loadBigEndian<uint64_t>(d);
 			Peer *const p = new Peer(RR);
 			int n = p->unmarshal(d + 8, dl - 8);
 			if (n < 0) {
@@ -173,7 +175,7 @@ void Topology::m_writeRootList(void *tPtr)
 	uint8_t *const roots = (uint8_t *)malloc((ZT_IDENTITY_MARSHAL_SIZE_MAX + ZT_LOCATOR_MARSHAL_SIZE_MAX + 2) * m_roots.size());
 	if (roots) { // sanity check
 		int p = 0;
-		for (Map< Identity,SharedPtr<const Locator> >::const_iterator r(m_roots.begin());r!=m_roots.end();++r) {
+		for (Map<Identity, SharedPtr<const Locator> >::const_iterator r(m_roots.begin());r != m_roots.end();++r) {
 			int pp = r->first.marshal(roots + p, false);
 			if (pp > 0) {
 				p += pp;
@@ -193,9 +195,9 @@ void Topology::m_writeRootList(void *tPtr)
 void Topology::m_updateRootPeers(void *tPtr)
 {
 	// assumes m_peers_l is locked for write
-	Vector< SharedPtr<Peer> > rp;
-	for (Map< Identity,SharedPtr<const Locator> >::iterator r(m_roots.begin());r!=m_roots.end();++r) {
-		Map< Address,SharedPtr<Peer> >::iterator pp(m_peers.find(r->first.address()));
+	Vector<SharedPtr<Peer> > rp;
+	for (Map<Identity, SharedPtr<const Locator> >::iterator r(m_roots.begin());r != m_roots.end();++r) {
+		Map<Address, SharedPtr<Peer> >::iterator pp(m_peers.find(r->first.address()));
 		SharedPtr<Peer> p;
 		if (pp != m_peers.end())
 			p = pp->second;