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"
 	"zerotier/pkg/zerotier"
 )
 )
 
 
-// Help dumps help to stdout
 func Help() {
 func Help() {
 	fmt.Printf(`ZeroTier Network Hypervisor Service Version %d.%d.%d
 	fmt.Printf(`ZeroTier Network Hypervisor Service Version %d.%d.%d
 (c)2013-2020 ZeroTier, Inc.
 (c)2013-2020 ZeroTier, Inc.
@@ -38,11 +37,11 @@ Commands:
   service                               Start as service
   service                               Start as service
   status                                Show node status, identity, and config
   status                                Show node status, identity, and config
   peers                                 List all VL1 peers
   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
   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?
     manageips <boolean>                 Is IP management allowed?
     manageroutes <boolean>              Is route management allowed?
     manageroutes <boolean>              Is route management allowed?
     globalips <boolean>                 Allow assignment of global IPs?
     globalips <boolean>                 Allow assignment of global IPs?
@@ -62,11 +61,20 @@ Commands:
     verify <identity> <file> <sig>      Verify a signature
     verify <identity> <file> <sig>      Verify a signature
   locator <command> [args]              Locator management commands
   locator <command> [args]              Locator management commands
     new <identity> <address> [...]      Create and sign a new locator
     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.
 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
 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
 Identities can be specified verbatim on the command line or as a path to
 a file. This is detected automatically.
 a file. This is detected automatically.
-
 `,zerotier.CoreVersionMajor, zerotier.CoreVersionMinor, zerotier.CoreVersionRevision)
 `,zerotier.CoreVersionMajor, zerotier.CoreVersionMinor, zerotier.CoreVersionRevision)
 }
 }

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

@@ -23,7 +23,6 @@ import (
 	"zerotier/pkg/zerotier"
 	"zerotier/pkg/zerotier"
 )
 )
 
 
-// Identity command
 func Identity(args []string) {
 func Identity(args []string) {
 	if len(args) > 0 {
 	if len(args) > 0 {
 		switch 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)
 	idData, err := ioutil.ReadFile(s)
 	if err != nil {
 	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)
 		os.Exit(1)
 	}
 	}
 	id, err := zerotier.NewIdentityFromString(string(idData))
 	id, err := zerotier.NewIdentityFromString(string(idData))
 	if err != nil {
 	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)
 		os.Exit(1)
 	}
 	}
 	return id
 	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 {
 func networkStatusStr(status int) string {
 	switch status {
 	switch status {
 	case zerotier.NetworkStatusNotFound:
 	case zerotier.NetworkStatusNotFound:
 		return "NOTFOUND"
 		return "NOTFOUND"
 	case zerotier.NetworkStatusAccessDenied:
 	case zerotier.NetworkStatusAccessDenied:
-		return "DENIED"
+		return "ACCESSDENIED"
 	case zerotier.NetworkStatusRequestConfiguration:
 	case zerotier.NetworkStatusRequestConfiguration:
 		return "UPDATING"
 		return "UPDATING"
 	case zerotier.NetworkStatusOK:
 	case zerotier.NetworkStatusOK:

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

@@ -21,7 +21,6 @@ import (
 	"zerotier/pkg/zerotier"
 	"zerotier/pkg/zerotier"
 )
 )
 
 
-// Network CLI command
 func Network(basePath, authToken string, args []string, jsonOutput bool) {
 func Network(basePath, authToken string, args []string, jsonOutput bool) {
 	if len(args) != 1 {
 	if len(args) != 1 {
 		Help()
 		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":
 	case "peers", "listpeers", "lspeers":
 		authTokenRequired(authToken)
 		authTokenRequired(authToken)
 		cli.Peers(basePath, authToken, cmdArgs, *jflag, false)
 		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":
 	case "join":
 		authTokenRequired(authToken)
 		authTokenRequired(authToken)
 		cli.Join(basePath, authToken, cmdArgs)
 		cli.Join(basePath, authToken, cmdArgs)
@@ -151,8 +144,19 @@ func main() {
 		cli.Set(basePath, authToken, cmdArgs)
 		cli.Set(basePath, authToken, cmdArgs)
 	case "identity":
 	case "identity":
 		cli.Identity(cmdArgs)
 		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()
 	cli.Help()
 	os.Exit(1)
 	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 _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 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 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 "C"
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
-	"fmt"
+	"strings"
 	"unsafe"
 	"unsafe"
 )
 )
 
 
@@ -29,6 +30,42 @@ type Endpoint struct {
 	cep C.ZT_Endpoint
 	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.
 // Type returns this endpoint's type.
 func (ep *Endpoint) Type() int {
 func (ep *Endpoint) Type() int {
 	return int(ep.cep._type)
 	return int(ep.cep._type)
@@ -77,15 +114,12 @@ func (ep *Endpoint) MAC() MAC {
 }
 }
 
 
 func (ep *Endpoint) String() string {
 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) {
 func (ep *Endpoint) MarshalJSON() ([]byte, error) {
@@ -94,6 +128,15 @@ func (ep *Endpoint) MarshalJSON() ([]byte, error) {
 }
 }
 
 
 func (ep *Endpoint) UnmarshalJSON(j []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
 	return nil
 }
 }

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

@@ -32,7 +32,7 @@ func NewFingerprintFromString(fps string) (*Fingerprint, error) {
 	if len(fps) < AddressStringLength {
 	if len(fps) < AddressStringLength {
 		return nil, ErrInvalidZeroTierAddress
 		return nil, ErrInvalidZeroTierAddress
 	}
 	}
-	ss := strings.Split(fps, "/")
+	ss := strings.Split(fps, "-")
 	if len(ss) < 1 || len(ss) > 2 {
 	if len(ss) < 1 || len(ss) > 2 {
 		return nil, ErrInvalidParameter
 		return nil, ErrInvalidParameter
 	}
 	}
@@ -67,7 +67,7 @@ func newFingerprintFromCFingerprint(cfp *C.ZT_Fingerprint) *Fingerprint {
 
 
 func (fp *Fingerprint) String() string {
 func (fp *Fingerprint) String() string {
 	if len(fp.Hash) == 48 {
 	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()
 	return fp.Address.String()
 }
 }

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

@@ -64,9 +64,26 @@ func NewLocatorFromBytes(lb []byte) (*Locator, error) {
 	return goloc, nil
 	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.
 // 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.
 // 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)
 	cfp := C.ZT_Locator_fingerprint(loc.cl)
 	if uintptr(unsafe.Pointer(cfp)) == 0 {
 	if uintptr(unsafe.Pointer(cfp)) == 0 {
 		err = ErrInternal
 		err = ErrInternal
@@ -84,3 +101,14 @@ func (loc *Locator) GetInfo(id *Identity) (fp *Fingerprint, eps []Endpoint, vali
 	}
 	}
 	return
 	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
  * Create and sign a new locator
  *
  *
@@ -2149,6 +2160,14 @@ ZT_SDK_API ZT_Locator *ZT_Locator_unmarshal(
 	const void *data,
 	const void *data,
 	unsigned int len);
 	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
  * 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);
 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.
  * 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);
 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
  * Get the number of endpoints in this locator
  *
  *

+ 30 - 5
node/Endpoint.cpp

@@ -16,7 +16,7 @@
 
 
 namespace ZeroTier {
 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;
 	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;
 			break;
 		case ZT_ENDPOINT_TYPE_ZEROTIER:
 		case ZT_ENDPOINT_TYPE_ZEROTIER:
 			s[0] = s_endpointTypeChars[ZT_ENDPOINT_TYPE_ZEROTIER];
 			s[0] = s_endpointTypeChars[ZT_ENDPOINT_TYPE_ZEROTIER];
-			s[1] = '/';
+			s[1] = '-';
 			zt().toString(s + 2);
 			zt().toString(s + 2);
 			break;
 			break;
 		case ZT_ENDPOINT_TYPE_ETHERNET:
 		case ZT_ENDPOINT_TYPE_ETHERNET:
 		case ZT_ENDPOINT_TYPE_WIFI_DIRECT:
 		case ZT_ENDPOINT_TYPE_WIFI_DIRECT:
 		case ZT_ENDPOINT_TYPE_BLUETOOTH:
 		case ZT_ENDPOINT_TYPE_BLUETOOTH:
 			s[0] = s_endpointTypeChars[this->type];
 			s[0] = s_endpointTypeChars[this->type];
-			s[1] = '/';
+			s[1] = '-';
 			eth().toString(s + 2);
 			eth().toString(s + 2);
 			break;
 			break;
 		case ZT_ENDPOINT_TYPE_IP:
 		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_TCP:
 		case ZT_ENDPOINT_TYPE_IP_HTTP2:
 		case ZT_ENDPOINT_TYPE_IP_HTTP2:
 			s[0] = s_endpointTypeChars[this->type];
 			s[0] = s_endpointTypeChars[this->type];
-			s[1] = '/';
+			s[1] = '-';
 			ip().toString(s + 2);
 			ip().toString(s + 2);
 			break;
 			break;
 	}
 	}
+
+	return s;
 }
 }
 
 
 bool Endpoint::fromString(const char *s) noexcept
 bool Endpoint::fromString(const char *s) noexcept
@@ -57,7 +59,7 @@ bool Endpoint::fromString(const char *s) noexcept
 	if ((!s) || (!*s))
 	if ((!s) || (!*s))
 		return true;
 		return true;
 
 
-	const char *start = strchr(s, '/');
+	const char *start = strchr(s, '-');
 	if (start) {
 	if (start) {
 		char tmp[16];
 		char tmp[16];
 		for (unsigned int i = 0;i < 15;++i) {
 		for (unsigned int i = 0;i < 15;++i) {
@@ -248,3 +250,26 @@ bool Endpoint::operator<(const Endpoint &ep) const noexcept
 }
 }
 
 
 } // namespace ZeroTier
 } // 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
 	ZT_INLINE Fingerprint zt() const noexcept
 	{ return Fingerprint(this->value.fp); }
 	{ 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;
 	bool fromString(const char *s) noexcept;
 
 

+ 1 - 1
node/Fingerprint.hpp

@@ -54,7 +54,7 @@ public:
 	{
 	{
 		Address(this->address).toString(s);
 		Address(this->address).toString(s);
 		if (haveHash()) {
 		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));
 			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);
 			const unsigned int signlen = marshal(signdata, true);
 			return id.verify(signdata, signlen, m_signature.data(), m_signature.size());
 			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;
 	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
 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);
 	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);
 	m_ts = (int64_t) Utils::loadBigEndian<uint64_t>(data);
 	int p = 8;
 	int p = 8;
 
 
-	int l = m_signer.unmarshal(data + p,len - p);
+	int l = m_signer.unmarshal(data + p, len - p);
 	if (l <= 0)
 	if (l <= 0)
 		return -1;
 		return -1;
 	p += l;
 	p += l;
@@ -123,7 +146,7 @@ int Locator::unmarshal(const uint8_t *data, const int len) noexcept
 		return -1;
 		return -1;
 	const unsigned int siglen = Utils::loadBigEndian<uint16_t>(data + p);
 	const unsigned int siglen = Utils::loadBigEndian<uint16_t>(data + p);
 	p += 2;
 	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;
 		return -1;
 	m_signature.unsafeSetSize(siglen);
 	m_signature.unsafeSetSize(siglen);
 	Utils::copy(m_signature.data(), data + p, 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))
 		if ((ts <= 0) || (!endpoints) || (endpointCount == 0) || (!signer))
 			return nullptr;
 			return nullptr;
 		ZeroTier::Locator *loc = new ZeroTier::Locator();
 		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]);
 			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;
 			delete loc;
 			return nullptr;
 			return nullptr;
 		}
 		}
@@ -168,45 +207,62 @@ ZT_Locator *ZT_Locator_unmarshal(
 		if ((!data) || (len == 0))
 		if ((!data) || (len == 0))
 			return nullptr;
 			return nullptr;
 		ZeroTier::Locator *loc = new ZeroTier::Locator();
 		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;
 			delete loc;
 			return nullptr;
 			return nullptr;
 		}
 		}
 		return reinterpret_cast<ZT_Locator *>(loc);
 		return reinterpret_cast<ZT_Locator *>(loc);
-	} catch ( ... ) {
+	} catch (...) {
 		return nullptr;
 		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))
 	if ((!loc) || (bufSize < ZT_LOCATOR_MARSHAL_SIZE_MAX))
 		return -1;
 		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)
 const ZT_Fingerprint *ZT_Locator_fingerprint(const ZT_Locator *loc)
 {
 {
 	if (!loc)
 	if (!loc)
 		return nullptr;
 		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)
 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)
 	if (!loc)
 		return nullptr;
 		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 nullptr;
 	return reinterpret_cast<const ZT_Endpoint *>(&(reinterpret_cast<const ZeroTier::Locator *>(loc)->endpoints()[ep]));
 	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))
 	if ((!loc) || (!signer))
 		return 0;
 		return 0;

+ 18 - 4
node/Locator.hpp

@@ -24,6 +24,7 @@
 
 
 #define ZT_LOCATOR_MAX_ENDPOINTS 8
 #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_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 {
 namespace ZeroTier {
 
 
@@ -105,14 +106,27 @@ public:
 	 */
 	 */
 	bool verify(const Identity &id) const noexcept;
 	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
 	explicit ZT_INLINE operator bool() const noexcept
 	{ return m_ts > 0; }
 	{ 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 marshal(uint8_t data[ZT_LOCATOR_MARSHAL_SIZE_MAX], bool excludeSignature = false) const noexcept;
-
 	int unmarshal(const uint8_t *data, int len) noexcept;
 	int unmarshal(const uint8_t *data, int len) noexcept;
 
 
 private:
 private: