mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-16 16:15:23 -04:00
988b54515e
* feat(tailscale): add connect/disconnect/exit-node/LAN-access backend The Tailscale backend previously exposed only read-only status (tailscale.getStatus, tailscale.refresh). This adds write actions through the existing tailscale.com/client/local integration: - tailscale.connect / tailscale.disconnect (EditPrefs WantRunning) - tailscale.setExitNode (EditPrefs ExitNodeID; empty id clears it and any legacy ExitNodeIP, mirroring `tailscale set --exit-node`) - tailscale.setAllowLanAccess (EditPrefs ExitNodeAllowLANAccess) The manager's client interface gains GetPrefs/EditPrefs; fetchState merges ExitNodeAllowLANAccess from prefs, and Peer exposes ExitNodeOption so the UI can list exit-node-capable peers. * feat(tailscale): expose the new actions in TailscaleService Adds connectTailscale/disconnectTailscale, setExitNode/clearExitNode and setAllowLanAccess wrappers, plus derived exitNodeOptions/currentExitNode and the exitNodeAllowLanAccess state. Write-action errors surface via ToastService. * feat(tailscale): add connection, exit-node and LAN-access controls to the widget The control-center widget toggle was a no-op. It now connects/disconnects, and the detail panel gains a connection status row with a connect/disconnect button, an exit-node picker and a LAN-access toggle.
137 lines
2.9 KiB
Go
137 lines
2.9 KiB
Go
package tailscale
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"tailscale.com/ipn/ipnstate"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
// convertStatus converts an ipnstate.Status into our TailscaleState IPC type.
|
|
func convertStatus(status *ipnstate.Status) *TailscaleState {
|
|
connected := status.BackendState == "Running"
|
|
|
|
state := &TailscaleState{
|
|
Connected: connected,
|
|
BackendState: status.BackendState,
|
|
Version: status.Version,
|
|
}
|
|
|
|
if status.CurrentTailnet != nil {
|
|
state.TailnetName = status.CurrentTailnet.Name
|
|
state.MagicDNSSuffix = status.CurrentTailnet.MagicDNSSuffix
|
|
}
|
|
|
|
if !connected {
|
|
return state
|
|
}
|
|
|
|
users := status.User
|
|
|
|
if status.Self != nil {
|
|
state.Self = convertPeerStatus(status.Self, users)
|
|
}
|
|
|
|
if len(status.Peer) > 0 {
|
|
peers := make([]Peer, 0, len(status.Peer))
|
|
for _, ps := range status.Peer {
|
|
peers = append(peers, convertPeerStatus(ps, users))
|
|
}
|
|
sort.Slice(peers, func(i, j int) bool {
|
|
if peers[i].Online != peers[j].Online {
|
|
return peers[i].Online
|
|
}
|
|
return strings.ToLower(peers[i].Hostname) < strings.ToLower(peers[j].Hostname)
|
|
})
|
|
state.Peers = peers
|
|
}
|
|
|
|
return state
|
|
}
|
|
|
|
// convertPeerStatus converts an ipnstate.PeerStatus into our Peer IPC type.
|
|
func convertPeerStatus(ps *ipnstate.PeerStatus, users map[tailcfg.UserID]tailcfg.UserProfile) Peer {
|
|
dnsName := strings.TrimSuffix(ps.DNSName, ".")
|
|
|
|
// DNSName first label is unique per node; OS HostName is not.
|
|
hostname := ps.HostName
|
|
if dnsName != "" {
|
|
parts := strings.SplitN(dnsName, ".", 2)
|
|
if len(parts) > 0 && parts[0] != "" {
|
|
hostname = parts[0]
|
|
}
|
|
}
|
|
|
|
peer := Peer{
|
|
ID: string(ps.ID),
|
|
Hostname: hostname,
|
|
DNSName: dnsName,
|
|
OS: ps.OS,
|
|
Online: ps.Online,
|
|
Active: ps.Active,
|
|
ExitNode: ps.ExitNode,
|
|
ExitNodeOption: ps.ExitNodeOption,
|
|
Relay: ps.Relay,
|
|
RxBytes: ps.RxBytes,
|
|
TxBytes: ps.TxBytes,
|
|
}
|
|
|
|
for _, ip := range ps.TailscaleIPs {
|
|
if ip.Is4() {
|
|
if peer.TailscaleIP == "" {
|
|
peer.TailscaleIP = ip.String()
|
|
}
|
|
} else {
|
|
if peer.TailscaleIPv6 == "" {
|
|
peer.TailscaleIPv6 = ip.String()
|
|
}
|
|
}
|
|
}
|
|
|
|
if ps.Tags != nil {
|
|
peer.Tags = ps.Tags.AsSlice()
|
|
}
|
|
|
|
if ps.UserID > 0 {
|
|
if user, ok := users[ps.UserID]; ok {
|
|
peer.Owner = user.LoginName
|
|
}
|
|
}
|
|
|
|
if !ps.LastSeen.IsZero() {
|
|
peer.LastSeen = formatRelativeTime(ps.LastSeen)
|
|
}
|
|
|
|
return peer
|
|
}
|
|
|
|
// formatRelativeTime formats a time as a human-readable relative duration (e.g., "5 minutes ago").
|
|
func formatRelativeTime(t time.Time) string {
|
|
d := time.Since(t)
|
|
switch {
|
|
case d < time.Minute:
|
|
return "just now"
|
|
case d < time.Hour:
|
|
m := int(d.Minutes())
|
|
if m == 1 {
|
|
return "1 minute ago"
|
|
}
|
|
return fmt.Sprintf("%d minutes ago", m)
|
|
case d < 24*time.Hour:
|
|
h := int(d.Hours())
|
|
if h == 1 {
|
|
return "1 hour ago"
|
|
}
|
|
return fmt.Sprintf("%d hours ago", h)
|
|
default:
|
|
days := int(d.Hours() / 24)
|
|
if days == 1 {
|
|
return "1 day ago"
|
|
}
|
|
return fmt.Sprintf("%d days ago", days)
|
|
}
|
|
}
|