From c3e0f262d1c501d46a5feac6aa911f1e59b4b3fb Mon Sep 17 00:00:00 2001
From: Adam Ierymenko <adam.ierymenko@zerotier.com>
Date: Wed, 2 Oct 2019 09:34:44 -0700
Subject: [PATCH] Regularize JSON stuff

---
 go/cmd/zerotier/cli/network.go    |   4 +-
 go/cmd/zerotier/cli/networks.go   |   2 +-
 go/pkg/zerotier/address.go        |   2 +-
 go/pkg/zerotier/api.go            | 135 ++++++++++++++++++------------
 go/pkg/zerotier/errors.go         |   7 ++
 go/pkg/zerotier/localconfig.go    |  28 +++----
 go/pkg/zerotier/locator.go        |  14 ++--
 go/pkg/zerotier/multicastgroup.go |   4 +-
 go/pkg/zerotier/nativetap.go      |  26 ++++--
 go/pkg/zerotier/path.go           |  28 +++----
 go/pkg/zerotier/peer.go           |  12 +--
 go/pkg/zerotier/root.go           |   4 +-
 go/pkg/zerotier/route.go          |  12 +--
 13 files changed, 164 insertions(+), 114 deletions(-)

diff --git a/go/cmd/zerotier/cli/network.go b/go/cmd/zerotier/cli/network.go
index bce85cc5f..54bb7683b 100644
--- a/go/cmd/zerotier/cli/network.go
+++ b/go/cmd/zerotier/cli/network.go
@@ -48,7 +48,7 @@ func Network(basePath, authToken string, args []string, jsonOutput bool) {
 		fmt.Printf("%s: %s\n", nwids, network.Config.Name)
 		fmt.Printf("\tstatus:\t%s\n", networkStatusStr(network.Config.Status))
 		enabled := "no"
-		if network.TapDeviceEnabled {
+		if network.PortEnabled {
 			enabled = "yes"
 		}
 		bridge := "no"
@@ -59,7 +59,7 @@ func Network(basePath, authToken string, args []string, jsonOutput bool) {
 		if network.Config.BroadcastEnabled {
 			broadcast = "on"
 		}
-		fmt.Printf("\tport:\t%s dev %s type %s mtu %d enabled %s bridge %s broadcast %s\n", network.Config.MAC.String(), network.TapDeviceName, network.TapDeviceType, network.Config.MTU, enabled, bridge, broadcast)
+		fmt.Printf("\tport:\t%s dev %s type %s mtu %d enabled %s bridge %s broadcast %s\n", network.Config.MAC.String(), network.PortName, network.PortType, network.Config.MTU, enabled, bridge, broadcast)
 		fmt.Printf("\tmanaged addresses:\t")
 		for i, a := range network.Config.AssignedAddresses {
 			if i > 0 {
diff --git a/go/cmd/zerotier/cli/networks.go b/go/cmd/zerotier/cli/networks.go
index 8abf36486..46d39e728 100644
--- a/go/cmd/zerotier/cli/networks.go
+++ b/go/cmd/zerotier/cli/networks.go
@@ -34,7 +34,7 @@ func Networks(basePath, authToken string, args []string, jsonOutput bool) {
 			if nw.Config.Type == zerotier.NetworkTypePublic {
 				t = "PUBLIC"
 			}
-			fmt.Printf("%.16x %-24s %-17s %-16s %-7s %-16s ", uint64(nw.ID), nw.Config.Name, nw.Config.MAC.String(), networkStatusStr(nw.Config.Status), t, nw.TapDeviceName)
+			fmt.Printf("%.16x %-24s %-17s %-16s %-7s %-16s ", uint64(nw.ID), nw.Config.Name, nw.Config.MAC.String(), networkStatusStr(nw.Config.Status), t, nw.PortName)
 			for i, ip := range nw.Config.AssignedAddresses {
 				if i > 0 {
 					fmt.Print(',')
diff --git a/go/pkg/zerotier/address.go b/go/pkg/zerotier/address.go
index 5131bf290..95d3e133b 100644
--- a/go/pkg/zerotier/address.go
+++ b/go/pkg/zerotier/address.go
@@ -49,7 +49,7 @@ func (a Address) String() string {
 
 // MarshalJSON marshals this Address as a string
 func (a Address) MarshalJSON() ([]byte, error) {
-	return []byte("\"" + a.String() + "\""), nil
+	return []byte(fmt.Sprintf("\"%.10x\"", uint64(a))), nil
 }
 
 // UnmarshalJSON unmarshals this Address from a string
diff --git a/go/pkg/zerotier/api.go b/go/pkg/zerotier/api.go
index 887e16e02..5d72f8ddc 100644
--- a/go/pkg/zerotier/api.go
+++ b/go/pkg/zerotier/api.go
@@ -22,6 +22,7 @@ import (
 	"net"
 	"net/http"
 	"path"
+	"runtime"
 	"strings"
 	"time"
 
@@ -31,6 +32,8 @@ import (
 // APISocketName is the default socket name for accessing the API
 const APISocketName = "apisocket"
 
+var startTime = TimeMs()
+
 // APIGet makes a query to the API via a Unix domain or windows pipe socket
 func APIGet(basePath, socketName, authToken, queryPath string, obj interface{}) (int, error) {
 	client, err := createNamedSocketHTTPClient(basePath, socketName)
@@ -106,31 +109,38 @@ func APIDelete(basePath, socketName, authToken, queryPath string, result interfa
 
 // APIStatus is the object returned by API status inquiries
 type APIStatus struct {
-	Address                 Address
-	Clock                   int64
-	Config                  LocalConfig
-	Online                  bool
-	PeerCount               int
-	PathCount               int
-	Identity                *Identity
-	InterfaceAddresses      []net.IP       `json:",omitempty"`
-	MappedExternalAddresses []*InetAddress `json:",omitempty"`
-	Version                 string
-	VersionMajor            int
-	VersionMinor            int
-	VersionRevision         int
-	VersionBuild            int
+	Address                 Address        `json:"address"`
+	Clock                   int64          `json:"clock"`
+	StartupTime             int64          `json:"startupTime"`
+	Config                  LocalConfig    `json:"config"`
+	Online                  bool           `json:"online"`
+	PeerCount               int            `json:"peerCount"`
+	PathCount               int            `json:"pathCount"`
+	Identity                *Identity      `json:"identity"`
+	InterfaceAddresses      []net.IP       `json:"interfaceAddresses,omitempty"`
+	MappedExternalAddresses []*InetAddress `json:"mappedExternalAddresses,omitempty"`
+	Version                 string         `json:"version"`
+	VersionMajor            int            `json:"versionMajor"`
+	VersionMinor            int            `json:"versionMinor"`
+	VersionRevision         int            `json:"versionRevision"`
+	VersionBuild            int            `json:"versionBuild"`
+	OS                      string         `json:"os"`
+	Architecture            string         `json:"architecture"`
+	Concurrency             int            `json:"cpus"`
+	Runtime                 string         `json:"runtimeVersion"`
 }
 
 // APINetwork is the object returned by API network inquiries
 type APINetwork struct {
-	ID                     NetworkID
-	Config                 NetworkConfig
-	Settings               *NetworkLocalSettings `json:",omitempty"`
-	MulticastSubscriptions []*MulticastGroup     `json:",omitempty"`
-	TapDeviceType          string
-	TapDeviceName          string
-	TapDeviceEnabled       bool
+	ID                     NetworkID             `json:"id"`
+	Config                 NetworkConfig         `json:"config"`
+	Settings               *NetworkLocalSettings `json:"settings,omitempty"`
+	MulticastSubscriptions []*MulticastGroup     `json:"multicastSubscriptions,omitempty"`
+	PortType               string                `json:"portType"`
+	PortName               string                `json:"portName"`
+	PortEnabled            bool                  `json:"portEnabled"`
+	PortErrorCode          int                   `json:"portErrorCode"`
+	PortError              string                `json:"portError"`
 }
 
 func apiNetworkFromNetwork(n *Network) *APINetwork {
@@ -140,19 +150,21 @@ func apiNetworkFromNetwork(n *Network) *APINetwork {
 	ls := n.LocalSettings()
 	nn.Settings = &ls
 	nn.MulticastSubscriptions = n.MulticastSubscriptions()
-	nn.TapDeviceType = n.Tap().Type()
-	nn.TapDeviceName = n.Tap().DeviceName()
-	nn.TapDeviceEnabled = n.Tap().Enabled()
+	nn.PortType = n.Tap().Type()
+	nn.PortName = n.Tap().DeviceName()
+	nn.PortEnabled = n.Tap().Enabled()
+	ec, errStr := n.Tap().Error()
+	nn.PortErrorCode = ec
+	nn.PortError = errStr
 	return &nn
 }
 
 func apiSetStandardHeaders(out http.ResponseWriter) {
-	now := time.Now().UTC()
 	h := out.Header()
 	h.Set("Cache-Control", "no-cache, no-store, must-revalidate")
 	h.Set("Expires", "0")
 	h.Set("Pragma", "no-cache")
-	h.Set("Date", now.Format(time.RFC1123))
+	h.Set("Date", time.Now().UTC().Format(time.RFC1123))
 }
 
 func apiSendObj(out http.ResponseWriter, req *http.Request, httpStatusCode int, obj interface{}) error {
@@ -178,7 +190,7 @@ func apiSendObj(out http.ResponseWriter, req *http.Request, httpStatusCode int,
 func apiReadObj(out http.ResponseWriter, req *http.Request, dest interface{}) (err error) {
 	err = json.NewDecoder(req.Body).Decode(&dest)
 	if err != nil {
-		_ = apiSendObj(out, req, http.StatusBadRequest, nil)
+		_ = apiSendObj(out, req, http.StatusBadRequest, &APIErr{"invalid JSON: " + err.Error()})
 	}
 	return
 }
@@ -192,7 +204,7 @@ func apiCheckAuth(out http.ResponseWriter, req *http.Request, token string) bool
 	if len(ah) > 0 && strings.TrimSpace(ah) == token {
 		return true
 	}
-	_ = apiSendObj(out, req, http.StatusUnauthorized, nil)
+	_ = apiSendObj(out, req, http.StatusUnauthorized, &APIErr{"authorization token not found or incorrect (checked X-ZT1-Auth and Authorization headers)"})
 	return false
 }
 
@@ -223,6 +235,8 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e
 
 	smux := http.NewServeMux()
 
+	////////////////////////////////////////////////////////////////////////////
+
 	smux.HandleFunc("/status", func(out http.ResponseWriter, req *http.Request) {
 		defer func() {
 			e := recover()
@@ -234,8 +248,8 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e
 		if !apiCheckAuth(out, req, authToken) {
 			return
 		}
-
 		apiSetStandardHeaders(out)
+
 		if req.Method == http.MethodGet || req.Method == http.MethodHead {
 			pathCount := 0
 			peers := node.Peers()
@@ -245,6 +259,7 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e
 			_ = apiSendObj(out, req, http.StatusOK, &APIStatus{
 				Address:                 node.Address(),
 				Clock:                   TimeMs(),
+				StartupTime:             startTime,
 				Config:                  node.LocalConfig(),
 				Online:                  node.Online(),
 				PeerCount:               len(peers),
@@ -257,34 +272,41 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e
 				VersionMinor:            CoreVersionMinor,
 				VersionRevision:         CoreVersionRevision,
 				VersionBuild:            CoreVersionBuild,
+				OS:                      runtime.GOOS,
+				Architecture:            runtime.GOARCH,
+				Concurrency:             runtime.NumCPU(),
+				Runtime:                 runtime.Version(),
 			})
 		} else {
 			out.Header().Set("Allow", "GET, HEAD")
-			_ = apiSendObj(out, req, http.StatusMethodNotAllowed, nil)
+			_ = apiSendObj(out, req, http.StatusMethodNotAllowed, &APIErr{"/status is read-only"})
 		}
 	})
 
+	////////////////////////////////////////////////////////////////////////////
+
 	smux.HandleFunc("/config", func(out http.ResponseWriter, req *http.Request) {
 		defer func() {
 			e := recover()
 			if e != nil {
-				_ = apiSendObj(out, req, http.StatusInternalServerError, nil)
+				_ = apiSendObj(out, req, http.StatusInternalServerError, &APIErr{"caught unexpected error in request handler"})
 			}
 		}()
 
 		if !apiCheckAuth(out, req, authToken) {
 			return
 		}
-
 		apiSetStandardHeaders(out)
+
 		if req.Method == http.MethodPost || req.Method == http.MethodPut {
 			var c LocalConfig
 			if apiReadObj(out, req, &c) == nil {
 				_, err := node.SetLocalConfig(&c)
 				if err != nil {
-					_ = apiSendObj(out, req, http.StatusBadRequest, nil)
+					_ = apiSendObj(out, req, http.StatusBadRequest, &APIErr{"error applying local config: " + err.Error()})
 				} else {
-					_ = apiSendObj(out, req, http.StatusOK, node.LocalConfig())
+					lc := node.LocalConfig()
+					_ = apiSendObj(out, req, http.StatusOK, &lc)
 				}
 			}
 		} else if req.Method == http.MethodGet || req.Method == http.MethodHead {
@@ -295,18 +317,19 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e
 		}
 	})
 
+	////////////////////////////////////////////////////////////////////////////
+
 	smux.HandleFunc("/peer/", func(out http.ResponseWriter, req *http.Request) {
 		defer func() {
 			e := recover()
 			if e != nil {
-				_ = apiSendObj(out, req, http.StatusInternalServerError, nil)
+				_ = apiSendObj(out, req, http.StatusInternalServerError, &APIErr{"caught unexpected error in request handler"})
 			}
 		}()
 
 		if !apiCheckAuth(out, req, authToken) {
 			return
 		}
-
 		apiSetStandardHeaders(out)
 
 		var queriedID Address
@@ -314,7 +337,7 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e
 			var err error
 			queriedID, err = NewAddressFromString(req.URL.Path[6:])
 			if err != nil {
-				_ = apiSendObj(out, req, http.StatusNotFound, nil)
+				_ = apiSendObj(out, req, http.StatusNotFound, &APIErr{"peer not found"})
 				return
 			}
 		}
@@ -328,28 +351,29 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e
 						return
 					}
 				}
-				_ = apiSendObj(out, req, http.StatusNotFound, nil)
+				_ = apiSendObj(out, req, http.StatusNotFound, &APIErr{"peer not found"})
 			} else {
 				_ = apiSendObj(out, req, http.StatusOK, peers)
 			}
 		} else {
 			out.Header().Set("Allow", "GET, HEAD")
-			_ = apiSendObj(out, req, http.StatusMethodNotAllowed, nil)
+			_ = apiSendObj(out, req, http.StatusMethodNotAllowed, &APIErr{"peers are read only"})
 		}
 	})
 
+	////////////////////////////////////////////////////////////////////////////
+
 	smux.HandleFunc("/network/", func(out http.ResponseWriter, req *http.Request) {
 		defer func() {
 			e := recover()
 			if e != nil {
-				_ = apiSendObj(out, req, http.StatusInternalServerError, nil)
+				_ = apiSendObj(out, req, http.StatusInternalServerError, &APIErr{"caught unexpected error in request handler"})
 			}
 		}()
 
 		if !apiCheckAuth(out, req, authToken) {
 			return
 		}
-
 		apiSetStandardHeaders(out)
 
 		var queriedID NetworkID
@@ -374,7 +398,7 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e
 						return
 					}
 				}
-				_ = apiSendObj(out, req, http.StatusNotFound, nil)
+				_ = apiSendObj(out, req, http.StatusNotFound, &APIErr{"network not found"})
 			}
 		} else if req.Method == http.MethodPost || req.Method == http.MethodPut {
 			if queriedID == 0 {
@@ -386,7 +410,7 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e
 					if n == nil {
 						n, err := node.Join(nw.ID, nw.Settings, nil)
 						if err != nil {
-							_ = apiSendObj(out, req, http.StatusBadRequest, nil)
+							_ = apiSendObj(out, req, http.StatusBadRequest, &APIErr{"only individual networks can be added or modified with POST/PUT"})
 						} else {
 							_ = apiSendObj(out, req, http.StatusOK, apiNetworkFromNetwork(n))
 						}
@@ -413,26 +437,27 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e
 						return
 					}
 				}
-				_ = apiSendObj(out, req, http.StatusNotFound, nil)
+				_ = apiSendObj(out, req, http.StatusNotFound, &APIErr{"network not found"})
 			}
 		} else {
 			out.Header().Set("Allow", "GET, HEAD, PUT, POST, DELETE")
-			_ = apiSendObj(out, req, http.StatusMethodNotAllowed, nil)
+			_ = apiSendObj(out, req, http.StatusMethodNotAllowed, &APIErr{"unsupported method " + req.Method})
 		}
 	})
 
+	////////////////////////////////////////////////////////////////////////////
+
 	smux.HandleFunc("/root/", func(out http.ResponseWriter, req *http.Request) {
 		defer func() {
 			e := recover()
 			if e != nil {
-				_ = apiSendObj(out, req, http.StatusInternalServerError, nil)
+				_ = apiSendObj(out, req, http.StatusInternalServerError, &APIErr{"caught unexpected error in request handler"})
 			}
 		}()
 
 		if !apiCheckAuth(out, req, authToken) {
 			return
 		}
-
 		apiSetStandardHeaders(out)
 
 		var queriedName string
@@ -453,15 +478,19 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e
 			}
 			_ = apiSendObj(out, req, http.StatusNotFound, nil)
 		} else if req.Method == http.MethodPost || req.Method == http.MethodPut {
+			if len(queriedName) == 0 {
+				_ = apiSendObj(out, req, http.StatusBadRequest, &APIErr{"only individual roots can be added or modified with POST/PUT"})
+				return
+			}
 			var r Root
 			if apiReadObj(out, req, &r) == nil {
 				if r.Name != queriedName {
-					_ = apiSendObj(out, req, http.StatusBadRequest, nil)
+					_ = apiSendObj(out, req, http.StatusBadRequest, &APIErr{"root name does not match name in path"})
 					return
 				}
 				err := node.SetRoot(r.Name, r.Locator)
 				if err != nil {
-					_ = apiSendObj(out, req, http.StatusBadRequest, nil)
+					_ = apiSendObj(out, req, http.StatusBadRequest, &APIErr{"set/update root failed: " + err.Error()})
 				} else {
 					roots := node.Roots()
 					for _, r := range roots {
@@ -470,7 +499,7 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e
 							return
 						}
 					}
-					_ = apiSendObj(out, req, http.StatusNotFound, nil)
+					_ = apiSendObj(out, req, http.StatusNotFound, &APIErr{"set/update root failed: root set but not subsequently found in list"})
 				}
 			}
 		} else if req.Method == http.MethodGet || req.Method == http.MethodHead {
@@ -481,13 +510,15 @@ func createAPIServer(basePath string, node *Node) (*http.Server, *http.Server, e
 					return
 				}
 			}
-			_ = apiSendObj(out, req, http.StatusNotFound, nil)
+			_ = apiSendObj(out, req, http.StatusNotFound, &APIErr{"root not found"})
 		} else {
 			out.Header().Set("Allow", "GET, HEAD, PUT, POST, DELETE")
-			_ = apiSendObj(out, req, http.StatusMethodNotAllowed, nil)
+			_ = apiSendObj(out, req, http.StatusMethodNotAllowed, &APIErr{"unsupported method: " + req.Method})
 		}
 	})
 
+	////////////////////////////////////////////////////////////////////////////
+
 	listener, err := createNamedSocketListener(basePath, APISocketName)
 	if err != nil {
 		return nil, nil, err
diff --git a/go/pkg/zerotier/errors.go b/go/pkg/zerotier/errors.go
index 5dedc87fd..f9716f9f1 100644
--- a/go/pkg/zerotier/errors.go
+++ b/go/pkg/zerotier/errors.go
@@ -32,3 +32,10 @@ const (
 	ErrInvalidSignature         Err = "invalid signature"
 	ErrSecretKeyRequired        Err = "secret key required"
 )
+
+// APIErr is returned by the JSON API when a call fails
+type APIErr struct {
+	Reason string
+}
+
+func (e *APIErr) Error() string { return e.Reason }
diff --git a/go/pkg/zerotier/localconfig.go b/go/pkg/zerotier/localconfig.go
index 632c58d12..c5a210aae 100644
--- a/go/pkg/zerotier/localconfig.go
+++ b/go/pkg/zerotier/localconfig.go
@@ -39,49 +39,49 @@ type LocalConfigVirtualAddressConfiguration struct {
 // LocalConfigSettings contains node settings
 type LocalConfigSettings struct {
 	// PrimaryPort is the main UDP port and must be set (defaults to 9993)
-	PrimaryPort int
+	PrimaryPort int `json:"primaryPort"`
 
 	// SecondaryPort is the secondary UDP port, set to 0 to disbale (picked at random by default)
-	SecondaryPort int
+	SecondaryPort int `json:"secondaryPort"`
 
 	// TertiaryPort is a third UDP port, set to 0 to disable (picked at random by default)
-	TertiaryPort int
+	TertiaryPort int `json:"tertiaryPort"`
 
 	// PortSearch causes ZeroTier to try other ports automatically if it can't bind to configured ports
-	PortSearch bool
+	PortSearch bool `json:"portSearch"`
 
 	// PortMapping enables uPnP and NAT-PMP support
-	PortMapping bool
+	PortMapping bool `json:"portMapping"`
 
 	// LogSizeMax is the maximum size of the log in kilobytes or 0 for no limit and -1 to disable logging
-	LogSizeMax int
+	LogSizeMax int `json:"logSizeMax"`
 
 	// MultipathMode sets the multipath link aggregation mode
-	MuiltipathMode int
+	MuiltipathMode int `json:"multipathMode"`
 
 	// IP/port to bind for TCP access to control API (disabled if null)
-	APITCPBindAddress *InetAddress `json:",omitempty"`
+	APITCPBindAddress *InetAddress `json:"apiTCPBindAddress,omitempty"`
 
 	// InterfacePrefixBlacklist are prefixes of physical network interface names that won't be used by ZeroTier (e.g. "lo" or "utun")
-	InterfacePrefixBlacklist []string `json:",omitempty"`
+	InterfacePrefixBlacklist []string `json:"interfacePrefixBlacklist,omitempty"`
 
 	// ExplicitAddresses are explicit IP/port addresses to advertise to other nodes, such as externally mapped ports on a router
-	ExplicitAddresses []*InetAddress `json:",omitempty"`
+	ExplicitAddresses []*InetAddress `json:"explicitAddresses,omitempty"`
 }
 
 // LocalConfig is the local.conf file and stores local settings for the node.
 type LocalConfig struct {
 	// Physical path configurations by CIDR IP/bits
-	Physical map[string]*LocalConfigPhysicalPathConfiguration `json:",omitempty"`
+	Physical map[string]*LocalConfigPhysicalPathConfiguration `json:"physical,omitempty"`
 
 	// Virtual node specific configurations by 10-digit hex ZeroTier address
-	Virtual map[Address]*LocalConfigVirtualAddressConfiguration `json:",omitempty"`
+	Virtual map[Address]*LocalConfigVirtualAddressConfiguration `json:"virtual,omitempty"`
 
 	// Network local configurations by 16-digit hex ZeroTier network ID
-	Network map[NetworkID]*NetworkLocalSettings `json:",omitempty"`
+	Network map[NetworkID]*NetworkLocalSettings `json:"network,omitempty"`
 
 	// LocalConfigSettings contains other local settings for this node
-	Settings LocalConfigSettings `json:",omitempty"`
+	Settings LocalConfigSettings `json:"settings,omitempty"`
 }
 
 // Read this local config from a file, initializing to defaults if the file does not exist
diff --git a/go/pkg/zerotier/locator.go b/go/pkg/zerotier/locator.go
index 8c3beaaa2..ae9298362 100644
--- a/go/pkg/zerotier/locator.go
+++ b/go/pkg/zerotier/locator.go
@@ -24,8 +24,8 @@ import (
 
 // LocatorDNSSigningKey is the public (as a secure DNS name) and private keys for entering locators into DNS
 type LocatorDNSSigningKey struct {
-	SecureDNSName string
-	PrivateKey    []byte
+	SecureDNSName string `json:"secureDNSName"`
+	PrivateKey    []byte `json:"privateKey"`
 }
 
 // NewLocatorDNSSigningKey creates a new signing key and secure DNS name for storing locators in DNS
@@ -47,16 +47,16 @@ func NewLocatorDNSSigningKey() (*LocatorDNSSigningKey, error) {
 // and the others are always reconstructed from it.
 type Locator struct {
 	// Identity is the full identity of the node being located
-	Identity *Identity
+	Identity *Identity `json:"identity"`
 
 	// Physical is a list of static physical network addresses for this node
-	Physical []*InetAddress
+	Physical []*InetAddress `json:"physical,omitempty"`
 
 	// Virtual is a list of ZeroTier nodes that can relay to this node
-	Virtual []*Identity
+	Virtual []*Identity `json:"virtual,omitempty"`
 
 	// Bytes is the raw serialized Locator
-	Bytes []byte
+	Bytes []byte `json:"bytes,omitempty"`
 }
 
 // NewLocator creates a new locator with the given identity and addresses and the current time as timestamp.
@@ -172,7 +172,7 @@ func (l *Locator) MakeTXTRecords(key *LocatorDNSSigningKey) ([]string, error) {
 }
 
 type locatorForUnmarshal struct {
-	Bytes []byte
+	Bytes []byte `json:"bytes,omitempty"`
 }
 
 // UnmarshalJSON unmarshals this Locator from a byte array in JSON.
diff --git a/go/pkg/zerotier/multicastgroup.go b/go/pkg/zerotier/multicastgroup.go
index 27b30ccb8..d85713ff7 100644
--- a/go/pkg/zerotier/multicastgroup.go
+++ b/go/pkg/zerotier/multicastgroup.go
@@ -17,8 +17,8 @@ import "fmt"
 
 // MulticastGroup represents a normal Ethernet multicast or broadcast address plus 32 additional ZeroTier-specific bits
 type MulticastGroup struct {
-	MAC MAC
-	ADI uint32
+	MAC MAC    `json:"mac"`
+	ADI uint32 `json:"adi"`
 }
 
 // String returns MAC#ADI
diff --git a/go/pkg/zerotier/nativetap.go b/go/pkg/zerotier/nativetap.go
index dae784116..560f9c2c7 100644
--- a/go/pkg/zerotier/nativetap.go
+++ b/go/pkg/zerotier/nativetap.go
@@ -11,6 +11,8 @@
  */
 /****/
 
+// This wraps EthernetTap from osdep/
+
 package zerotier
 
 //#cgo CFLAGS: -O3
@@ -159,17 +161,21 @@ func (t *nativeTap) AddMulticastGroupChangeHandler(handler func(bool, *Multicast
 func (t *nativeTap) AddRoute(r *Route) error {
 	rc := 0
 	if r != nil {
+		var via []byte
+		if r.Via != nil {
+			via = *r.Via
+		}
 		if len(r.Target.IP) == 4 {
 			mask, _ := r.Target.Mask.Size()
-			if len(r.Via) == 4 {
-				rc = int(C.ZT_GoTap_addRoute(t.tap, AFInet, unsafe.Pointer(&r.Target.IP[0]), C.int(mask), AFInet, unsafe.Pointer(&r.Via[0]), C.uint(r.Metric)))
+			if len(via) == 4 {
+				rc = int(C.ZT_GoTap_addRoute(t.tap, AFInet, unsafe.Pointer(&r.Target.IP[0]), C.int(mask), AFInet, unsafe.Pointer(&via[0]), C.uint(r.Metric)))
 			} else {
 				rc = int(C.ZT_GoTap_addRoute(t.tap, AFInet, unsafe.Pointer(&r.Target.IP[0]), C.int(mask), 0, nil, C.uint(r.Metric)))
 			}
 		} else if len(r.Target.IP) == 16 {
 			mask, _ := r.Target.Mask.Size()
-			if len(r.Via) == 4 {
-				rc = int(C.ZT_GoTap_addRoute(t.tap, AFInet6, unsafe.Pointer(&r.Target.IP[0]), C.int(mask), AFInet6, unsafe.Pointer(&r.Via[0]), C.uint(r.Metric)))
+			if len(via) == 16 {
+				rc = int(C.ZT_GoTap_addRoute(t.tap, AFInet6, unsafe.Pointer(&r.Target.IP[0]), C.int(mask), AFInet6, unsafe.Pointer(&via[0]), C.uint(r.Metric)))
 			} else {
 				rc = int(C.ZT_GoTap_addRoute(t.tap, AFInet6, unsafe.Pointer(&r.Target.IP[0]), C.int(mask), 0, nil, C.uint(r.Metric)))
 			}
@@ -185,17 +191,21 @@ func (t *nativeTap) AddRoute(r *Route) error {
 func (t *nativeTap) RemoveRoute(r *Route) error {
 	rc := 0
 	if r != nil {
+		var via []byte
+		if r.Via != nil {
+			via = *r.Via
+		}
 		if len(r.Target.IP) == 4 {
 			mask, _ := r.Target.Mask.Size()
-			if len(r.Via) == 4 {
-				rc = int(C.ZT_GoTap_removeRoute(t.tap, AFInet, unsafe.Pointer(&r.Target.IP[0]), C.int(mask), AFInet, unsafe.Pointer(&r.Via[0]), C.uint(r.Metric)))
+			if len(via) == 4 {
+				rc = int(C.ZT_GoTap_removeRoute(t.tap, AFInet, unsafe.Pointer(&r.Target.IP[0]), C.int(mask), AFInet, unsafe.Pointer(&(via[0])), C.uint(r.Metric)))
 			} else {
 				rc = int(C.ZT_GoTap_removeRoute(t.tap, AFInet, unsafe.Pointer(&r.Target.IP[0]), C.int(mask), 0, nil, C.uint(r.Metric)))
 			}
 		} else if len(r.Target.IP) == 16 {
 			mask, _ := r.Target.Mask.Size()
-			if len(r.Via) == 4 {
-				rc = int(C.ZT_GoTap_removeRoute(t.tap, AFInet6, unsafe.Pointer(&r.Target.IP[0]), C.int(mask), AFInet6, unsafe.Pointer(&r.Via[0]), C.uint(r.Metric)))
+			if len(via) == 16 {
+				rc = int(C.ZT_GoTap_removeRoute(t.tap, AFInet6, unsafe.Pointer(&r.Target.IP[0]), C.int(mask), AFInet6, unsafe.Pointer(&via[0]), C.uint(r.Metric)))
 			} else {
 				rc = int(C.ZT_GoTap_removeRoute(t.tap, AFInet6, unsafe.Pointer(&r.Target.IP[0]), C.int(mask), 0, nil, C.uint(r.Metric)))
 			}
diff --git a/go/pkg/zerotier/path.go b/go/pkg/zerotier/path.go
index 3d0e2e138..d4d2672f6 100644
--- a/go/pkg/zerotier/path.go
+++ b/go/pkg/zerotier/path.go
@@ -17,18 +17,18 @@ import "net"
 
 // Path is a path to another peer on the network
 type Path struct {
-	IP                     net.IP
-	Port                   int
-	LastSend               int64
-	LastReceive            int64
-	TrustedPathID          uint64
-	Latency                float32
-	PacketDelayVariance    float32
-	ThroughputDisturbCoeff float32
-	PacketErrorRatio       float32
-	PacketLossRatio        float32
-	Stability              float32
-	Throughput             uint64
-	MaxThroughput          uint64
-	Allocation             float32
+	IP                     net.IP  `json:"ip"`
+	Port                   int     `json:"port"`
+	LastSend               int64   `json:"lastSend"`
+	LastReceive            int64   `json:"lastReceive"`
+	TrustedPathID          uint64  `json:"trustedPathID"`
+	Latency                float32 `json:"latency"`
+	PacketDelayVariance    float32 `json:"packetDelayVariance"`
+	ThroughputDisturbCoeff float32 `json:"throughputDisturbCoeff"`
+	PacketErrorRatio       float32 `json:"packetErrorRatio"`
+	PacketLossRatio        float32 `json:"packetLossRatio"`
+	Stability              float32 `json:"stability"`
+	Throughput             uint64  `json:"throughput"`
+	MaxThroughput          uint64  `json:"maxThroughput"`
+	Allocation             float32 `json:"allocation"`
 }
diff --git a/go/pkg/zerotier/peer.go b/go/pkg/zerotier/peer.go
index 792f76875..053252916 100644
--- a/go/pkg/zerotier/peer.go
+++ b/go/pkg/zerotier/peer.go
@@ -15,10 +15,10 @@ package zerotier
 
 // Peer is another ZeroTier node
 type Peer struct {
-	Address Address
-	Version [3]int
-	Latency int
-	Role    int
-	Paths   []Path
-	Clock   int64
+	Address Address `json:"address"`
+	Version [3]int  `json:"version"`
+	Latency int     `json:"latency"`
+	Role    int     `json:"role"`
+	Paths   []Path  `json:"paths,omitempty"`
+	Clock   int64   `json:"clock"`
 }
diff --git a/go/pkg/zerotier/root.go b/go/pkg/zerotier/root.go
index 98afc79ca..73affb178 100644
--- a/go/pkg/zerotier/root.go
+++ b/go/pkg/zerotier/root.go
@@ -15,6 +15,6 @@ package zerotier
 
 // Root describes a root server used to find and establish communication with other nodes.
 type Root struct {
-	Name    string
-	Locator *Locator
+	Name    string   `json:"name"`
+	Locator *Locator `json:"locator,omitempty"`
 }
diff --git a/go/pkg/zerotier/route.go b/go/pkg/zerotier/route.go
index 44805bc1f..8ff515ff1 100644
--- a/go/pkg/zerotier/route.go
+++ b/go/pkg/zerotier/route.go
@@ -21,16 +21,16 @@ import (
 // Route represents a route in a host's routing table
 type Route struct {
 	// Target for this route
-	Target net.IPNet
+	Target net.IPNet `json:"target"`
 
 	// Via is how to reach this target (null/empty if the target IP range is local to this virtual LAN)
-	Via *net.IP
+	Via *net.IP `json:"via,omitempty"`
 
 	// Route flags (currently unused, always 0)
-	Flags uint16
+	Flags uint16 `json:"flags"`
 
 	// Metric is an interface metric that can affect route priority (behavior can be OS-specific)
-	Metric uint16
+	Metric uint16 `json:"metric"`
 }
 
 // String returns a string representation of this route
@@ -46,7 +46,9 @@ func (r *Route) key() (k [6]uint64) {
 	copy(((*[16]byte)(unsafe.Pointer(&k[0])))[:], r.Target.IP)
 	ones, bits := r.Target.Mask.Size()
 	k[2] = (uint64(ones) << 32) | uint64(bits)
-	copy(((*[16]byte)(unsafe.Pointer(&k[3])))[:], r.Via)
+	if r.Via != nil {
+		copy(((*[16]byte)(unsafe.Pointer(&k[3])))[:], *r.Via)
+	}
 	k[5] = (uint64(r.Flags) << 32) | uint64(r.Metric)
 	return
 }