Skip to content

Commit

Permalink
hardened ipn.State enum (#16)
Browse files Browse the repository at this point in the history
* hardened ipn.State enum

* nit

* Comment NewIPNStateFromString

* nit v2

---------

Co-authored-by: Lexi Mattick <[email protected]>
  • Loading branch information
nikolaydubina and kognise authored Aug 26, 2024
1 parent 7b099e6 commit 75163fc
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 25 deletions.
40 changes: 35 additions & 5 deletions libts/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package libts

import (
"context"
"fmt"
"slices"
"strings"

Expand All @@ -17,9 +18,8 @@ type State struct {
Prefs *ipn.Prefs

// Current Tailscale backend state.
// "NoState", "NeedsLogin", "NeedsMachineAuth", "Stopped",
// "Starting", "Running".
BackendState string
BackendState ipn.State

// Current Tailscale version. This is a shortened version string like "1.70.0".
TSVersion string

Expand Down Expand Up @@ -70,6 +70,31 @@ func getSortedExitNodes(tsStatus *ipnstate.Status) []*ipnstate.PeerStatus {
return exitNodes
}

// Create an ipn.State from the string representation.
//
// This string representation comes from Tailscale's API and, because Go does not have
// proper enums, this is the best way to convert it back to a "typed" representation.
func NewIPNStateFromString(v string) (ipn.State, error) {
switch v {
case "NoState":
return ipn.NoState, nil
case "InUseOtherUser":
return ipn.InUseOtherUser, nil
case "NeedsLogin":
return ipn.NeedsLogin, nil
case "NeedsMachineAuth":
return ipn.NeedsMachineAuth, nil
case "Stopped":
return ipn.Stopped, nil
case "Starting":
return ipn.Starting, nil
case "Running":
return ipn.Running, nil
default:
return ipn.NoState, fmt.Errorf("unknown ipn state: %s", v)
}
}

// Make a current State by making necessary Tailscale API calls.
func GetState(ctx context.Context) (State, error) {
status, err := Status(ctx)
Expand All @@ -87,10 +112,15 @@ func GetState(ctx context.Context) (State, error) {
return State{}, err
}

backendState, err := NewIPNStateFromString(status.BackendState)
if err != nil {
return State{}, fmt.Errorf("cannot get status from state: %w", err)
}

state := State{
Prefs: prefs,
AuthURL: status.AuthURL,
BackendState: status.BackendState,
BackendState: backendState,
TSVersion: status.Version,
Self: status.Self,
SortedExitNodes: getSortedExitNodes(status),
Expand All @@ -114,7 +144,7 @@ func GetState(ctx context.Context) (State, error) {
if lock.Enabled && lock.NodeKey != nil && !lock.PublicKey.IsZero() {
state.LockKey = &lock.PublicKey

if !lock.NodeKeySigned && state.BackendState == ipn.Running.String() {
if !lock.NodeKeySigned && state.BackendState == ipn.Running {
state.IsLockedOut = true
}
}
Expand Down
2 changes: 1 addition & 1 deletion menus.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (

// Update all of the menu UIs from the current state.
func (m *model) updateMenus() {
if m.state.BackendState == ipn.Running.String() {
if m.state.BackendState == ipn.Running {
// Update the device info submenu.
{
submenuItems := []ui.SubmenuItem{
Expand Down
8 changes: 4 additions & 4 deletions update.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case ".":
switch m.state.BackendState {
// If running, stop Tailscale.
case ipn.Running.String():
case ipn.Running:
return m, func() tea.Msg {
err := libts.Down(ctx)
if err != nil {
Expand All @@ -147,7 +147,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}

// If stopped, start Tailscale.
case ipn.Stopped.String():
case ipn.Stopped:
return m, func() tea.Msg {
err := libts.Up(ctx)
if err != nil {
Expand All @@ -157,7 +157,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}

// If we need to login...
case ipn.NeedsLogin.String():
case ipn.NeedsLogin:
if m.state.AuthURL == "" {
// If we haven't started the login flow yet, do so.
// Tailscale will open their browser for us.
Expand All @@ -179,7 +179,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}

case ipn.Starting.String():
case ipn.Starting:
// If we have an AuthURL in the Starting state, that means the user is reauthenticating
// and we also need to open the browser!
// (But not if we're root on Linux.)
Expand Down
30 changes: 15 additions & 15 deletions view.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,30 @@ import (
)

// Format the status button in the header bar.
func renderStatusButton(backendState string, isUsingExitNode bool) string {
func renderStatusButton(backendState ipn.State, isUsingExitNode bool) string {
buttonStyle := lipgloss.NewStyle().
Padding(0, 1)

switch backendState {
case ipn.NeedsLogin.String():
case ipn.NeedsLogin:
return buttonStyle.
Background(ui.Yellow).
Foreground(ui.Black).
Render("Needs Login")

case ipn.NeedsMachineAuth.String():
case ipn.NeedsMachineAuth:
return buttonStyle.
Background(ui.Yellow).
Foreground(ui.Black).
Render("Needs Machine Auth")

case ipn.Starting.String():
case ipn.Starting:
return buttonStyle.
Background(ui.Blue).
Foreground(ui.White).
Render("Starting...")

case ipn.Running.String():
case ipn.Running:
text := "Connected"
if isUsingExitNode {
text += " - Exit Node"
Expand All @@ -45,13 +45,13 @@ func renderStatusButton(backendState string, isUsingExitNode bool) string {
Foreground(ui.Black).
Render(text)

case ipn.Stopped.String():
case ipn.Stopped:
return buttonStyle.
Background(ui.Red).
Foreground(ui.Black).
Render("Not Connected")

case ipn.NoState.String():
case ipn.NoState:
return buttonStyle.
Background(ui.Blue).
Foreground(ui.White).
Expand Down Expand Up @@ -93,7 +93,7 @@ func renderHeader(m *model) string {
var status strings.Builder
status.WriteString("Status: ")
status.WriteString(renderStatusButton(m.state.BackendState, m.state.CurrentExitNode != nil))
if m.state.BackendState == ipn.Running.String() {
if m.state.BackendState == ipn.Running {
status.WriteString(lipgloss.NewStyle().
Faint(true).
PaddingLeft(1).
Expand Down Expand Up @@ -154,7 +154,7 @@ func renderMiddleBanner(m *model, height int, text string) string {
func renderStatusBar(m *model) string {
var text string

if m.statusText == "" && m.canWrite && m.state.BackendState == ipn.Running.String() {
if m.statusText == "" && m.canWrite && m.state.BackendState == ipn.Running {
// If there's no other status, we're running, and we have write access, show up/down.
text = lipgloss.NewStyle().
Faint(true).
Expand Down Expand Up @@ -242,16 +242,16 @@ func (m model) View() string {
Render(m.state.AuthURL)

switch m.state.BackendState {
case ipn.Running.String():
case ipn.Running:
middle = lipgloss.NewStyle().
Height(middleHeight).
Render(m.menu.Render(middleHeight))

case ipn.NeedsMachineAuth.String():
case ipn.NeedsMachineAuth:
// TODO: Figure out what this state actually is so we can be helpful to the user.
middle = renderMiddleBanner(&m, middleHeight, "Tailscale status is NeedsMachineAuth.")

case ipn.NeedsLogin.String():
case ipn.NeedsLogin:
lines := []string{
lipgloss.NewStyle().
Bold(true).
Expand Down Expand Up @@ -280,17 +280,17 @@ func (m model) View() string {

middle = renderMiddleBanner(&m, middleHeight, strings.Join(lines, "\n"))

case ipn.Stopped.String():
case ipn.Stopped:
middle = renderMiddleBanner(&m, middleHeight, strings.Join([]string{
`The Tailscale daemon isn't running.`,
``,
`Press . to bring Tailscale up.`,
}, "\n"))

case ipn.NoState.String():
case ipn.NoState:
middle = renderMiddleBanner(&m, middleHeight, ui.PoggersAnimationFrame(m.animationT))

case ipn.Starting.String():
case ipn.Starting:
if m.state.AuthURL == "" {
middle = renderMiddleBanner(&m, middleHeight, ui.PoggersAnimationFrame(m.animationT))
} else {
Expand Down

0 comments on commit 75163fc

Please sign in to comment.