cmd/tailscale: implement Tailscale 1.6 default route setting

Fixes tailscale/tailscale#1401

Signed-off-by: Elias Naur <mail@eliasnaur.com>
pull/7/head
Elias Naur 5 years ago
parent 71e0f2bd94
commit 3b1a5e7a71

@ -18,6 +18,7 @@ import (
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"inet.af/netaddr"
"github.com/tailscale/tailscale-android/jni" "github.com/tailscale/tailscale-android/jni"
"tailscale.com/ipn" "tailscale.com/ipn"
@ -61,15 +62,43 @@ type clientState struct {
Peers []UIPeer Peers []UIPeer
} }
type ExitStatus uint8
const (
// No exit node selected.
ExitNone ExitStatus = iota
// Exit node selected and exists, but is offline or missing.
ExitOffline
// Exit node selected and online.
ExitOnline
)
type ExitNode struct {
Label string
Online bool
ID tailcfg.StableNodeID
}
type BackendState struct { type BackendState struct {
Prefs *ipn.Prefs
State ipn.State State ipn.State
NetworkMap *netmap.NetworkMap NetworkMap *netmap.NetworkMap
LostInternet bool LostInternet bool
// Exits are the peers that can act as exit node.
Exits []ExitNode
// ExitState describes the state of our exit node.
ExitStatus ExitStatus
// Exit is our current exit node, if any.
Exit ExitNode
} }
// UIEvent is an event flowing from the UI to the backend. // UIEvent is an event flowing from the UI to the backend.
type UIEvent interface{} type UIEvent interface{}
type RouteAllEvent struct {
ID tailcfg.StableNodeID
}
type ConnectEvent struct { type ConnectEvent struct {
Enable bool Enable bool
} }
@ -178,7 +207,6 @@ func (a *App) runBackend() error {
alarmChan = timer.C alarmChan = timer.C
} }
} }
var prefs *ipn.Prefs
notifications := make(chan ipn.Notify, 1) notifications := make(chan ipn.Notify, 1)
startErr := make(chan error) startErr := make(chan error)
// Start from a goroutine to avoid deadlock when Start // Start from a goroutine to avoid deadlock when Start
@ -208,16 +236,18 @@ func (a *App) runBackend() error {
} }
configErrs <- b.updateTUN(service, cfg) configErrs <- b.updateTUN(service, cfg)
case n := <-notifications: case n := <-notifications:
exitWasOnline := state.ExitStatus == ExitOnline
if p := n.Prefs; p != nil { if p := n.Prefs; p != nil {
first := prefs == nil first := state.Prefs == nil
prefs = p.Clone() state.Prefs = p.Clone()
state.updateExitNodes()
if first { if first {
prefs.Hostname = a.hostname() state.Prefs.Hostname = a.hostname()
prefs.OSVersion = a.osVersion() state.Prefs.OSVersion = a.osVersion()
prefs.DeviceModel = a.modelName() state.Prefs.DeviceModel = a.modelName()
go b.backend.SetPrefs(prefs) go b.backend.SetPrefs(state.Prefs)
} }
a.setPrefs(prefs) a.setPrefs(state.Prefs)
} }
if s := n.State; s != nil { if s := n.State; s != nil {
oldState := state.State oldState := state.State
@ -249,11 +279,16 @@ func (a *App) runBackend() error {
} }
if m := n.NetMap; m != nil { if m := n.NetMap; m != nil {
state.NetworkMap = m state.NetworkMap = m
state.updateExitNodes()
a.notify(state) a.notify(state)
if service != 0 { if service != 0 {
alarm(a.notifyExpiry(service, m.Expiry)) alarm(a.notifyExpiry(service, m.Expiry))
} }
} }
// Notify if a previously online exit is not longer online (or missing).
if service != 0 && exitWasOnline && state.ExitStatus == ExitOffline {
a.pushNotify(service, "Connection Lost", "Your exit node is offline. Disable your exit node or contact your network admin for help.")
}
case <-alarmChan: case <-alarmChan:
if m := state.NetworkMap; m != nil && service != 0 { if m := state.NetworkMap; m != nil && service != 0 {
alarm(a.notifyExpiry(service, m.Expiry)) alarm(a.notifyExpiry(service, m.Expiry))
@ -263,8 +298,8 @@ func (a *App) runBackend() error {
case OAuth2Event: case OAuth2Event:
go b.backend.Login(e.Token) go b.backend.Login(e.Token)
case ToggleEvent: case ToggleEvent:
prefs.WantRunning = !prefs.WantRunning state.Prefs.WantRunning = !state.Prefs.WantRunning
go b.backend.SetPrefs(prefs) go b.backend.SetPrefs(state.Prefs)
case WebAuthEvent: case WebAuthEvent:
if !signingIn { if !signingIn {
go b.backend.StartLoginInteractive() go b.backend.StartLoginInteractive()
@ -273,8 +308,11 @@ func (a *App) runBackend() error {
case LogoutEvent: case LogoutEvent:
go b.backend.Logout() go b.backend.Logout()
case ConnectEvent: case ConnectEvent:
prefs.WantRunning = e.Enable state.Prefs.WantRunning = e.Enable
go b.backend.SetPrefs(prefs) go b.backend.SetPrefs(state.Prefs)
case RouteAllEvent:
state.Prefs.ExitNodeID = e.ID
go b.backend.SetPrefs(state.Prefs)
} }
case s := <-onConnect: case s := <-onConnect:
jni.Do(a.jvm, func(env *jni.Env) error { jni.Do(a.jvm, func(env *jni.Env) error {
@ -337,6 +375,56 @@ func (a *App) isChromeOS() bool {
return chromeOS return chromeOS
} }
func (s *BackendState) updateExitNodes() {
s.ExitStatus = ExitNone
var exitID tailcfg.StableNodeID
if p := s.Prefs; p != nil {
exitID = p.ExitNodeID
if exitID != "" {
s.ExitStatus = ExitOffline
}
}
hasMyExit := exitID == ""
s.Exits = nil
var peers []*tailcfg.Node
if s.NetworkMap != nil {
peers = s.NetworkMap.Peers
}
for _, p := range peers {
canRoute := false
for _, r := range p.AllowedIPs {
if r == netaddr.MustParseIPPrefix("0.0.0.0/0") || r == netaddr.MustParseIPPrefix("::/0") {
canRoute = true
break
}
}
myExit := p.StableID == exitID
hasMyExit = hasMyExit || myExit
exit := ExitNode{
Label: p.DisplayName(true),
Online: canRoute,
ID: p.StableID,
}
if myExit {
s.Exit = exit
if canRoute {
s.ExitStatus = ExitOnline
}
}
if canRoute || myExit {
s.Exits = append(s.Exits, exit)
}
}
sort.Slice(s.Exits, func(i, j int) bool {
return s.Exits[i].Label < s.Exits[j].Label
})
if !hasMyExit {
// Insert node missing from netmap.
s.Exit = ExitNode{Label: "Unknown device", ID: exitID}
s.Exits = append([]ExitNode{s.Exit}, s.Exits...)
}
}
// hostname builds a hostname from android.os.Build fields, in place of a // hostname builds a hostname from android.os.Build fields, in place of a
// useless os.Hostname(). // useless os.Hostname().
func (a *App) hostname() string { func (a *App) hostname() string {
@ -442,17 +530,20 @@ func (a *App) notifyExpiry(service jni.Object, expiry time.Time) *time.Timer {
default: default:
return time.NewTimer(d - aday) return time.NewTimer(d - aday)
} }
err := jni.Do(a.jvm, func(env *jni.Env) error { if err := a.pushNotify(service, title, msg); err != nil {
fatalErr(err)
}
return t
}
func (a *App) pushNotify(service jni.Object, title, msg string) error {
return jni.Do(a.jvm, func(env *jni.Env) error {
cls := jni.GetObjectClass(env, service) cls := jni.GetObjectClass(env, service)
notify := jni.GetMethodID(env, cls, "notify", "(Ljava/lang/String;Ljava/lang/String;)V") notify := jni.GetMethodID(env, cls, "notify", "(Ljava/lang/String;Ljava/lang/String;)V")
jtitle := jni.JavaString(env, title) jtitle := jni.JavaString(env, title)
jmessage := jni.JavaString(env, msg) jmessage := jni.JavaString(env, msg)
return jni.CallVoidMethod(env, service, notify, jni.Value(jtitle), jni.Value(jmessage)) return jni.CallVoidMethod(env, service, notify, jni.Value(jtitle), jni.Value(jmessage))
}) })
if err != nil {
fatalErr(err)
}
return t
} }
func (a *App) notify(state BackendState) { func (a *App) notify(state BackendState) {
@ -638,15 +729,19 @@ func (a *App) updateState(act jni.Object, state *clientState) {
state.browseURL = "" state.browseURL = ""
} }
state.Peers = nil netmap := state.backend.NetworkMap
netMap := state.backend.NetworkMap var (
if netMap == nil { peers []*tailcfg.Node
return myID tailcfg.UserID
)
if netmap != nil {
peers = netmap.Peers
myID = netmap.User
} }
// Split into sections. // Split into sections.
users := make(map[tailcfg.UserID]struct{}) users := make(map[tailcfg.UserID]struct{})
var peers []UIPeer var uiPeers []UIPeer
for _, p := range netMap.Peers { for _, p := range peers {
if q := state.query; q != "" { if q := state.query; q != "" {
// Filter peers according to search query. // Filter peers according to search query.
host := strings.ToLower(p.Hostinfo.Hostname) host := strings.ToLower(p.Hostinfo.Hostname)
@ -660,20 +755,19 @@ func (a *App) updateState(act jni.Object, state *clientState) {
} }
} }
users[p.User] = struct{}{} users[p.User] = struct{}{}
peers = append(peers, UIPeer{ uiPeers = append(uiPeers, UIPeer{
Owner: p.User, Owner: p.User,
Peer: p, Peer: p,
}) })
} }
// Add section (user) headers. // Add section (user) headers.
for u := range users { for u := range users {
name := netMap.UserProfiles[u].DisplayName name := netmap.UserProfiles[u].DisplayName
name = strings.ToUpper(name) name = strings.ToUpper(name)
peers = append(peers, UIPeer{Owner: u, Name: name}) uiPeers = append(uiPeers, UIPeer{Owner: u, Name: name})
} }
myID := state.backend.NetworkMap.User sort.Slice(uiPeers, func(i, j int) bool {
sort.Slice(peers, func(i, j int) bool { lhs, rhs := uiPeers[i], uiPeers[j]
lhs, rhs := peers[i], peers[j]
if lu, ru := lhs.Owner, rhs.Owner; ru != lu { if lu, ru := lhs.Owner, rhs.Owner; ru != lu {
// Sort own peers first. // Sort own peers first.
if lu == myID { if lu == myID {
@ -696,7 +790,7 @@ func (a *App) updateState(act jni.Object, state *clientState) {
rName := rp.DisplayName(rp.User == myID) rName := rp.DisplayName(rp.User == myID)
return lName < rName || lName == rName && lp.ID < rp.ID return lName < rName || lName == rName && lp.ID < rp.ID
}) })
state.Peers = peers state.Peers = uiPeers
} }
func (a *App) prepareVPN(act jni.Object) error { func (a *App) prepareVPN(act jni.Object) error {
@ -729,6 +823,8 @@ func (a *App) processUIEvents(w *app.Window, events []UIEvent, act jni.Object, s
requestBackend(e) requestBackend(e)
case ConnectEvent: case ConnectEvent:
requestBackend(e) requestBackend(e)
case RouteAllEvent:
requestBackend(e)
case CopyEvent: case CopyEvent:
w.WriteClipboard(e.Text) w.WriteClipboard(e.Text)
case GoogleAuthEvent: case GoogleAuthEvent:

@ -27,8 +27,8 @@ import (
"golang.org/x/exp/shiny/materialdesign/icons" "golang.org/x/exp/shiny/materialdesign/icons"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/netmap"
"eliasnaur.com/font/roboto/robotobold"
"eliasnaur.com/font/roboto/robotoregular" "eliasnaur.com/font/roboto/robotoregular"
_ "image/png" _ "image/png"
@ -47,14 +47,25 @@ type UI struct {
// webSigin is the button for the web-based sign-in flow. // webSigin is the button for the web-based sign-in flow.
webSignin widget.Clickable webSignin widget.Clickable
// googleSignin is the button for native Google Sign-in // googleSignin is the button for native Google Sign-in.
googleSignin widget.Clickable googleSignin widget.Clickable
// openExitDialog opens the exit node picker.
openExitDialog widget.Clickable
signinType signinType signinType signinType
self widget.Clickable self widget.Clickable
peers []widget.Clickable peers []widget.Clickable
// exitDialog is state for the exit node dialog.
exitDialog struct {
show bool
dismiss Dismiss
exits widget.Enum
list layout.List
}
intro struct { intro struct {
list layout.List list layout.List
start widget.Clickable start widget.Clickable
@ -68,6 +79,7 @@ type UI struct {
copy widget.Clickable copy widget.Clickable
reauth widget.Clickable reauth widget.Clickable
exits widget.Clickable
logout widget.Clickable logout widget.Clickable
} }
@ -81,11 +93,10 @@ type UI struct {
icons struct { icons struct {
search *widget.Icon search *widget.Icon
more *widget.Icon more *widget.Icon
exitStatus *widget.Icon
logo paint.ImageOp logo paint.ImageOp
google paint.ImageOp google paint.ImageOp
} }
events []UIEvent
} }
type signinType uint8 type signinType uint8
@ -101,6 +112,12 @@ type UIPeer struct {
Peer *tailcfg.Node Peer *tailcfg.Node
} }
// menuItem describes an item in a popup menu.
type menuItem struct {
title string
btn *widget.Clickable
}
const ( const (
headerColor = 0x496495 headerColor = 0x496495
infoColor = 0x3a517b infoColor = 0x3a517b
@ -131,6 +148,10 @@ func newUI(store *stateStore) (*UI, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
exitStatus, err := widget.NewIcon(icons.NavigationMenu)
if err != nil {
return nil, err
}
logoData, err := tailscalePngBytes() logoData, err := tailscalePngBytes()
if err != nil { if err != nil {
return nil, err return nil, err
@ -151,7 +172,14 @@ func newUI(store *stateStore) (*UI, error) {
if err != nil { if err != nil {
panic(fmt.Sprintf("failed to parse font: %v", err)) panic(fmt.Sprintf("failed to parse font: %v", err))
} }
fonts := []text.FontFace{{Font: text.Font{Typeface: "Roboto"}, Face: face}} faceBold, err := opentype.Parse(robotobold.TTF)
if err != nil {
panic(fmt.Sprintf("failed to parse font: %v", err))
}
fonts := []text.FontFace{
{Font: text.Font{Typeface: "Roboto"}, Face: face},
{Font: text.Font{Typeface: "Roboto", Weight: text.Bold}, Face: faceBold},
}
ui := &UI{ ui := &UI{
theme: material.NewTheme(fonts), theme: material.NewTheme(fonts),
store: store, store: store,
@ -159,13 +187,16 @@ func newUI(store *stateStore) (*UI, error) {
ui.intro.show, _ = store.ReadBool(keyShowIntro, true) ui.intro.show, _ = store.ReadBool(keyShowIntro, true)
ui.icons.search = searchIcon ui.icons.search = searchIcon
ui.icons.more = moreIcon ui.icons.more = moreIcon
ui.icons.exitStatus = exitStatus
ui.icons.logo = paint.NewImageOp(logo) ui.icons.logo = paint.NewImageOp(logo)
ui.icons.google = paint.NewImageOp(google) ui.icons.google = paint.NewImageOp(google)
ui.icons.more.Color = rgb(white) ui.icons.more.Color = rgb(white)
ui.icons.search.Color = mulAlpha(ui.theme.Palette.Fg, 0xbb) ui.icons.search.Color = mulAlpha(ui.theme.Palette.Fg, 0xbb)
ui.icons.exitStatus.Color = rgb(white)
ui.root.Axis = layout.Vertical ui.root.Axis = layout.Vertical
ui.intro.list.Axis = layout.Vertical ui.intro.list.Axis = layout.Vertical
ui.search.SingleLine = true ui.search.SingleLine = true
ui.exitDialog.list.Axis = layout.Vertical
return ui, nil return ui, nil
} }
@ -183,14 +214,14 @@ func (ui *UI) onBack() bool {
} }
func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientState) []UIEvent { func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientState) []UIEvent {
ui.events = nil var events []UIEvent
if ui.enabled.Changed() { if ui.enabled.Changed() {
ui.events = append(ui.events, ConnectEvent{Enable: ui.enabled.Value}) events = append(events, ConnectEvent{Enable: ui.enabled.Value})
} }
for _, e := range ui.search.Events() { for _, e := range ui.search.Events() {
if _, ok := e.(widget.ChangeEvent); ok { if _, ok := e.(widget.ChangeEvent); ok {
ui.events = append(ui.events, SearchEvent{Query: ui.search.Text()}) events = append(events, SearchEvent{Query: ui.search.Text()})
break break
} }
} }
@ -199,36 +230,57 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat
} }
netmap := state.backend.NetworkMap netmap := state.backend.NetworkMap
var localName, localAddr string var (
var expiry time.Time localName, localAddr string
expiry time.Time
userID tailcfg.UserID
exitID tailcfg.StableNodeID
)
if netmap != nil { if netmap != nil {
userID = netmap.User
expiry = netmap.Expiry expiry = netmap.Expiry
localName = netmap.SelfNode.DisplayName(false) localName = netmap.SelfNode.DisplayName(false)
if addrs := netmap.Addresses; len(addrs) > 0 { if addrs := netmap.Addresses; len(addrs) > 0 {
localAddr = addrs[0].IP.String() localAddr = addrs[0].IP.String()
} }
} }
if p := state.backend.Prefs; p != nil {
exitID = p.ExitNodeID
}
if d := &ui.exitDialog; d.show {
if newID := tailcfg.StableNodeID(d.exits.Value); newID != exitID {
d.show = false
events = append(events, RouteAllEvent{newID})
}
} else {
d.exits.Value = string(exitID)
}
if ui.googleSignin.Clicked() { if ui.googleSignin.Clicked() {
ui.signinType = googleSignin ui.signinType = googleSignin
ui.events = append(ui.events, GoogleAuthEvent{}) events = append(events, GoogleAuthEvent{})
} }
if ui.webSignin.Clicked() { if ui.webSignin.Clicked() {
ui.signinType = webSignin ui.signinType = webSignin
ui.events = append(ui.events, WebAuthEvent{}) events = append(events, WebAuthEvent{})
} }
if ui.menuClicked(&ui.menu.copy) && localAddr != "" { if ui.menuClicked(&ui.menu.copy) && localAddr != "" {
ui.copyAddress(gtx, localAddr) events = append(events, CopyEvent{Text: localAddr})
ui.showCopied(gtx, localAddr)
} }
if ui.menuClicked(&ui.menu.reauth) { if ui.menuClicked(&ui.menu.reauth) {
ui.events = append(ui.events, ReauthEvent{}) events = append(events, ReauthEvent{})
}
if ui.menuClicked(&ui.menu.exits) || ui.openExitDialog.Clicked() {
ui.exitDialog.show = true
} }
if ui.menuClicked(&ui.menu.logout) { if ui.menuClicked(&ui.menu.logout) {
ui.events = append(ui.events, LogoutEvent{}) events = append(events, LogoutEvent{})
} }
for len(ui.peers) < len(state.Peers) { for len(ui.peers) < len(state.Peers) {
@ -238,7 +290,7 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat
ui.peers = ui.peers[:max] ui.peers = ui.peers[:max]
} }
const numHeaders = 5 const numHeaders = 6
n := numHeaders + len(state.Peers) n := numHeaders + len(state.Peers)
needsLogin := state.backend.State == ipn.NeedsLogin needsLogin := state.backend.State == ipn.NeedsLogin
ui.root.Layout(gtx, n, func(gtx C, idx int) D { ui.root.Layout(gtx, n, func(gtx C, idx int) D {
@ -256,18 +308,24 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat
if netmap == nil || state.backend.State < ipn.Stopped { if netmap == nil || state.backend.State < ipn.Stopped {
return D{} return D{}
} }
for ui.self.Clicked() {
events = append(events, CopyEvent{Text: localAddr})
ui.showCopied(gtx, localAddr)
}
return ui.layoutLocal(gtx, sysIns, localName, localAddr) return ui.layoutLocal(gtx, sysIns, localName, localAddr)
case 2: case 2:
return ui.layoutExitStatus(gtx, &state.backend)
case 3:
if state.backend.State < ipn.Stopped { if state.backend.State < ipn.Stopped {
return D{} return D{}
} }
return ui.layoutSearchbar(gtx, sysIns) return ui.layoutSearchbar(gtx, sysIns)
case 3: case 4:
if !needsLogin || state.backend.LostInternet { if !needsLogin || state.backend.LostInternet {
return D{} return D{}
} }
return ui.layoutSignIn(gtx, &state.backend) return ui.layoutSignIn(gtx, &state.backend)
case 4: case 5:
if !state.backend.LostInternet { if !state.backend.LostInternet {
return D{} return D{}
} }
@ -280,24 +338,33 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat
p := &state.Peers[pidx] p := &state.Peers[pidx]
if p.Peer == nil { if p.Peer == nil {
name := p.Name name := p.Name
if p.Owner == netmap.User { if p.Owner == userID {
name = "MY DEVICES" name = "MY DEVICES"
} }
return ui.layoutSection(gtx, sysIns, name) return ui.layoutSection(gtx, sysIns, name)
} else { } else {
clk := &ui.peers[pidx] clk := &ui.peers[pidx]
return ui.layoutPeer(gtx, sysIns, p, netmap, clk) if clk.Clicked() {
if addrs := p.Peer.Addresses; len(addrs) > 0 {
a := addrs[0].IP.String()
events = append(events, CopyEvent{Text: a})
ui.showCopied(gtx, a)
}
}
return ui.layoutPeer(gtx, sysIns, p, userID, clk)
} }
} }
}) })
}) })
ui.layoutExitNodeDialog(gtx, sysIns, state.backend.Exits)
// Popup messages. // Popup messages.
ui.layoutMessage(gtx, sysIns) ui.layoutMessage(gtx, sysIns)
// 3-dots menu. // 3-dots menu.
if ui.menu.show { if ui.menu.show {
ui.layoutMenu(gtx, sysIns, expiry) ui.layoutMenu(gtx, sysIns, expiry, exitID != "" || len(state.backend.Exits) > 0)
} }
// "Get started". // "Get started".
@ -309,7 +376,7 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat
ui.layoutIntro(gtx, sysIns) ui.layoutIntro(gtx, sysIns)
} }
return ui.events return events
} }
func (ui *UI) ShowMessage(msg string) { func (ui *UI) ShowMessage(msg string) {
@ -321,10 +388,11 @@ func (ui *UI) ShowMessage(msg string) {
type Dismiss struct { type Dismiss struct {
} }
func (d *Dismiss) Add(gtx layout.Context) { func (d *Dismiss) Add(gtx layout.Context, color color.NRGBA) {
defer op.Save(gtx.Ops).Load() defer op.Save(gtx.Ops).Load()
pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops) pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops)
pointer.InputOp{Tag: d, Types: pointer.Press}.Add(gtx.Ops) pointer.InputOp{Tag: d, Types: pointer.Press}.Add(gtx.Ops)
paint.Fill(gtx.Ops, color)
} }
func (d *Dismiss) Dismissed(gtx layout.Context) bool { func (d *Dismiss) Dismissed(gtx layout.Context) bool {
@ -338,6 +406,51 @@ func (d *Dismiss) Dismissed(gtx layout.Context) bool {
return false return false
} }
func (ui *UI) layoutExitStatus(gtx layout.Context, state *BackendState) layout.Dimensions {
var bg color.NRGBA
var text string
switch state.ExitStatus {
case ExitNone:
return D{}
case ExitOffline:
text = "Exit node offline"
bg = rgb(0xc65835)
case ExitOnline:
text = "Using exit node"
bg = rgb(0x338b51)
}
paint.Fill(gtx.Ops, bg)
return material.Clickable(gtx, &ui.openExitDialog, func(gtx C) D {
gtx.Constraints.Min.X = gtx.Constraints.Max.X
return layout.Inset{
Top: unit.Dp(12),
Bottom: unit.Dp(12),
Right: unit.Dp(24),
Left: unit.Dp(24),
}.Layout(gtx, func(gtx C) D {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
lbl := material.Body2(ui.theme, text)
lbl.Color = rgb(white)
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
node := material.Body2(ui.theme, state.Exit.Label)
node.Color = argb(0x88ffffff)
return node.Layout(gtx)
}),
)
}),
layout.Rigid(func(gtx C) D {
return ui.icons.exitStatus.Layout(gtx, unit.Dp(24))
}),
)
})
})
}
// layoutSignIn lays out the sign in button(s). // layoutSignIn lays out the sign in button(s).
func (ui *UI) layoutSignIn(gtx layout.Context, state *BackendState) layout.Dimensions { func (ui *UI) layoutSignIn(gtx layout.Context, state *BackendState) layout.Dimensions {
return layout.Inset{Top: unit.Dp(48), Left: unit.Dp(48), Right: unit.Dp(48)}.Layout(gtx, func(gtx C) D { return layout.Inset{Top: unit.Dp(48), Left: unit.Dp(48), Right: unit.Dp(48)}.Layout(gtx, func(gtx C) D {
@ -509,28 +622,77 @@ func (ui *UI) menuClicked(btn *widget.Clickable) bool {
return cl return cl
} }
// layoutMenu lays out the menu activated by the 3 dots button. // layoutExitNodeDialog lays out the exit node selection dialog. If the user changed the node,
func (ui *UI) layoutMenu(gtx layout.Context, sysIns system.Insets, expiry time.Time) { // true is returned along with the node.
ui.menu.dismiss.Add(gtx) func (ui *UI) layoutExitNodeDialog(gtx layout.Context, sysIns system.Insets, exits []ExitNode) {
if ui.menu.dismiss.Dismissed(gtx) { d := &ui.exitDialog
ui.menu.show = false if d.dismiss.Dismissed(gtx) {
d.show = false
} }
if !d.show {
return
}
d.dismiss.Add(gtx, argb(0x66000000))
layout.Inset{ layout.Inset{
Top: unit.Add(gtx.Metric, sysIns.Top, unit.Dp(2)), Top: unit.Add(gtx.Metric, sysIns.Top, unit.Dp(16)),
Right: unit.Add(gtx.Metric, sysIns.Right, unit.Dp(2)), Right: unit.Add(gtx.Metric, sysIns.Right, unit.Dp(16)),
Bottom: unit.Add(gtx.Metric, sysIns.Bottom, unit.Dp(16)),
Left: unit.Add(gtx.Metric, sysIns.Left, unit.Dp(16)),
}.Layout(gtx, func(gtx C) D { }.Layout(gtx, func(gtx C) D {
return layout.NE.Layout(gtx, func(gtx C) D { return layout.Center.Layout(gtx, func(gtx C) D {
return widget.Border{Color: argb(0x33000000), CornerRadius: unit.Dp(2), Width: unit.Px(1)}.Layout(gtx, func(gtx C) D { gtx.Constraints.Min.X = gtx.Px(unit.Dp(250))
return Background{Color: rgb(0xfafafa), CornerRadius: unit.Dp(2)}.Layout(gtx, func(gtx C) D { gtx.Constraints.Max.X = gtx.Constraints.Min.X
menu := &ui.menu return layoutDialog(gtx, func(gtx C) D {
items := []struct { return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
btn *widget.Clickable layout.Rigid(func(gtx C) D {
title string // Header.
}{ return layout.Inset{
{title: "Copy My IP Address", btn: &menu.copy}, Top: unit.Dp(16),
{title: "Reauthenticate", btn: &menu.reauth}, Right: unit.Dp(20),
{title: "Log out", btn: &menu.logout}, Left: unit.Dp(20),
Bottom: unit.Dp(16),
}.Layout(gtx, func(gtx C) D {
l := material.Body1(ui.theme, "Use exit node...")
l.Font.Weight = text.Bold
return l.Layout(gtx)
})
}),
layout.Flexed(1, func(gtx C) D {
gtx.Constraints.Min.Y = 0
// Add "none" exit node.
n := len(exits) + 1
return d.list.Layout(gtx, n, func(gtx C, idx int) D {
switch idx {
case n - 1:
} }
node := ExitNode{Label: "None", Online: true}
if idx >= 1 {
node = exits[idx-1]
}
lbl := node.Label
if !node.Online {
lbl = lbl + " (offline)"
}
btn := material.RadioButton(ui.theme, &d.exits, string(node.ID), lbl)
if !node.Online {
btn.Color = rgb(0xbbbbbb)
btn.IconColor = btn.Color
}
return layout.Inset{
Right: unit.Dp(16),
Left: unit.Dp(16),
Bottom: unit.Dp(16),
}.Layout(gtx, btn.Layout)
})
}),
)
})
})
})
}
func layoutMenu(th *material.Theme, gtx layout.Context, items []menuItem, header layout.Widget) layout.Dimensions {
return layoutDialog(gtx, func(gtx C) D {
// Lay out menu items twice; once for // Lay out menu items twice; once for
// measuring the widest item, once for actual layout. // measuring the widest item, once for actual layout.
var maxWidth int var maxWidth int
@ -544,19 +706,7 @@ func (ui *UI) layoutMenu(gtx layout.Context, sysIns system.Insets, expiry time.T
Bottom: unit.Dp(4), Bottom: unit.Dp(4),
}.Layout(gtx, func(gtx C) D { }.Layout(gtx, func(gtx C) D {
gtx.Constraints.Min.X = minWidth gtx.Constraints.Min.X = minWidth
var expiryStr string dims := header(gtx)
const fmtStr = time.Stamp
switch {
case expiry.IsZero():
expiryStr = "Expires: (never)"
case time.Now().After(expiry):
expiryStr = fmt.Sprintf("Expired: %s", expiry.Format(fmtStr))
default:
expiryStr = fmt.Sprintf("Expires: %s", expiry.Format(fmtStr))
}
l := material.Caption(ui.theme, expiryStr)
l.Color = rgb(0x8f8f8f)
dims := l.Layout(gtx)
if w := dims.Size.X; w > maxWidth { if w := dims.Size.X; w > maxWidth {
maxWidth = w maxWidth = w
} }
@ -570,7 +720,7 @@ func (ui *UI) layoutMenu(gtx layout.Context, sysIns system.Insets, expiry time.T
return material.Clickable(gtx, it.btn, func(gtx C) D { return material.Clickable(gtx, it.btn, func(gtx C) D {
return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx C) D { return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx C) D {
gtx.Constraints.Min.X = minWidth gtx.Constraints.Min.X = minWidth
dims := material.Body1(ui.theme, it.title).Layout(gtx) dims := material.Body1(th, it.title).Layout(gtx)
if w := dims.Size.X; w > maxWidth { if w := dims.Size.X; w > maxWidth {
maxWidth = w maxWidth = w
} }
@ -589,6 +739,50 @@ func (ui *UI) layoutMenu(gtx layout.Context, sysIns system.Insets, expiry time.T
minWidth = maxWidth minWidth = maxWidth
return f.Layout(gtx, children...) return f.Layout(gtx, children...)
}) })
}
func layoutDialog(gtx layout.Context, w layout.Widget) layout.Dimensions {
return widget.Border{Color: argb(0x33000000), CornerRadius: unit.Dp(2), Width: unit.Px(1)}.Layout(gtx, func(gtx C) D {
return Background{Color: rgb(0xfafafa), CornerRadius: unit.Dp(2)}.Layout(gtx, w)
})
}
// layoutMenu lays out the menu activated by the 3 dots button.
func (ui *UI) layoutMenu(gtx layout.Context, sysIns system.Insets, expiry time.Time, showExits bool) {
ui.menu.dismiss.Add(gtx, color.NRGBA{})
if ui.menu.dismiss.Dismissed(gtx) {
ui.menu.show = false
}
layout.Inset{
Top: unit.Add(gtx.Metric, sysIns.Top, unit.Dp(2)),
Right: unit.Add(gtx.Metric, sysIns.Right, unit.Dp(2)),
}.Layout(gtx, func(gtx C) D {
return layout.NE.Layout(gtx, func(gtx C) D {
menu := &ui.menu
items := []menuItem{
{title: "Copy My IP Address", btn: &menu.copy},
}
if showExits {
items = append(items, menuItem{title: "Use exit node...", btn: &menu.exits})
}
items = append(items,
menuItem{title: "Reauthenticate", btn: &menu.reauth},
menuItem{title: "Log out", btn: &menu.logout},
)
return layoutMenu(ui.theme, gtx, items, func(gtx C) D {
var expiryStr string
const fmtStr = time.Stamp
switch {
case expiry.IsZero():
expiryStr = "Expires: (never)"
case time.Now().After(expiry):
expiryStr = fmt.Sprintf("Expired: %s", expiry.Format(fmtStr))
default:
expiryStr = fmt.Sprintf("Expires: %s", expiry.Format(fmtStr))
}
l := material.Caption(ui.theme, expiryStr)
l.Color = rgb(0x8f8f8f)
return l.Layout(gtx)
}) })
}) })
}) })
@ -627,12 +821,7 @@ func (ui *UI) showMessage(gtx layout.Context, msg string) {
// layoutPeer lays out a peer name and IP address (e.g. // layoutPeer lays out a peer name and IP address (e.g.
// "localhost\n100.100.100.101") // "localhost\n100.100.100.101")
func (ui *UI) layoutPeer(gtx layout.Context, sysIns system.Insets, p *UIPeer, netmap *netmap.NetworkMap, clk *widget.Clickable) layout.Dimensions { func (ui *UI) layoutPeer(gtx layout.Context, sysIns system.Insets, p *UIPeer, user tailcfg.UserID, clk *widget.Clickable) layout.Dimensions {
for clk.Clicked() {
if addrs := p.Peer.Addresses; len(addrs) > 0 {
ui.copyAddress(gtx, addrs[0].IP.String())
}
}
return material.Clickable(gtx, clk, func(gtx C) D { return material.Clickable(gtx, clk, func(gtx C) D {
return layout.Inset{ return layout.Inset{
Top: unit.Dp(8), Top: unit.Dp(8),
@ -644,7 +833,7 @@ func (ui *UI) layoutPeer(gtx layout.Context, sysIns system.Insets, p *UIPeer, ne
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx C) D { return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx C) D {
name := p.Peer.DisplayName(p.Peer.User == netmap.User) name := p.Peer.DisplayName(p.Peer.User == user)
return material.H6(ui.theme, name).Layout(gtx) return material.H6(ui.theme, name).Layout(gtx)
}) })
}), }),
@ -722,11 +911,9 @@ func (ui *UI) layoutTop(gtx layout.Context, sysIns system.Insets, state *Backend
if state.State <= ipn.NeedsLogin { if state.State <= ipn.NeedsLogin {
return D{} return D{}
} }
return material.Clickable(gtx, &ui.menu.open, func(gtx C) D { btn := material.IconButton(ui.theme, &ui.menu.open, ui.icons.more)
return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx C) D { btn.Background = color.NRGBA{}
return ui.icons.more.Layout(gtx, unit.Dp(24)) return btn.Layout(gtx)
})
})
}), }),
) )
}) })
@ -750,17 +937,13 @@ func statusString(state ipn.State) string {
} }
} }
func (ui *UI) copyAddress(gtx layout.Context, addr string) { func (ui *UI) showCopied(gtx layout.Context, addr string) {
ui.events = append(ui.events, CopyEvent{Text: addr})
ui.showMessage(gtx, fmt.Sprintf("Copied %s", addr)) ui.showMessage(gtx, fmt.Sprintf("Copied %s", addr))
} }
// layoutLocal lays out the information box about the local node's // layoutLocal lays out the information box about the local node's
// name and IP address. // name and IP address.
func (ui *UI) layoutLocal(gtx layout.Context, sysIns system.Insets, host, addr string) layout.Dimensions { func (ui *UI) layoutLocal(gtx layout.Context, sysIns system.Insets, host, addr string) layout.Dimensions {
for ui.self.Clicked() {
ui.copyAddress(gtx, addr)
}
return Background{Color: rgb(headerColor)}.Layout(gtx, func(gtx C) D { return Background{Color: rgb(headerColor)}.Layout(gtx, func(gtx C) D {
return layout.Inset{ return layout.Inset{
Right: unit.Max(gtx.Metric, sysIns.Right, unit.Dp(8)), Right: unit.Max(gtx.Metric, sysIns.Right, unit.Dp(8)),

Loading…
Cancel
Save