Browse Source

Move some toString/fromString to C++ since any ZT code base would need it, and stub out the controller commands.

Adam Ierymenko 5 years ago
parent
commit
1970dab13d

+ 17 - 0
go/cmd/zerotier/cli/controller.go

@@ -0,0 +1,17 @@
+/*
+ * 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
+
+func Controller(basePath, authToken string, args []string, jsonOutput bool) {
+}

+ 18 - 11
go/cmd/zerotier/cli/help.go

@@ -18,7 +18,6 @@ import (
 	"zerotier/pkg/zerotier"
 )
 
-// Help dumps help to stdout
 func Help() {
 	fmt.Printf(`ZeroTier Network Hypervisor Service Version %d.%d.%d
 (c)2013-2020 ZeroTier, Inc.
@@ -38,11 +37,11 @@ Commands:
   service                               Start as service
   status                                Show node status, identity, and config
   peers                                 List all VL1 peers
-  join <network ID> [fingerprint]       Join a virtual network
-  leave <network ID>                    Leave a virtual network
+  join <network> [fingerprint]          Join a virtual network
+  leave <network>                       Leave a virtual network
   networks                              List VL2 virtual networks
-  network <network ID>                  Show verbose network info
-  set <network ID> [option] [value]     Get or set a network config option
+  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?
@@ -62,11 +61,20 @@ Commands:
     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
-  roots                                 List root peers
-  addroot <identity> <locator>          Add a root or update its locator
-  addroot <url>                         Add or update roots from a URL
-  removeroot <address>                  Remove a peer from the root list
+    show <locator> [identity]           Show locator information
+  root [command]                        Root management commands
+    list                                List root peers (same as no command)
+    add <identity> <locator>            Add or manually update a root
+    add <url>                           Add or update root(s) from a URL
+    remove <address>                    Un-designate a peer as a root
+  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
 
 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
@@ -78,6 +86,5 @@ node.
 
 Identities can be specified verbatim on the command line or as a path to
 a file. This is detected automatically.
-
 `,zerotier.CoreVersionMajor, zerotier.CoreVersionMinor, zerotier.CoreVersionRevision)
 }

+ 0 - 1
go/cmd/zerotier/cli/identity.go

@@ -23,7 +23,6 @@ import (
 	"zerotier/pkg/zerotier"
 )
 
-// Identity command
 func Identity(args []string) {
 	if len(args) > 0 {
 		switch args[0] {

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

@@ -0,0 +1,83 @@
+/*
+ * 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)
+}

+ 23 - 3
go/cmd/zerotier/cli/misc.go

@@ -99,23 +99,43 @@ func readIdentity(s string) *zerotier.Identity {
 	}
 	idData, err := ioutil.ReadFile(s)
 	if err != nil {
-		fmt.Printf("FATAL: identity '%s' cannot be resolved as file or literal identity: %s", s, err.Error())
+		fmt.Printf("FATAL: identity '%s' cannot be parsed as file or literal: %s", s, err.Error())
 		os.Exit(1)
 	}
 	id, err := zerotier.NewIdentityFromString(string(idData))
 	if err != nil {
-		fmt.Printf("FATAL: identity '%s' cannot be resolved as file or literal identity: %s", s, err.Error())
+		fmt.Printf("FATAL: identity '%s' cannot be parsed as file or literal: %s", s, err.Error())
 		os.Exit(1)
 	}
 	return id
 }
 
+func readLocator(s string) *zerotier.Locator {
+	if strings.ContainsRune(s, '@') {
+		loc, _ := zerotier.NewLocatorFromString(s)
+		if loc != nil {
+			return loc
+		}
+	}
+	locData, err := ioutil.ReadFile(s)
+	if err != nil {
+		fmt.Printf("FATAL: locator '%s' cannot be parsed as file or literal: %s", s, err.Error())
+		os.Exit(1)
+	}
+	loc, err := zerotier.NewLocatorFromString(string(locData))
+	if err != nil {
+		fmt.Printf("FATAL: locator '%s' cannot be parsed as file or literal: %s", s, err.Error())
+		os.Exit(1)
+	}
+	return loc
+}
+
 func networkStatusStr(status int) string {
 	switch status {
 	case zerotier.NetworkStatusNotFound:
 		return "NOTFOUND"
 	case zerotier.NetworkStatusAccessDenied:
-		return "DENIED"
+		return "ACCESSDENIED"
 	case zerotier.NetworkStatusRequestConfiguration:
 		return "UPDATING"
 	case zerotier.NetworkStatusOK:

+ 0 - 1
go/cmd/zerotier/cli/network.go

@@ -21,7 +21,6 @@ import (
 	"zerotier/pkg/zerotier"
 )
 
-// Network CLI command
 func Network(basePath, authToken string, args []string, jsonOutput bool) {
 	if len(args) != 1 {
 		Help()

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

@@ -0,0 +1,17 @@
+/*
+ * 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
+
+func Root(basePath, authToken string, args []string, jsonOutput bool) {
+}

+ 11 - 7
go/cmd/zerotier/zerotier.go

@@ -127,13 +127,6 @@ func main() {
 	case "peers", "listpeers", "lspeers":
 		authTokenRequired(authToken)
 		cli.Peers(basePath, authToken, cmdArgs, *jflag, false)
-	case "roots", "listroots", "lsroots":
-		authTokenRequired(authToken)
-		cli.Peers(basePath, authToken, cmdArgs, *jflag, true)
-	case "addroot":
-		// TODO
-	case "removeroot":
-		// TODO
 	case "join":
 		authTokenRequired(authToken)
 		cli.Join(basePath, authToken, cmdArgs)
@@ -151,8 +144,19 @@ 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)
+	case "controller":
+		authTokenRequired(authToken)
+		cli.Controller(basePath, authToken, cmdArgs, *jflag)
 	}
 
+	// Commands in the 'cli' sub-package do not return, so if we make
+	// it here the command was not recognized.
+
 	cli.Help()
 	os.Exit(1)
 }

+ 53 - 10
go/pkg/zerotier/endpoint.go

@@ -5,11 +5,12 @@ package zerotier
 // static inline uint64_t _getAddress(const ZT_Endpoint *ep) { return ep->value.fp.address; }
 // static inline uint64_t _getMAC(const ZT_Endpoint *ep) { return ep->value.mac; }
 // static inline const struct sockaddr_storage *_getSS(const ZT_Endpoint *ep) { return &(ep->value.ss); }
+// static inline void _setSS(ZT_Endpoint *ep,const void *ss) { memcpy(&(ep->value.ss),ss,sizeof(struct sockaddr_storage)); }
 import "C"
 
 import (
 	"encoding/json"
-	"fmt"
+	"strings"
 	"unsafe"
 )
 
@@ -29,6 +30,42 @@ type Endpoint struct {
 	cep C.ZT_Endpoint
 }
 
+// NewEndpointFromString constructs a new endpoint from an InetAddress or Endpoint string.
+// This will auto detect whether this is a plain InetAddress or an Endpoint in string
+// format. If the former it's created as a ZT_ENDPOINT_TYPE_IP_UDP endpoint.
+func NewEndpointFromString(s string) (*Endpoint, error) {
+	if len(s) == 0 {
+		var ep Endpoint
+		ep.cep._type = C.ZT_ENDPOINT_TYPE_NIL
+		return &ep, nil
+	}
+	if strings.IndexRune(s, '-') > 0 || (strings.IndexRune(s, ':') < 0 && strings.IndexRune(s, '.') < 0) {
+		var ep Endpoint
+		cs := C.CString(s)
+		defer C.free(cs)
+		if C.ZT_Endpoint_fromString(cs) == nil {
+			return nil, ErrInvalidParameter
+		}
+		return &ep, nil
+	}
+	inaddr := NewInetAddressFromString(s)
+	if inaddr == nil {
+		return nil, ErrInvalidParameter
+	}
+	return NewEndpointFromInetAddress(inaddr)
+}
+
+func NewEndpointFromInetAddress(addr *InetAddress) (*Endpoint, error) {
+	var ep Endpoint
+	var ss C.struct_sockaddr_storage
+	if !makeSockaddrStorage(addr.IP, addr.Port, &ss) {
+		return nil, ErrInvalidParameter
+	}
+	ep.cep._type = C.ZT_ENDPOINT_TYPE_IP_UDP
+	C._setSS(&ep.cep, unsafe.Pointer(&ss))
+	return &ep, nil
+}
+
 // Type returns this endpoint's type.
 func (ep *Endpoint) Type() int {
 	return int(ep.cep._type)
@@ -77,15 +114,12 @@ func (ep *Endpoint) MAC() MAC {
 }
 
 func (ep *Endpoint) String() string {
-	switch ep.cep._type {
-	case EndpointTypeZeroTier:
-		return fmt.Sprintf("%d/%s", ep.Type(), ep.Fingerprint().String())
-	case EndpointTypeEthernet, EndpointTypeWifiDirect, EndpointTypeBluetooth:
-		return fmt.Sprintf("%d/%s", ep.Type(), ep.MAC().String())
-	case EndpointTypeIp, EndpointTypeIpUdp, EndpointTypeIpTcp, EndpointTypeIpHttp2:
-		return fmt.Sprintf("%d/%s", ep.Type(), ep.InetAddress().String())
+	var buf [4096]byte
+	cs := C.ZT_Endpoint_toString(&ep.cep,unsafe.Pointer(&buf[0]),4096)
+	if cs == nil {
+		return "0"
 	}
-	return fmt.Sprintf("%d", ep.Type())
+	return C.GoString(cs)
 }
 
 func (ep *Endpoint) MarshalJSON() ([]byte, error) {
@@ -94,6 +128,15 @@ func (ep *Endpoint) MarshalJSON() ([]byte, error) {
 }
 
 func (ep *Endpoint) UnmarshalJSON(j []byte) error {
-	// TODO
+	var s string
+	err := json.Unmarshal(j, &s)
+	if err != nil {
+		return err
+	}
+	ep2, err := NewEndpointFromString(s)
+	if err != nil {
+		return err
+	}
+	*ep = *ep2
 	return nil
 }

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

@@ -32,7 +32,7 @@ func NewFingerprintFromString(fps string) (*Fingerprint, error) {
 	if len(fps) < AddressStringLength {
 		return nil, ErrInvalidZeroTierAddress
 	}
-	ss := strings.Split(fps, "/")
+	ss := strings.Split(fps, "-")
 	if len(ss) < 1 || len(ss) > 2 {
 		return nil, ErrInvalidParameter
 	}
@@ -67,7 +67,7 @@ func newFingerprintFromCFingerprint(cfp *C.ZT_Fingerprint) *Fingerprint {
 
 func (fp *Fingerprint) String() string {
 	if len(fp.Hash) == 48 {
-		return fmt.Sprintf("%.10x/%s", uint64(fp.Address), Base32StdLowerCase.EncodeToString(fp.Hash))
+		return fmt.Sprintf("%.10x-%s", uint64(fp.Address), Base32StdLowerCase.EncodeToString(fp.Hash))
 	}
 	return fp.Address.String()
 }

+ 29 - 1
go/pkg/zerotier/locator.go

@@ -64,9 +64,26 @@ func NewLocatorFromBytes(lb []byte) (*Locator, error) {
 	return goloc, nil
 }
 
+func NewLocatorFromString(s string) (*Locator, error) {
+	if len(s) == 0 {
+		return nil, ErrInvalidParameter
+	}
+	sb := []byte(s)
+	sb = append(sb,0)
+	loc := C.ZT_Locator_fromString(unsafe.Pointer(&sb[0]))
+	if uintptr(loc) == 0 {
+		return nil, ErrInvalidParameter
+	}
+	goloc := new(Locator)
+	goloc.cl = unsafe.Pointer(loc)
+	runtime.SetFinalizer(goloc, locatorFinalizer)
+	return goloc, nil
+}
+
 // GetInfo gets information about this locator, also validating its signature if 'id' is non-nil.
 // If 'id' is nil the 'valid' return value is undefined.
-func (loc *Locator) GetInfo(id *Identity) (fp *Fingerprint, eps []Endpoint, valid bool, err error) {
+func (loc *Locator) GetInfo(id *Identity) (ts int64, fp *Fingerprint, eps []Endpoint, valid bool, err error) {
+	ts = int64(C.ZT_Locator_timestamp(loc.cl))
 	cfp := C.ZT_Locator_fingerprint(loc.cl)
 	if uintptr(unsafe.Pointer(cfp)) == 0 {
 		err = ErrInternal
@@ -84,3 +101,14 @@ func (loc *Locator) GetInfo(id *Identity) (fp *Fingerprint, eps []Endpoint, vali
 	}
 	return
 }
+
+func (loc *Locator) String() string {
+	var buf [4096]byte
+	C.ZT_Locator_toString(loc.cl,unsafe.Pointer(&buf[0]),4096)
+	for i:=range buf {
+		if buf[i] == 0 {
+			return string(buf[0:i])
+		}
+	}
+	return ""
+}

+ 40 - 0
include/ZeroTierCore.h

@@ -2123,6 +2123,17 @@ ZT_SDK_API void ZT_Identity_delete(ZT_Identity *id);
 
 /* ---------------------------------------------------------------------------------------------------------------- */
 
+ZT_SDK_API char *ZT_Endpoint_toString(
+	const ZT_Endpoint *ep,
+	char *buf,
+	int capacity);
+
+ZT_SDK_API int ZT_Endpoint_fromString(
+	ZT_Endpoint *ep,
+	const char *str);
+
+/* ---------------------------------------------------------------------------------------------------------------- */
+
 /**
  * Create and sign a new locator
  *
@@ -2149,6 +2160,14 @@ ZT_SDK_API ZT_Locator *ZT_Locator_unmarshal(
 	const void *data,
 	unsigned int len);
 
+/**
+ * Decode a locator from string format
+ *
+ * @param str String format locator
+ * @return Locator or NULL if string is not valid
+ */
+ZT_SDK_API ZT_Locator *ZT_Locator_fromString(const char *str);
+
 /**
  * Serialize this locator into a buffer
  *
@@ -2159,6 +2178,19 @@ ZT_SDK_API ZT_Locator *ZT_Locator_unmarshal(
  */
 ZT_SDK_API int ZT_Locator_marshal(const ZT_Locator *loc,void *buf,unsigned int bufSize);
 
+/**
+ * Get this locator in string format
+ *
+ * @param loc Locator
+ * @param buf Buffer to store string
+ * @param capacity Capacity of buffer in bytes (recommended size: 4096)
+ * @return Pointer to buffer or NULL if an error occurs
+ */
+ZT_SDK_API char *ZT_Locator_toString(
+	const ZT_Locator *loc,
+	char *buf,
+	int capacity);
+
 /**
  * Get a pointer to the fingerprint of this locator's signer.
  *
@@ -2169,6 +2201,14 @@ ZT_SDK_API int ZT_Locator_marshal(const ZT_Locator *loc,void *buf,unsigned int b
  */
 ZT_SDK_API const ZT_Fingerprint *ZT_Locator_fingerprint(const ZT_Locator *loc);
 
+/**
+ * Get a locator's timestamp
+ *
+ * @param loc Locator to query
+ * @return Locator timestamp in milliseconds since epoch
+ */
+ZT_SDK_API int64_t ZT_Locator_timestamp(const ZT_Locator *loc);
+
 /**
  * Get the number of endpoints in this locator
  *

+ 30 - 5
node/Endpoint.cpp

@@ -16,7 +16,7 @@
 
 namespace ZeroTier {
 
-void Endpoint::toString(char s[ZT_ENDPOINT_STRING_SIZE_MAX]) const noexcept
+char *Endpoint::toString(char s[ZT_ENDPOINT_STRING_SIZE_MAX]) const noexcept
 {
 	static const char *const s_endpointTypeChars = ZT_ENDPOINT_TYPE_CHAR_INDEX;
 
@@ -30,14 +30,14 @@ void Endpoint::toString(char s[ZT_ENDPOINT_STRING_SIZE_MAX]) const noexcept
 			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:
@@ -45,10 +45,12 @@ void Endpoint::toString(char s[ZT_ENDPOINT_STRING_SIZE_MAX]) const noexcept
 		case ZT_ENDPOINT_TYPE_IP_TCP:
 		case ZT_ENDPOINT_TYPE_IP_HTTP2:
 			s[0] = s_endpointTypeChars[this->type];
-			s[1] = '/';
+			s[1] = '-';
 			ip().toString(s + 2);
 			break;
 	}
+
+	return s;
 }
 
 bool Endpoint::fromString(const char *s) noexcept
@@ -57,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) {
 		char tmp[16];
 		for (unsigned int i = 0;i < 15;++i) {
@@ -248,3 +250,26 @@ bool Endpoint::operator<(const Endpoint &ep) const noexcept
 }
 
 } // namespace ZeroTier
+
+extern "C" {
+
+char *ZT_Endpoint_toString(
+	const ZT_Endpoint *ep,
+	char *buf,
+	int capacity)
+{
+	if ((!ep) || (!buf) || (capacity < ZT_ENDPOINT_STRING_SIZE_MAX))
+		return nullptr;
+	return reinterpret_cast<const ZeroTier::Endpoint *>(ep)->toString(buf);
+}
+
+int ZT_Endpoint_fromString(
+	ZT_Endpoint *ep,
+	const char *str)
+{
+	if ((!ep) || (!str))
+		return ZT_RESULT_ERROR_BAD_PARAMETER;
+	return reinterpret_cast<ZeroTier::Endpoint *>(ep)->fromString(str) ? ZT_RESULT_OK : ZT_RESULT_ERROR_BAD_PARAMETER;
+}
+
+} // C API

+ 2 - 7
node/Endpoint.hpp

@@ -147,14 +147,9 @@ public:
 	ZT_INLINE Fingerprint zt() const noexcept
 	{ return Fingerprint(this->value.fp); }
 
-	void toString(char s[ZT_ENDPOINT_STRING_SIZE_MAX]) const noexcept;
+	char *toString(char s[ZT_ENDPOINT_STRING_SIZE_MAX]) const noexcept;
 
-	ZT_INLINE String toString() const
-	{
-		char tmp[ZT_ENDPOINT_STRING_SIZE_MAX];
-		toString(tmp);
-		return String(tmp);
-	}
+	ZT_INLINE String toString() const { char tmp[ZT_ENDPOINT_STRING_SIZE_MAX]; return String(toString(tmp)); }
 
 	bool fromString(const char *s) noexcept;
 

+ 1 - 1
node/Fingerprint.hpp

@@ -54,7 +54,7 @@ public:
 	{
 		Address(this->address).toString(s);
 		if (haveHash()) {
-			s[ZT_ADDRESS_LENGTH_HEX] = '/';
+			s[ZT_ADDRESS_LENGTH_HEX] = '-';
 			Utils::b32e(this->hash, ZT_FINGERPRINT_HASH_SIZE, s + (ZT_ADDRESS_LENGTH_HEX + 1), ZT_FINGERPRINT_STRING_SIZE_MAX - (ZT_ADDRESS_LENGTH_HEX + 1));
 		}
 	}

+ 70 - 14
node/Locator.cpp

@@ -53,10 +53,33 @@ bool Locator::verify(const Identity &id) const noexcept
 			const unsigned int signlen = marshal(signdata, true);
 			return id.verify(signdata, signlen, m_signature.data(), m_signature.size());
 		}
-	} catch ( ... ) {} // fail verify on any unexpected exception
+	} catch (...) {} // fail verify on any unexpected exception
 	return false;
 }
 
+char *Locator::toString(char s[ZT_LOCATOR_STRING_SIZE_MAX]) const noexcept
+{
+	static_assert(ZT_LOCATOR_STRING_SIZE_MAX > ((((ZT_LOCATOR_MARSHAL_SIZE_MAX / 5) + 1) * 8) + ZT_ADDRESS_LENGTH_HEX + 1), "overflow");
+	uint8_t bin[ZT_LOCATOR_MARSHAL_SIZE_MAX];
+	Address(m_signer.address).toString(s);
+	s[ZT_ADDRESS_LENGTH_HEX] = '@';
+	Utils::b32e(bin, marshal(bin, false), s + (ZT_ADDRESS_LENGTH_HEX + 1), ZT_LOCATOR_STRING_SIZE_MAX - (ZT_ADDRESS_LENGTH_HEX + 1));
+	return s;
+}
+
+bool Locator::fromString(const char *s) noexcept
+{
+	if (!s)
+		return false;
+	if (strlen(s) < (ZT_ADDRESS_LENGTH_HEX + 1))
+		return false;
+	uint8_t bin[ZT_LOCATOR_MARSHAL_SIZE_MAX];
+	const int bl = Utils::b32d(s + (ZT_ADDRESS_LENGTH_HEX + 1), bin, ZT_LOCATOR_MARSHAL_SIZE_MAX);
+	if ((bl <= 0) || (bl > ZT_LOCATOR_MARSHAL_SIZE_MAX))
+		return false;
+	return unmarshal(bin, bl) > 0;
+}
+
 int Locator::marshal(uint8_t data[ZT_LOCATOR_MARSHAL_SIZE_MAX], const bool excludeSignature) const noexcept
 {
 	Utils::storeBigEndian<uint64_t>(data, (uint64_t) m_ts);
@@ -96,7 +119,7 @@ int Locator::unmarshal(const uint8_t *data, const int len) noexcept
 	m_ts = (int64_t) Utils::loadBigEndian<uint64_t>(data);
 	int p = 8;
 
-	int l = m_signer.unmarshal(data + p,len - p);
+	int l = m_signer.unmarshal(data + p, len - p);
 	if (l <= 0)
 		return -1;
 	p += l;
@@ -123,7 +146,7 @@ int Locator::unmarshal(const uint8_t *data, const int len) noexcept
 		return -1;
 	const unsigned int siglen = Utils::loadBigEndian<uint16_t>(data + p);
 	p += 2;
-	if (unlikely((siglen > ZT_SIGNATURE_BUFFER_SIZE) || ((p + (int)siglen) > len)))
+	if (unlikely((siglen > ZT_SIGNATURE_BUFFER_SIZE) || ((p + (int) siglen) > len)))
 		return -1;
 	m_signature.unsafeSetSize(siglen);
 	Utils::copy(m_signature.data(), data + p, siglen);
@@ -148,9 +171,25 @@ ZT_Locator *ZT_Locator_create(
 		if ((ts <= 0) || (!endpoints) || (endpointCount == 0) || (!signer))
 			return nullptr;
 		ZeroTier::Locator *loc = new ZeroTier::Locator();
-		for(unsigned int i=0;i<endpointCount;++i)
+		for (unsigned int i = 0;i < endpointCount;++i)
 			loc->add(reinterpret_cast<const ZeroTier::Endpoint *>(endpoints)[i]);
-		if (!loc->sign(ts,*reinterpret_cast<const ZeroTier::Identity *>(signer))) {
+		if (!loc->sign(ts, *reinterpret_cast<const ZeroTier::Identity *>(signer))) {
+			delete loc;
+			return nullptr;
+		}
+		return reinterpret_cast<ZT_Locator *>(loc);
+	} catch (...) {
+		return nullptr;
+	}
+}
+
+ZT_Locator *ZT_Locator_fromString(const char *str)
+{
+	try {
+		if (!str)
+			return nullptr;
+		ZeroTier::Locator *loc = new ZeroTier::Locator();
+		if (!loc->fromString(str)) {
 			delete loc;
 			return nullptr;
 		}
@@ -168,45 +207,62 @@ ZT_Locator *ZT_Locator_unmarshal(
 		if ((!data) || (len == 0))
 			return nullptr;
 		ZeroTier::Locator *loc = new ZeroTier::Locator();
-		if (loc->unmarshal(reinterpret_cast<const uint8_t *>(data),(int)len) <= 0) {
+		if (loc->unmarshal(reinterpret_cast<const uint8_t *>(data), (int) len) <= 0) {
 			delete loc;
 			return nullptr;
 		}
 		return reinterpret_cast<ZT_Locator *>(loc);
-	} catch ( ... ) {
+	} catch (...) {
 		return nullptr;
 	}
 }
 
-int ZT_Locator_marshal(const ZT_Locator *loc,void *buf,unsigned int bufSize)
+int ZT_Locator_marshal(const ZT_Locator *loc, void *buf, unsigned int bufSize)
 {
 	if ((!loc) || (bufSize < ZT_LOCATOR_MARSHAL_SIZE_MAX))
 		return -1;
-	return reinterpret_cast<const ZeroTier::Locator *>(loc)->marshal(reinterpret_cast<uint8_t *>(buf),(int)bufSize);
+	return reinterpret_cast<const ZeroTier::Locator *>(loc)->marshal(reinterpret_cast<uint8_t *>(buf), (int) bufSize);
+}
+
+char *ZT_Locator_toString(
+	const ZT_Locator *loc,
+	char *buf,
+	int capacity)
+{
+	if ((!loc) || (capacity < ZT_LOCATOR_STRING_SIZE_MAX))
+		return nullptr;
+	return reinterpret_cast<const ZeroTier::Locator *>(loc)->toString(buf);
 }
 
 const ZT_Fingerprint *ZT_Locator_fingerprint(const ZT_Locator *loc)
 {
 	if (!loc)
 		return nullptr;
-	return (ZT_Fingerprint *)(&(reinterpret_cast<const ZeroTier::Locator *>(loc)->signer()));
+	return (ZT_Fingerprint *) (&(reinterpret_cast<const ZeroTier::Locator *>(loc)->signer()));
+}
+
+int64_t ZT_Locator_timestamp(const ZT_Locator *loc)
+{
+	if (!loc)
+		return 0;
+	return reinterpret_cast<const ZeroTier::Locator *>(loc)->timestamp();
 }
 
 unsigned int ZT_Locator_endpointCount(const ZT_Locator *loc)
 {
-	return (loc) ? (unsigned int)(reinterpret_cast<const ZeroTier::Locator *>(loc)->endpoints().size()) : 0;
+	return (loc) ? (unsigned int) (reinterpret_cast<const ZeroTier::Locator *>(loc)->endpoints().size()) : 0;
 }
 
-const ZT_Endpoint *ZT_Locator_endpoint(const ZT_Locator *loc,const unsigned int ep)
+const ZT_Endpoint *ZT_Locator_endpoint(const ZT_Locator *loc, const unsigned int ep)
 {
 	if (!loc)
 		return nullptr;
-	if (ep >= (unsigned int)(reinterpret_cast<const ZeroTier::Locator *>(loc)->endpoints().size()))
+	if (ep >= (unsigned int) (reinterpret_cast<const ZeroTier::Locator *>(loc)->endpoints().size()))
 		return nullptr;
 	return reinterpret_cast<const ZT_Endpoint *>(&(reinterpret_cast<const ZeroTier::Locator *>(loc)->endpoints()[ep]));
 }
 
-int ZT_Locator_verify(const ZT_Locator *loc,const ZT_Identity *signer)
+int ZT_Locator_verify(const ZT_Locator *loc, const ZT_Identity *signer)
 {
 	if ((!loc) || (!signer))
 		return 0;

+ 18 - 4
node/Locator.hpp

@@ -24,6 +24,7 @@
 
 #define ZT_LOCATOR_MAX_ENDPOINTS 8
 #define ZT_LOCATOR_MARSHAL_SIZE_MAX (8 + ZT_FINGERPRINT_MARSHAL_SIZE + 2 + (ZT_LOCATOR_MAX_ENDPOINTS * ZT_ENDPOINT_MARSHAL_SIZE_MAX) + 2 + 2 + ZT_SIGNATURE_BUFFER_SIZE)
+#define ZT_LOCATOR_STRING_SIZE_MAX 4096
 
 namespace ZeroTier {
 
@@ -105,14 +106,27 @@ public:
 	 */
 	bool verify(const Identity &id) const noexcept;
 
+	/**
+	 * Convert this locator to a string
+	 *
+	 * @param s String buffer
+	 * @return Pointer to buffer
+	 */
+	char *toString(char s[ZT_LOCATOR_STRING_SIZE_MAX]) const noexcept;
+
+	/**
+	 * Decode a string format locator
+	 *
+	 * @param s Locator from toString()
+	 * @return True if format was valid
+	 */
+	bool fromString(const char *s) noexcept;
+
 	explicit ZT_INLINE operator bool() const noexcept
 	{ return m_ts > 0; }
 
-	static constexpr int marshalSizeMax() noexcept
-	{ return ZT_LOCATOR_MARSHAL_SIZE_MAX; }
-
+	static constexpr int marshalSizeMax() noexcept { return ZT_LOCATOR_MARSHAL_SIZE_MAX; }
 	int marshal(uint8_t data[ZT_LOCATOR_MARSHAL_SIZE_MAX], bool excludeSignature = false) const noexcept;
-
 	int unmarshal(const uint8_t *data, int len) noexcept;
 
 private: