mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-05 04:59:26 +09:00
api/v2: add device shields-up, SSH, distro and connectivity fields
Same read shapes as SaaS. Latency stays empty: headscale drops it on arrival.
This commit is contained in:
1 parent
9ba1c5e5e2
commit
7063c04581
3 files changed
+152
-2
No files matched your search
@@ -21,8 +21,9 @@ func init() {
|
||||
}
|
||||
|
||||
// Device is the Tailscale device response. Headscale nodes map onto it; fields
|
||||
// Headscale does not track are emitted as their zero value. The route slices
|
||||
// are only populated for the fields=all variant, matching Tailscale.
|
||||
// Headscale does not track are left out, apart from UpdateAvailable. The route
|
||||
// slices and the host detail are only populated for the fields=all variant,
|
||||
// matching Tailscale.
|
||||
type Device struct {
|
||||
Addresses []string `json:"addresses" nullable:"false"`
|
||||
Name string `json:"name"`
|
||||
@@ -45,6 +46,9 @@ type Device struct {
|
||||
OS string `json:"os"`
|
||||
ClientVersion string `json:"clientVersion"`
|
||||
UpdateAvailable bool `json:"updateAvailable"`
|
||||
|
||||
BlocksIncomingConnections bool `json:"blocksIncomingConnections"`
|
||||
|
||||
// OAuthClientID names the auth key the device joined with. For a key a
|
||||
// token made, Tailscale names the token's OAuth client, which headscale
|
||||
// does not record, so it is left out.
|
||||
@@ -53,6 +57,37 @@ type Device struct {
|
||||
// Populated only when fields=all is requested, then as [] when empty.
|
||||
AdvertisedRoutes []string `json:"advertisedRoutes,omitzero"`
|
||||
EnabledRoutes []string `json:"enabledRoutes,omitzero"`
|
||||
|
||||
SSHEnabled *bool `json:"sshEnabled,omitempty"`
|
||||
Distro *Distro `json:"distro,omitempty"`
|
||||
ClientConnectivity *ClientConnectivity `json:"clientConnectivity,omitempty"`
|
||||
}
|
||||
|
||||
// Distro is the Linux distribution a device reports.
|
||||
type Distro struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
CodeName string `json:"codeName"`
|
||||
}
|
||||
|
||||
// ClientConnectivity is how a device reaches the network, from what it reports.
|
||||
// Latency stays empty: headscale drops a client's DERP latency on arrival.
|
||||
type ClientConnectivity struct {
|
||||
Endpoints []string `json:"endpoints" nullable:"false"`
|
||||
MappingVariesByDestIP bool `json:"mappingVariesByDestIP"`
|
||||
Latency map[string]any `json:"latency" nullable:"false"`
|
||||
ClientSupports ClientSupports `json:"clientSupports"`
|
||||
}
|
||||
|
||||
// ClientSupports is what a device's network allows. HairPinning stays null:
|
||||
// clients no longer report it.
|
||||
type ClientSupports struct {
|
||||
HairPinning *bool `json:"hairPinning"`
|
||||
IPv6 bool `json:"ipv6"`
|
||||
PCP bool `json:"pcp"`
|
||||
PMP bool `json:"pmp"`
|
||||
UDP bool `json:"udp"`
|
||||
UPnP bool `json:"upnp"`
|
||||
}
|
||||
|
||||
// DeviceRoutes is the GET /device/{id}/routes response: what the node announces
|
||||
@@ -496,9 +531,39 @@ func deviceFromView(view types.NodeView, allFields bool, baseDomain string) Devi
|
||||
d.LastSeen = &ls
|
||||
}
|
||||
|
||||
hi := view.Hostinfo()
|
||||
d.BlocksIncomingConnections = hi.Valid() && hi.ShieldsUp()
|
||||
|
||||
if allFields {
|
||||
d.AdvertisedRoutes = emptyIfNil(util.PrefixesToString(view.AnnouncedRoutes()))
|
||||
d.EnabledRoutes = emptyIfNil(util.PrefixesToString(view.ApprovedRoutes().AsSlice()))
|
||||
|
||||
sshEnabled := hi.Valid() && hi.TailscaleSSHEnabled()
|
||||
d.SSHEnabled = &sshEnabled
|
||||
d.Distro = &Distro{}
|
||||
d.ClientConnectivity = &ClientConnectivity{
|
||||
Endpoints: make([]string, 0, view.Endpoints().Len()),
|
||||
Latency: map[string]any{},
|
||||
}
|
||||
|
||||
for _, ep := range view.Endpoints().All() {
|
||||
d.ClientConnectivity.Endpoints = append(d.ClientConnectivity.Endpoints, ep.String())
|
||||
}
|
||||
|
||||
if hi.Valid() {
|
||||
d.Distro = &Distro{Name: hi.Distro(), Version: hi.DistroVersion(), CodeName: hi.DistroCodeName()}
|
||||
|
||||
if ni := hi.NetInfo(); ni.Valid() {
|
||||
d.ClientConnectivity.MappingVariesByDestIP = ni.MappingVariesByDestIP().EqualBool(true)
|
||||
d.ClientConnectivity.ClientSupports = ClientSupports{
|
||||
IPv6: ni.WorkingIPv6().EqualBool(true),
|
||||
PCP: ni.PCP().EqualBool(true),
|
||||
PMP: ni.PMP().EqualBool(true),
|
||||
UDP: ni.WorkingUDP().EqualBool(true),
|
||||
UPnP: ni.UPnP().EqualBool(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return d
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package apiv2
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
)
|
||||
|
||||
// TestDeviceHostDetail pins where each host detail comes from, and that only
|
||||
// blocksIncomingConnections leaves the fields=all shape.
|
||||
func TestDeviceHostDetail(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
node := &types.Node{
|
||||
GivenName: "host",
|
||||
Endpoints: []netip.AddrPort{netip.MustParseAddrPort("192.0.2.1:41641")},
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
ShieldsUp: true,
|
||||
Distro: "nixos",
|
||||
DistroVersion: "26.05",
|
||||
DistroCodeName: "yarara",
|
||||
SSH_HostKeys: []string{"ssh-ed25519 AAAA"},
|
||||
NetInfo: &tailcfg.NetInfo{
|
||||
MappingVariesByDestIP: opt.NewBool(true),
|
||||
WorkingUDP: opt.NewBool(true),
|
||||
UPnP: opt.NewBool(true),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
short := deviceFromView(node.View(), false, "")
|
||||
assert.True(t, short.BlocksIncomingConnections)
|
||||
assert.Nil(t, short.SSHEnabled)
|
||||
assert.Nil(t, short.Distro)
|
||||
assert.Nil(t, short.ClientConnectivity)
|
||||
|
||||
all := deviceFromView(node.View(), true, "")
|
||||
assert.True(t, *all.SSHEnabled)
|
||||
assert.Equal(t, Distro{Name: "nixos", Version: "26.05", CodeName: "yarara"}, *all.Distro)
|
||||
assert.Equal(t, ClientConnectivity{
|
||||
Endpoints: []string{"192.0.2.1:41641"},
|
||||
MappingVariesByDestIP: true,
|
||||
Latency: map[string]any{},
|
||||
ClientSupports: ClientSupports{UDP: true, UPnP: true},
|
||||
}, *all.ClientConnectivity)
|
||||
|
||||
// A node that has reported nothing still has the fields=all shape.
|
||||
bare := deviceFromView((&types.Node{GivenName: "bare"}).View(), true, "")
|
||||
assert.False(t, *bare.SSHEnabled)
|
||||
assert.Equal(t, Distro{}, *bare.Distro)
|
||||
assert.Equal(t, []string{}, bare.ClientConnectivity.Endpoints)
|
||||
}
|
||||
Reference in new issue
Block a user