diff --git a/go/cmd/zerotier/cli/common.go b/go/cmd/zerotier/cli/common.go new file mode 100644 index 000000000..6b2c99799 --- /dev/null +++ b/go/cmd/zerotier/cli/common.go @@ -0,0 +1,45 @@ +/* + * Copyright (c)2019 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: 2023-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" + "net/http" + "os" + "zerotier/pkg/zerotier" +) + +func apiGet(basePath, authToken, urlPath string, result interface{}) { + statusCode, err := zerotier.APIGet(basePath, zerotier.APISocketName, authToken, urlPath, result) + if err != nil { + fmt.Printf("FATAL: API response code %d: %s\n", statusCode, err.Error()) + os.Exit(1) + return + } + if statusCode != http.StatusOK { + if statusCode == http.StatusUnauthorized { + fmt.Printf("FATAL: API response code %d: unauthorized (authorization token incorrect)\n", statusCode) + } + fmt.Printf("FATAL: API response code %d\n", statusCode) + os.Exit(1) + return + } +} + +func enabledDisabled(f bool) string { + if f { + return "ENABLED" + } + return "DISABLED" +} diff --git a/go/cmd/zerotier/cli/peers.go b/go/cmd/zerotier/cli/peers.go index 6770492e4..87a3c8a73 100644 --- a/go/cmd/zerotier/cli/peers.go +++ b/go/cmd/zerotier/cli/peers.go @@ -13,6 +13,51 @@ package cli +import ( + "encoding/json" + "fmt" + "os" + "zerotier/pkg/zerotier" +) + // Peers CLI command -func Peers(basePath, authToken string, args []string) { +func Peers(basePath, authToken string, args []string, jsonOutput bool) { + var peers []zerotier.Peer + apiGet(basePath, authToken, "/peer", &peers) + + if jsonOutput { + j, _ := json.MarshalIndent(&peers, "", " ") + fmt.Println(string(j)) + } else { + fmt.Printf(" \n") + for _, peer := range peers { + role := "LEAF" + link := "RELAY" + lastTX, lastRX := int64(0), int64(0) + address := "" + if len(peer.Paths) > 0 { + link = "DIRECT" + lastTX, lastRX = peer.Clock-peer.Paths[0].LastSend, peer.Clock-peer.Paths[0].LastReceive + if lastTX < 0 { + lastTX = 0 + } + if lastRX < 0 { + lastRX = 0 + } + address = fmt.Sprintf("%s/%d", peer.Paths[0].IP.String(), peer.Paths[0].Port) + } + fmt.Printf("%.10x %-7s %-6s %-5d %-6s %-8d %-8d %s\n", + uint64(peer.Address), + fmt.Sprintf("%d.%d.%d", peer.Version[0], peer.Version[1], peer.Version[2]), + role, + peer.Latency, + link, + lastTX, + lastRX, + address, + ) + } + } + + os.Exit(0) } diff --git a/go/cmd/zerotier/cli/status.go b/go/cmd/zerotier/cli/status.go index faf55028a..daba026c8 100644 --- a/go/cmd/zerotier/cli/status.go +++ b/go/cmd/zerotier/cli/status.go @@ -16,7 +16,6 @@ package cli import ( "encoding/json" "fmt" - "net/http" "os" "zerotier/pkg/zerotier" ) @@ -24,25 +23,50 @@ import ( // Status shows service status info func Status(basePath, authToken string, args []string, jsonOutput bool) { var status zerotier.APIStatus - statusCode, err := zerotier.APIGet(basePath, zerotier.APISocketName, authToken, "/status", &status) - if err != nil { - fmt.Printf("FATAL: API response code %d: %s\n", statusCode, err.Error()) - os.Exit(1) - return - } - if statusCode != http.StatusOK { - if statusCode == http.StatusUnauthorized { - fmt.Printf("FATAL: API response code %d: unauthorized (authorization token incorrect)\n", statusCode) - } - fmt.Printf("FATAL: API response code %d\n", statusCode) - os.Exit(1) - return - } + apiGet(basePath, authToken, "/status", &status) if jsonOutput { j, _ := json.MarshalIndent(&status, "", " ") fmt.Println(string(j)) } else { + online := "ONLINE" + if !status.Online { + online = "OFFLINE" + } + fmt.Printf("%.10x: %s %s\n", uint64(status.Address), online, status.Version) + fmt.Printf("\tports: %d %d %d\n", status.Config.Settings.PrimaryPort, status.Config.Settings.SecondaryPort, status.Config.Settings.TertiaryPort) + fmt.Printf("\tport search: %s\n", enabledDisabled(status.Config.Settings.PortSearch)) + fmt.Printf("\tport mapping (uPnP/NAT-PMP): %s\n", enabledDisabled(status.Config.Settings.PortMapping)) + fmt.Printf("\tmultipath mode: %d\n", status.Config.Settings.MuiltipathMode) + fmt.Printf("\tblacklisted interface prefixes: ") + for i, bl := range status.Config.Settings.InterfacePrefixBlacklist { + if i > 0 { + fmt.Print(',') + } + fmt.Print(bl) + } + fmt.Printf("\n\texplicit external addresses: ") + for i, ea := range status.Config.Settings.ExplicitAddresses { + if i > 0 { + fmt.Print(',') + } + fmt.Print(ea.String()) + } + fmt.Printf("\n\tsystem interface addresses: ") + for i, a := range status.InterfaceAddresses { + if i > 0 { + fmt.Print(',') + } + fmt.Print(a.String()) + } + fmt.Printf("\n\tmapped external addresses: ") + for i, a := range status.MappedExternalAddresses { + if i > 0 { + fmt.Print(',') + } + fmt.Print(a.String()) + } + fmt.Printf("\n\tidentity: %s\n", status.Identity.String()) } os.Exit(0) diff --git a/go/cmd/zerotier/zerotier.go b/go/cmd/zerotier/zerotier.go index d40eebe6f..d8dfec6d7 100644 --- a/go/cmd/zerotier/zerotier.go +++ b/go/cmd/zerotier/zerotier.go @@ -111,7 +111,7 @@ func main() { cli.Status(basePath, authToken, cmdArgs, *jflag) case "peers", "listpeers": authTokenRequired(authToken) - cli.Peers(basePath, authToken, cmdArgs) + cli.Peers(basePath, authToken, cmdArgs, *jflag) case "roots": authTokenRequired(authToken) cli.Roots(basePath, authToken, cmdArgs) diff --git a/go/pkg/zerotier/api.go b/go/pkg/zerotier/api.go index be1d6b786..81b29a8d4 100644 --- a/go/pkg/zerotier/api.go +++ b/go/pkg/zerotier/api.go @@ -96,6 +96,7 @@ type APIStatus struct { // APINetwork is the object returned by API network inquiries type APINetwork struct { + ID NetworkID Config *NetworkConfig Settings *NetworkLocalSettings MulticastSubscriptions []*MulticastGroup @@ -104,6 +105,20 @@ type APINetwork struct { TapDeviceEnabled bool } +func apiNetworkFromNetwork(n *Network) *APINetwork { + var nn APINetwork + nn.ID = n.ID() + c := n.Config() + nn.Config = &c + 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() + return &nn +} + func apiSetStandardHeaders(out http.ResponseWriter) { now := time.Now().UTC() h := out.Header() @@ -215,6 +230,7 @@ func createAPIServer(basePath string, node *Node) (*http.Server, error) { if req.Method == http.MethodPost || req.Method == http.MethodPut { var c LocalConfig if apiReadObj(out, req, &c) == nil { + node.SetLocalConfig(&c) apiSendObj(out, req, http.StatusOK, node.LocalConfig()) } } else if req.Method == http.MethodGet || req.Method == http.MethodHead { @@ -279,12 +295,40 @@ func createAPIServer(basePath string, node *Node) (*http.Server, error) { if req.Method == http.MethodPost || req.Method == http.MethodPut { if queriedID == 0 { apiSendObj(out, req, http.StatusBadRequest, nil) + } else { + var nw APINetwork + if apiReadObj(out, req, &nw) == nil { + n := node.GetNetwork(nw.ID) + if n == nil { + n, err := node.Join(nw.ID, nw.Settings, nil) + if err != nil { + apiSendObj(out, req, http.StatusBadRequest, nil) + } else { + apiSendObj(out, req, http.StatusOK, apiNetworkFromNetwork(n)) + } + } else { + if nw.Settings != nil { + n.SetLocalSettings(nw.Settings) + } + apiSendObj(out, req, http.StatusOK, apiNetworkFromNetwork(n)) + } + } } } else if req.Method == http.MethodGet || req.Method == http.MethodHead { + networks := node.Networks() if queriedID == 0 { // no queried ID lists all networks - networks := node.Networks() - apiSendObj(out, req, http.StatusOK, networks) + nws := make([]*APINetwork, 0, len(networks)) + for _, nw := range networks { + nws = append(nws, apiNetworkFromNetwork(nw)) + } + apiSendObj(out, req, http.StatusOK, nws) } else { + for _, nw := range networks { + if nw.ID() == queriedID { + apiSendObj(out, req, http.StatusOK, apiNetworkFromNetwork(nw)) + break + } + } } } else { out.Header().Set("Allow", "GET, HEAD, PUT, POST") diff --git a/go/pkg/zerotier/network.go b/go/pkg/zerotier/network.go index c6c60e735..d858c7f30 100644 --- a/go/pkg/zerotier/network.go +++ b/go/pkg/zerotier/network.go @@ -194,6 +194,13 @@ func (n *Network) Config() NetworkConfig { // SetLocalSettings modifies this network's local settings func (n *Network) SetLocalSettings(ls *NetworkLocalSettings) { n.updateConfig(nil, ls) } +// LocalSettings gets this network's current local settings +func (n *Network) LocalSettings() NetworkLocalSettings { + n.configLock.RLock() + defer n.configLock.RUnlock() + return n.settings +} + // MulticastSubscribe subscribes to a multicast group func (n *Network) MulticastSubscribe(mg *MulticastGroup) { n.node.log.Printf("%.16x joined multicast group %s", mg.String()) diff --git a/go/pkg/zerotier/node.go b/go/pkg/zerotier/node.go index 802f503a2..bb060348d 100644 --- a/go/pkg/zerotier/node.go +++ b/go/pkg/zerotier/node.go @@ -464,10 +464,13 @@ func (n *Node) SetLocalConfig(lc *LocalConfig) (restartRequired bool, err error) // Join joins a network // If tap is nil, the default system tap for this OS/platform is used (if available). -func (n *Node) Join(nwid uint64, tap Tap) (*Network, error) { +func (n *Node) Join(nwid NetworkID, settings *NetworkLocalSettings, tap Tap) (*Network, error) { n.networksLock.RLock() - if nw, have := n.networks[NetworkID(nwid)]; have { + if nw, have := n.networks[nwid]; have { n.log.Printf("join network %.16x ignored: already a member", nwid) + if settings != nil { + nw.SetLocalSettings(settings) + } return nw, nil } n.networksLock.RUnlock() @@ -488,8 +491,11 @@ func (n *Node) Join(nwid uint64, tap Tap) (*Network, error) { return nil, err } n.networksLock.Lock() - n.networks[NetworkID(nwid)] = nw + n.networks[nwid] = nw n.networksLock.Unlock() + if settings != nil { + nw.SetLocalSettings(settings) + } return nw, nil } @@ -504,6 +510,14 @@ func (n *Node) Leave(nwid uint64) error { return nil } +// GetNetwork looks up a network by ID or returns nil if not joined +func (n *Node) GetNetwork(nwid NetworkID) *Network { + n.networksLock.RLock() + nw := n.networks[nwid] + n.networksLock.RUnlock() + return nw +} + // Networks returns a list of networks that this node has joined func (n *Node) Networks() []*Network { var nws []*Network @@ -613,30 +627,35 @@ func (n *Node) Peers() []*Peer { p2.Paths = make([]Path, 0, int(p.pathCount)) for j := uintptr(0); j < uintptr(p.pathCount); j++ { pt := &p.paths[j] - a := sockaddrStorageToUDPAddr(&pt.address) - if a != nil { - p2.Paths = append(p2.Paths, Path{ - IP: a.IP, - Port: a.Port, - LastSend: int64(pt.lastSend), - LastReceive: int64(pt.lastReceive), - TrustedPathID: uint64(pt.trustedPathId), - Latency: float32(pt.latency), - PacketDelayVariance: float32(pt.packetDelayVariance), - ThroughputDisturbCoeff: float32(pt.throughputDisturbCoeff), - PacketErrorRatio: float32(pt.packetErrorRatio), - PacketLossRatio: float32(pt.packetLossRatio), - Stability: float32(pt.stability), - Throughput: uint64(pt.throughput), - MaxThroughput: uint64(pt.maxThroughput), - Allocation: float32(pt.allocation), - }) + if pt.alive != 0 { + a := sockaddrStorageToUDPAddr(&pt.address) + if a != nil { + p2.Paths = append(p2.Paths, Path{ + IP: a.IP, + Port: a.Port, + LastSend: int64(pt.lastSend), + LastReceive: int64(pt.lastReceive), + TrustedPathID: uint64(pt.trustedPathId), + Latency: float32(pt.latency), + PacketDelayVariance: float32(pt.packetDelayVariance), + ThroughputDisturbCoeff: float32(pt.throughputDisturbCoeff), + PacketErrorRatio: float32(pt.packetErrorRatio), + PacketLossRatio: float32(pt.packetLossRatio), + Stability: float32(pt.stability), + Throughput: uint64(pt.throughput), + MaxThroughput: uint64(pt.maxThroughput), + Allocation: float32(pt.allocation), + }) + } } } + sort.Slice(p2.Paths, func(a, b int) bool { return p2.Paths[a].LastReceive < p2.Paths[b].LastReceive }) + p2.Clock = TimeMs() peers = append(peers, p2) } C.ZT_Node_freeQueryResult(unsafe.Pointer(n.zn), unsafe.Pointer(pl)) } + sort.Slice(peers, func(a, b int) bool { return peers[a].Address < peers[b].Address }) return peers } diff --git a/go/pkg/zerotier/peer.go b/go/pkg/zerotier/peer.go index 9687942a0..792f76875 100644 --- a/go/pkg/zerotier/peer.go +++ b/go/pkg/zerotier/peer.go @@ -20,4 +20,5 @@ type Peer struct { Latency int Role int Paths []Path + Clock int64 } diff --git a/include/ZeroTierCore.h b/include/ZeroTierCore.h index 1f0a4f45f..59bb8d9f7 100644 --- a/include/ZeroTierCore.h +++ b/include/ZeroTierCore.h @@ -1234,9 +1234,9 @@ typedef struct char *ifname; /** - * Is path expired? + * Is path alive? */ - int expired; + int alive; /** * Is path preferred? diff --git a/node/Node.cpp b/node/Node.cpp index 32b664442..69db64569 100644 --- a/node/Node.cpp +++ b/node/Node.cpp @@ -537,6 +537,7 @@ ZT_PeerList *Node::peers() const p->latency = -1; p->role = RR->topology->isRoot((*pi)->identity()) ? ZT_PEER_ROLE_PLANET : ZT_PEER_ROLE_LEAF; + const int64_t now = _now; std::vector< SharedPtr > paths((*pi)->paths(_now)); SharedPtr bestp((*pi)->getAppropriatePath(_now,false)); p->hadAggregateLink |= (*pi)->hasAggregateLink(); @@ -546,7 +547,7 @@ ZT_PeerList *Node::peers() const p->paths[p->pathCount].lastSend = (*path)->lastOut(); p->paths[p->pathCount].lastReceive = (*path)->lastIn(); p->paths[p->pathCount].trustedPathId = RR->topology->getOutboundPathTrust((*path)->address()); - p->paths[p->pathCount].expired = 0; + p->paths[p->pathCount].alive = (*path)->alive(now) ? 1 : 0; p->paths[p->pathCount].preferred = ((*path) == bestp) ? 1 : 0; p->paths[p->pathCount].latency = (float)(*path)->latency(); p->paths[p->pathCount].packetDelayVariance = (*path)->packetDelayVariance();