From 5ac35b665b5e2308faa7a48f85b45b7b29e7d551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20Lensb=C3=B8l?= Date: Thu, 26 Feb 2026 12:59:45 -0500 Subject: [PATCH] client/systray: add installer for a freedesktop autostart file (#18767) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds freedesktop as an option for installing autostart desktop files for starting the systray application. Fixes #18766 Signed-off-by: Claus Lensbøl --- client/freedesktop/freedesktop.go | 43 +++++++ client/freedesktop/freedesktop_test.go | 145 +++++++++++++++++++++++ client/systray/startup-creator.go | 142 +++++++++++++++++++++- client/systray/tailscale-systray.desktop | 13 ++ client/systray/tailscale.png | Bin 0 -> 14069 bytes client/systray/tailscale.svg | 14 +++ cmd/tailscale/cli/configure_linux.go | 2 +- cmd/tailscale/depaware.txt | 1 + 8 files changed, 358 insertions(+), 2 deletions(-) create mode 100644 client/freedesktop/freedesktop.go create mode 100644 client/freedesktop/freedesktop_test.go create mode 100644 client/systray/tailscale-systray.desktop create mode 100644 client/systray/tailscale.png create mode 100644 client/systray/tailscale.svg diff --git a/client/freedesktop/freedesktop.go b/client/freedesktop/freedesktop.go new file mode 100644 index 000000000..6ed1e8ccf --- /dev/null +++ b/client/freedesktop/freedesktop.go @@ -0,0 +1,43 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +// Package freedesktop provides helpers for freedesktop systems. +package freedesktop + +import "strings" + +const needsEscape = " \t\n\"'\\><~|&;$*?#()`" + +var escaper = strings.NewReplacer(`"`, `\"`, "`", "\\`", `$`, `\$`, `\`, `\\`) + +// Quote quotes according to the Desktop Entry Specification, as below: +// +// Arguments may be quoted in whole. If an argument contains a reserved +// character the argument must be quoted. The rules for quoting of arguments is +// also applicable to the executable name or path of the executable program as +// provided. +// +// Quoting must be done by enclosing the argument between double quotes and +// escaping the double quote character, backtick character ("`"), dollar sign +// ("$") and backslash character ("\") by preceding it with an additional +// backslash character. Implementations must undo quoting before expanding field +// codes and before passing the argument to the executable program. Reserved +// characters are space (" "), tab, newline, double quote, single quote ("'"), +// backslash character ("\"), greater-than sign (">"), less-than sign ("<"), +// tilde ("~"), vertical bar ("|"), ampersand ("&"), semicolon (";"), dollar +// sign ("$"), asterisk ("*"), question mark ("?"), hash mark ("#"), parenthesis +// ("(") and (")") and backtick character ("`"). +func Quote(s string) string { + if s == "" { + return `""` + } + if !strings.ContainsAny(s, needsEscape) { + return s + } + + var b strings.Builder + b.WriteString(`"`) + escaper.WriteString(&b, s) + b.WriteString(`"`) + return b.String() +} diff --git a/client/freedesktop/freedesktop_test.go b/client/freedesktop/freedesktop_test.go new file mode 100644 index 000000000..07a1104f3 --- /dev/null +++ b/client/freedesktop/freedesktop_test.go @@ -0,0 +1,145 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +package freedesktop + +import ( + "strings" + "testing" +) + +func TestEscape(t *testing.T) { + tests := []struct { + name, input, want string + }{ + { + name: "no illegal chars", + input: "/home/user", + want: "/home/user", + }, + { + name: "empty string", + input: "", + want: "\"\"", + }, + { + name: "space", + input: " ", + want: "\" \"", + }, + { + name: "tab", + input: "\t", + want: "\"\t\"", + }, + { + name: "newline", + input: "\n", + want: "\"\n\"", + }, + { + name: "double quote", + input: "\"", + want: "\"\\\"\"", + }, + { + name: "single quote", + input: "'", + want: "\"'\"", + }, + { + name: "backslash", + input: "\\", + want: "\"\\\\\"", + }, + { + name: "greater than", + input: ">", + want: "\">\"", + }, + { + name: "less than", + input: "<", + want: "\"<\"", + }, + { + name: "tilde", + input: "~", + want: "\"~\"", + }, + { + name: "pipe", + input: "|", + want: "\"|\"", + }, + { + name: "ampersand", + input: "&", + want: "\"&\"", + }, + { + name: "semicolon", + input: ";", + want: "\";\"", + }, + { + name: "dollar", + input: "$", + want: "\"\\$\"", + }, + { + name: "asterisk", + input: "*", + want: "\"*\"", + }, + { + name: "question mark", + input: "?", + want: "\"?\"", + }, + { + name: "hash", + input: "#", + want: "\"#\"", + }, + { + name: "open paren", + input: "(", + want: "\"(\"", + }, + { + name: "close paren", + input: ")", + want: "\")\"", + }, + { + name: "backtick", + input: "`", + want: "\"\\`\"", + }, + { + name: "char without escape", + input: "/home/user\t", + want: "\"/home/user\t\"", + }, + { + name: "char with escape", + input: "/home/user\\", + want: "\"/home/user\\\\\"", + }, + { + name: "all illegal chars", + input: "/home/user" + needsEscape, + want: "\"/home/user \t\n\\\"'\\\\><~|&;\\$*?#()\\`\"", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Quote(tt.input) + if strings.Compare(got, tt.want) != 0 { + t.Errorf("expected %s, got %s", tt.want, got) + } + }) + } +} diff --git a/client/systray/startup-creator.go b/client/systray/startup-creator.go index 34d85e617..369190012 100644 --- a/client/systray/startup-creator.go +++ b/client/systray/startup-creator.go @@ -10,20 +10,34 @@ import ( "bufio" "bytes" _ "embed" + "errors" "fmt" "os" "os/exec" "path/filepath" "strings" + + "tailscale.com/client/freedesktop" ) //go:embed tailscale-systray.service var embedSystemd string +//go:embed tailscale-systray.desktop +var embedFreedesktop string + +//go:embed tailscale.svg +var embedLogoSvg string + +//go:embed tailscale.png +var embedLogoPng string + func InstallStartupScript(initSystem string) error { switch initSystem { case "systemd": return installSystemd() + case "freedesktop": + return installFreedesktop() default: return fmt.Errorf("unsupported init system '%s'", initSystem) } @@ -58,7 +72,7 @@ func installSystemd() error { systemdDir := filepath.Join(configDir, "systemd", "user") if err := os.MkdirAll(systemdDir, 0o755); err != nil { - return fmt.Errorf("failed creating systemd uuser dir: %w", err) + return fmt.Errorf("failed creating systemd user dir: %w", err) } serviceFile := filepath.Join(systemdDir, "tailscale-systray.service") @@ -74,3 +88,129 @@ func installSystemd() error { return nil } + +func installFreedesktop() error { + tmpDir, err := os.MkdirTemp("", "tailscale-systray") + if err != nil { + return fmt.Errorf("unable to make tmpDir: %w", err) + } + defer os.RemoveAll(tmpDir) + + // Install icon, and use it if it works, and if not change to some generic + // network/vpn icon. + iconName := "tailscale" + if err := installIcon(tmpDir); err != nil { + iconName = "network-transmit" + fmt.Printf("unable to install icon, continuing without: %s\n", err.Error()) + } + + // Create desktop file in a tmp dir + desktopTmpPath := filepath.Join(tmpDir, "tailscale-systray.desktop") + if err := os.WriteFile(desktopTmpPath, []byte(embedFreedesktop), + 0o0755); err != nil { + return fmt.Errorf("unable to create desktop file: %w", err) + } + + // Ensure autostart dir exists and install the desktop file + configDir, err := os.UserConfigDir() + if err != nil { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("unable to locate user home: %w", err) + } + configDir = filepath.Join(homeDir, ".config") + } + + autostartDir := filepath.Join(configDir, "autostart") + if err := os.MkdirAll(autostartDir, 0o644); err != nil { + return fmt.Errorf("failed creating freedesktop autostart dir: %w", err) + } + + desktopCmd := exec.Command("desktop-file-install", "--dir", autostartDir, + desktopTmpPath) + if output, err := desktopCmd.Output(); err != nil { + return fmt.Errorf("unable to install desktop file: %w - %s", err, output) + } + + // Find the path to tailscale, just in case it's not where the example file + // has it placed, and replace that before writing the file. + tailscaleBin, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to find tailscale binary %w", err) + } + tailscaleBin = freedesktop.Quote(tailscaleBin) + + // Make possible changes to the desktop file + runEdit := func(args ...string) error { + cmd := exec.Command("desktop-file-edit", args...) + out, err := cmd.Output() + if err != nil { + return fmt.Errorf("cmd: %s: %w\n%s", cmd.String(), err, out) + } + return nil + } + + edits := [][]string{ + {"--set-key=Exec", "--set-value=" + tailscaleBin + " systray"}, + {"--set-key=TryExec", "--set-value=" + tailscaleBin}, + {"--set-icon=" + iconName}, + } + + var errs []error + desktopFile := filepath.Join(autostartDir, "tailscale-systray.desktop") + for _, args := range edits { + args = append(args, desktopFile) + if err := runEdit(args...); err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return fmt.Errorf( + "failed changing autostart file, try rebooting: %w", errors.Join(errs...)) + } + + fmt.Printf("Successfully installed freedesktop autostart service to: %s\n", desktopFile) + fmt.Println("The service will run upon logging in.") + + return nil +} + +// installIcon installs an icon using the freedesktop tools. SVG support +// is still on its way for some distros, notably missing on Ubuntu 25.10 as of +// 2026-02-19. Try to install both icons and let the DE decide from what is +// available. +// Reference: https://gitlab.freedesktop.org/xdg/xdg-utils/-/merge_requests/116 +func installIcon(tmpDir string) error { + svgPath := filepath.Join(tmpDir, "tailscale.svg") + if err := os.WriteFile(svgPath, []byte(embedLogoSvg), 0o0644); err != nil { + return fmt.Errorf("unable to create svg: %w", err) + } + + pngPath := filepath.Join(tmpDir, "tailscale.png") + if err := os.WriteFile(pngPath, []byte(embedLogoPng), 0o0644); err != nil { + return fmt.Errorf("unable to create png: %w", err) + } + + var errs []error + installed := false + svgCmd := exec.Command("xdg-icon-resource", "install", "--size", "scalable", + "--novendor", svgPath, "tailscale") + if output, err := svgCmd.Output(); err != nil { + errs = append(errs, fmt.Errorf("unable to install svg: %s - %s", err, output)) + } else { + installed = true + } + pngCmd := exec.Command("xdg-icon-resource", "install", "--size", "512", + "--novendor", pngPath, "tailscale") + if output, err := pngCmd.Output(); err != nil { + errs = append(errs, fmt.Errorf("unable to install png: %s - %s", err, output)) + } else { + installed = true + } + + if !installed { + return errors.Join(errs...) + } + return nil +} diff --git a/client/systray/tailscale-systray.desktop b/client/systray/tailscale-systray.desktop new file mode 100644 index 000000000..b79b72d18 --- /dev/null +++ b/client/systray/tailscale-systray.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=Tailscale System Tray +Comment=Tailscale system tray applet for managing Tailscale +Exec=/usr/bin/tailscale systray +TryExec=/usr/bin/tailscale +Terminal=false +NoDisplay=true +StartupNotify=false +Icon=tailscale +Categories=Network;System; +X-GNOME-Autostart-enabled=true diff --git a/client/systray/tailscale.png b/client/systray/tailscale.png new file mode 100644 index 0000000000000000000000000000000000000000..d476e88fc262f5409fc76b484e0550a398196df5 GIT binary patch literal 14069 zcmdUW^ zDM-i0GrT{q=a2aQvb*;^edf%Z>zs4#OI>X>8cJ450051Ky0RVsNca;8D9GT~j(7hN z0FpHt$_fVFi1i5zCyV*Sjeo>ZPHql+C$%4Kx2=7{*mUXR8Pjfbs+buM1oyqk=ulTM zV9iF;541OBDXe!?@&xB_I`B7kq=*^nOY5Z@Df2VwslAjwzg)xnh0-nJ(&w*=Wj1Pz z(XCHe`6-HqOzTVc!^_w4w!ZZ@MW$XqSmPy|sonD-)GrcmI_CYvO8oxVY&CiC$iZQL zm6xTBaHDL&D)H{Nqh6CGGH|5|qRgI7yHE5O%f0&_vjn80@7 z!uv~@#& z7363Lq5Ge!2zeM3bKmKZx0}-OreHucDDAQwnHUZ4Kf08}w0nhVx8JFNNremeweRn`AU|=D4 z&Y@UaTbtD{%W;(IcXDh;dSC^_6|xKm7oTOxjXiXKNF+YLS&p6{Q+~?t(Xk8 z5wUNNNwC5WWyu-Kr#1UnP~iIIRmvK5;}l;KEG-`nOec=y3T*m43?Qf|X_8>i{$^l! zov!iZ=Wr7psQVpDPJ!$=4F+57aBXccFc+408UMTq&PxlNLu)dH4{UVDpdi9^^0R2Zn3ih_=4cmoefu|#M>%MobS*_B| zB7ibq4x-O=f4|{iZ#`ll$JkEj+8s||zmEd(w_^G zB~jqs)iTo<_Dy^e!crJvUt3?-M#Lh(RY8rdCwSWDlj~HQ!rUl=A0};{2mM}0IR~#<0B~W=@j`M+&|Swc zp?_#&02q(Wur%kH_5iIB^&?_r0IPPUm}N)_Ta3WY(1Fd5t2_EQo+qLt*BJmG0brvm&muojfJ0A$`3gCp_@vlyaNwPOH;?c;)M=>; zX0xIK6(YZOb9j*8A$-j9NFL|0lekh@G9BbJ`juk}sAMMnFE zJ$^QgMxmcs&lHMlRE*OBEZL8Wn{M}f!SFxW!ffCu02@xzfJTA@0su?D8zf0-A23XB zZ50jT+u#K{dS%Gbc52t34m*e!(x`I=AmvN0guJYrbd`yP`As|j{-KLt5RU-E@9H+l zmsZaJuOqm1`prlF7;K#oqQ78Y{vH7L#D3$=DVN&Lplf$AJpeR(=#NGE5V}+91QDCw zRsi^1$jSCMrCd5Z1M=Wz_^jmj^z=M{%2~3js}l?C@hH%JPPU#uB~ugp@cDcifWXy5 zLxijuveos#=IJni>h`Xs+%-AJvUc<>O`xnPGOL_W`mX>=E5)jW@muw|F3P1}XBxJ) zwxl9YgjFB4yJCtk`wPB7L14ds^+NEMk-|snm24H*CgU6%vb-PBdWk4DzC#Wxv{eDC z)_9QV{I;wgu{1RC5)SEWdhVVk$AK0(_UIwzAqs$?*bK3ipJSJtr9qlT0Nt(TtVh3?>Y18kvZ1<2(*TDmgSvDy3PlX=^kdlmG z<&+IPkHE`mCLBvcik+XISEB*oy4@EGZ2ksGt1J-#+|2a&qr8sO8ve=vO--n`;M050 z6*{luS_^U@4Et_yHZ`kXjXJU*#U3?3FM{30MtN4Ve9Hk13vhn8=z%rVgrAi$f?Fdd zpI`&`s-oy2R;^gL!6b8Il^MX>j9gGlh7bk-mcgJ{X0kZb2AF8a6fgs@sdi3tp92Nf z;o@*H@t(O8&M0)<+vq*maow`F1p1g39V>GR0kCG$7Bt&T}ttM~+B!HV&OLD(}4z#tM z?Cp2^veRz<;2w$_w$zC4-}6LpKRgAc@-58)uCvmdv#rjd9cqPo^WfxZH)nF}2o%Hp zyN0u{68L$q;}TJ?TAl>HGO=_Q!44p>0`NVBUbY2L9|05q9j#Yv_||U^zN8O(GArV8 zlUhBNJgu$m9L-W=UioA^rf$7#Y!E+E>NvFaR!6RIb*O-C{PXP7?GC9$F305x|ARka zWaUdH6CQn9T3r%}Hqy4A9G8sr_}B9v?Cf$$-~vXlm;TmlYqN22l@}Hk;#RHEE6P06 zzuyZSwp4G_ISxFM5V^J_&vAjH*rNLByo#08xzs!c4iOF+j^h1EUlF1NsqIyd$nfpF zgQW#?29C=ifirQ-2MDcklNGCFaQim9`rEn7p-b7`S#C>8L3hegx>Dj-9k4#f>*d=8 zJu=cRUQ_;F0pouwW@%T`VN)@l{Bn<1%ihbwo@~8UcY-TILPBuEh?unH+4qNs=e7$X zrUZ4g<=)0~h$?HM-h_vdeYQM{Hxf5{2G_ie2xnPXSgx4B5b?4iM|pfCHM@gmhmU_O z?1<$`e7Tx3o4y?d&Xwmb$s1OBEXUQkm?NhtvFbEcSVr-&k4Z_LL0DV9b0U@x+(rRY zwC_f(lvO+nZ>`SG9g^#|u@m}#>KBuIw`L_@L|b$d9ryoE9ENp@JxABDZjictL0y%Q z*k#b{LxtMg>%XDgOrgREFuwasE%2@M^X%TyYQY!|SCcBij;wsD_$qNuV0swc&cXF^ZmupDzNMV28U?mG@IvGwuFK67OS2kpT1KyuDj zshvvomfKt}lw;aEJ39O?5sMCLvwvw=AWbAmI!aA3GSs&P43y>eE4yy6472t$nXZ2) z=V~`2xe9~@k+b?(fr?W;^+}+Zf^ctDvp;?iWUzVFSdJqcq>G=I(F*WBFFL4m)C-1 z{O85Q#P%*@wk|K^tdEsDx))m1`Q3=8|7O!dJ64aWcOEP2@JpY+)H9z%p&!FbEQr67 zMA~o>8`zs6IyKtT!AD|mG{2(y)so*fng^E~Pr7R7@RV(=JL_Q8Zr9;u9+M}P#8YmQ zhxjjN!G{kN0MVr9wgRW-=lfzh)N%YkJZtv8(CoQvnS4}$; zyqUe&Z+aO?M+d>4xaZh@$ADk-j2)`h_G1nWG|P+qS|1gMb9!C|G)D($*ODs<6G$go z`9l5K3r+I9@DV^)l7y$kv-N3-DvP3MPXnudf}y-4MU!&zO!VGx$rtUgzmC|e4?-{{ zgb_ilm^(rH;@_IDvNvDSJmnrL_#z!Rl>lnb&0=}^FFn#Zx>c-h z<+@YMbo9~x@8nPBGL@FdbU;jBLr`e8|1$3~5OJ}bd26C%rXXMan(fA}WkmXJ{aH=K zcasqaEgVLsg_vSY$kU#`4l!sEY&LzM(DbuK7`;ZSJHf_!JJ2?@3t@{ldUk@Yhc5w^yK?weMl|fh@h7X4B7`+$ z!HT`G%GK-Rl+4W0bn>|FZcg}kRo?^9^HQGjnTK{olv|ku&_M&xG0jD(VK-D@B{k$$ z=h^HycNvKdTDiJhAoVlUq_zd6+X{;@PjIuA+~SI&T@G$`J4 zb}<`u$N8xe&FAIQHLl$Pn~aw&9Qn`Z*U2%y9W~Cg-B0=sNBwWRo}oWm zjE*)oKCgtPsgzFcpkW$LHB($5HF5k?TgZcefXVJpZ7Ls3UEC>4B9#c|f%n>a`IKbt z;uD^pdm5sqUAu~nm4obwr|VCHo2opfIt4FR#m*|!FPO5t*@gD{d+`v0h!6AfvmGpTZ*IH=2$S{qPqfpac!PYt| z@8=U(na(8dDf8vhS2!}zxe!<7k!ZmD?hz`+HTFUe=~dtL(xLXtB!BPP+iY7g!)vZP zWoOljIBK1a_|;;~>U$2Y^0P(q$y%V}B!Vuh*uEXB%Xx_+hY?p8!q#pGu zWMZ(2bs7xM^Ho}vze$2^$fBax6z%C^{f%XW-~T8dp3P3a_#r>R<={ugz$QIFrdgly z(HpwQ*n2na)sRTQKPoB<^;E4LPU_mL&0CX^rxD!fCP9IImieZLb3yDLrNInak$NqH zKNDJ(7JGfKDYGhxf9O7}boa={Kp@;YGilNd01$fhq%{469?s+ z4e?9+LrTZ0Vji9LoSqwHu2XW=v#(^@4W*5wZAHn~O-_UJ>46$N==%*9sa4mreYY#S z{5vZ*Gj?Z&tS}QfYR9xOIjLrCOsNwIh=~h8SzGA$^73*j-EDrHPy1K7IN7HQw-k83 zd3L3D=g}tY4bl(Kr~Xet0F#Hh zL2t>GBDzS-hfzD4y`m-H*M)a|t(I=PgkFG5IA^%;A*RT|ijSWSb)g3Uyr8E;Apy4{g#6&VGW__z_Y?D=foyHGv~6!~#AdAAwX6QD zf33eq|JjAbZ7VB0eq+0!z_RwyVpnpPyiG+sN4W(9!{Opgg^-}&99PrhA8t%7{PHd% zho$(wn=^?-B9|Y@kvQ#%l^k(T`GtXJ|+g5+nL&lMecT8*VK1E zzPb}IzS5%I;w-@+6@^POz!| zvC?e0)uLsAcx5(D0Z!}xF+R;=`^PHcd9UT^VUKb1C%A)uHE(}Bc7&jxtA0B%^(>(@ z{1x^&m{A5Vq9cNO?y|-)l6+HyOLrDa_(2x@v^M#8zTz$TmulXW^LxkzHu^A!p zCo8i5q3_XhW`}Xr?fq39xnD785(*Hg|4;Yde(4MG@GB$`k5?Q@2AON-Jq*E`{1wbf zXC0EXcOh$Yeay_l#^K*gpB5cnKj)aOaR&DU&DUh*QaOM2cvS>5()sCz%|<=3E=2I% zjEK+1a(-~t6iUjU@*KSWr2{crTW#MNiHb`+XebY%;2EnIJS4#eII2UT^!@c>W6Nhv za)bjLC2w!mdnx91=1w{06$ov4MvjE&aEyZd+pc4l(Vq2KB>_T+H5X;0GC4OoJ_@%! zMaT=eod`N^D?ZX8tXUByhPyBEbnE%|J%tl);`o6M-_jEKPB{h|ErF>Rer-^$1L}3pxyknLC4iSJ*vScU!a)ax^Fie zt{bi)n3^{|>rd0oiQwjrKWq`_e!0<}RU zqaIIm-)=YNClrQAUg5M^%Q2B^cKCGv3O73m7JXn&QvQ2rByK~+Eys^W{%+Ar(Gn)> zHItBVK#v_J=j}JXXs#j~UIqI5{#j}R%7!%y9>R&8@xBi-aeG`Rb0F+XAq_G5k*ew_P7^?OIy zJX>u!#h3tv5&@!@_SolA>T=Y#Rz}@qcPVSnKFX`^46C2zc3Au4zb1vmw(t>Fvn7^~ zQhtsSee%7Ox1t*#n}zAlofI)g4+{L1+W3mwP;)bD*&X3+$-VKR2&W2RU`Sf&GI6)Q z-H^R_(45xgZrOJqLDWGMn7&e4)$^&X_zV*5C`?{1yFX!P|EjRn>VM#{jmv6fDfj2R z>ECaaN25!%%5NN_{=WDRc}GLkxvIqD&@nI+3ekG3>QMyjY;a<9tkgZTGE}j5B>&|p zcbb+w*$-+s6*z++JzwF@TNKU^bJy91jzN46`{A4kUTtq0dNDgVvZsR?HJ`OMZj`7| z4S*o?1?*z4=gvtNV$xCpPedzH#gO2+a-m8AJ&>N2*kV8cW_q$;o#sv#-N)(qk>Dyf z;~5~#1Gv?V3oIpu1O)||N6q4rr9hAZ*o=;7>VQ2jF+FF4SbmefDX%yuB4U9Rf^^|J z)laU8LgsZ&n{+U%!x+=a#~bkEUqzu{4@<$DWVoB>NlF%HR^17>XG*?n6Pt7!jxddK zvq@tpDWc#tkctc+k z&s3aQ$qoKqBw=jDRvSyw0bZawLtwY=`IV`=cRM8wWB$&A_b*|Tgym;-h{#tNG;Uk2Y6eq|CY zHY@JnCfg(*je3{daqgjTnf2#niC~EbQ%dcsO?%tTSU?|V68^HW)0peoPsP;l5}}^+ zx{3*m8{e6$T;dOl|A|1r4jiyF_^+U}Ln%IraUvnw&g5uBk zQy^I1`B~C-ORrLi)Z8o7rSP7AZ(KtQy@Kl*2@ApWNROHl^B?WbettWbY$lZ=)3PRj!F<}(`-DKx_M-FeljeaaqgbGQ<=A1!6D~2Oe=@`R{7bx z=ze6IV-^nt=BT&Nv1A$d$q-BdG^&O2gB51^jEu7Esx46sgNgX#{V{0nsdvrWWEe)S z(FD?vfH`q)Egc=YnhR)%($7;+KglyyQ7R9K+SLfHO>x(T zK0G`eUs-I&^1?^GMWacX1Hn-rz{hzbZ>H<(_~5U*bDXo(c!hpxwS!&i zq=?2o1Yl`;8u|}-_^*$_JmviWIpXfX6mc?NSa<=3f@MQ&4#GDFy~m$w-JVO1m^EV0 znW?y6;ZftlUcP4HOx(%bo*EjGt`FZ1pAGFe4`5K8=PyJj=wJ%?Tloe2_`mm)(25lv z_uKAxx|&x$3X_V2$9-B-BhKX$A25T-Omeu*AGmFp9oQ-;Ft25|h>XX2q9eiC{?E`D7wP3-C?*xohPIN$b^fFZPWCd(%iImvVuN1`kl z2xC9xI&z1+#9a5>>s>pkQz&VT|8ghs-5^ad^*N@6!)w^cKE-E$!pr&o{qK`7h<=kh zWk39R!D~F7Y{}Pd*HA^QoOa?(2k5k2px=2(c|74Ym6&WNy}_7UuDA#zrJYiVUT85A zkgLb&Porwa5j@%hcK60y7g3DG@ALG(%j5d>%j~+6Zj$V@y=1t&_ES@2{ys3%XY@~W zHKQ3`&k{@7Uhj~a;`O#+LV_p!IOz9btjIsxGJlfP2_Zn7Km0w2y!>Y06d!3hMC-6I z+m}X6m+by-qNxvn9 z*FE3+Mn$|E{P(2U+{(Rv^}Jghy3BEp#QTO=iK$9q?GIaCbNy!``uXeyN-DeWJBD;~ zh#=9*{m=VzF);72fO4kw!|x7#;fgD%BSIwZJ-v!cNOx&~_dD;Ar>*m5-{HcOZkIHd z2FHTh=8W+dk&#iaZ+Q7!DjIBUZH@Yz`3fC*a&zFfz3LrL?(;*%2}*n| zbi$)CKh-%TnJ~3s{;6&LnHd>tTk~zfSao`E{v;-!@bTj>SKgTm&ISh9m(Hr$ABcTA z^3LVsPa+OgF^lztt=M8tn==C!gI0?0O;$LSx{)rmqhkRN0XNJ}k6P%r(*T({Lu?WW z=z&2NTe-e9WmI3fW~`Hg!!O0mnF0$!UEh(HAP>4pe{nRKu$&y%DJ5kS`S{<8#Z|2vz9{#<8X$ zia}1TP4y`PtbUcB3fX+|vihgBiu z!V3=~x-=1mo};V|&qVuaUW=XwB-{%KjqsM-Bh^@kl@FDLdQ=lF7ugdX3qzg%-Egy! zK%&8pTXGyZ?;Wt|tlMkjZlrn2x2=7qtv$kPxE9y~*8t zsHGYdWt6KbeSwnlS1l)_RMPxJ%0+{UF~a+gZX> zsX_!hRKfJE3wjHEDRW2H(CsorvyYSIUFI1bm=fa9ti(Tv@odsI7R)WlblIMDCCP>k zY-zor^>XF&=ny@1QmP;SCNktvqoo@;{FavKB=BhmkLi#P5L$X@UEfM&ffOPKT_=aUK=lxvh#byw&lQ zY{RX$ba5tEIku{hFcshUCL$IUy4AWIa9?oC_=-lKN|4v}Wf&w!q5o~PH_@K*bsnAF z{iknqEFg&RRu^))8?#=?4E!oaI=)ph#|+O6CN*Dhp<4Sd0#7)Jz{6fyG(mv%ZRkWa zC{e4QSMP?MaT`zqCRDeVAi`r|YXSr|wD%wY+1$!NPUv2~ z@RK`duJVxplI4@Q{}6Oa-2X{D`BzJ_gM^(z3?o+Jg+KB{`2@n00`zJ4O|yR{g468h z$kReXNR@n{+d=ZT6WSf6Ag`|%&F(@luX>ry`PZH~d$b91sl5p_R1UIm0J?OC_ZNZA zYPOH<%{kUW3KhiCaHV!1uuubrr&CwzNp=>dlL6|=V${tS6Yw0Nj)F=`>2L9 zpzW5t6mpB>@*m1PwWBA$2LEmNcj>yX4U29ER%zxwE7;_5`ciuy7*JbU>)!pBFokRO zdyG4HTm_7h@xC4oyeb8%iE4jePnU)h6v(*&^{vtl%r@<$NGycq>nQQIC!k(^^WPva`(u=&mYwbrm zCa-31)bS3POQK^t0Uw*T{8_G;oSR$vhK(5|#eMYApj17}fF3r$2)aFezJG`638hUx z_rFCMZuy$QidRL2NV9gi8vW_}wO=`0LBkBFkcWlmlYcWR5rNI`l8Ly*(0g+h7$o}M zyWW8$Y72Y$%nwNG3VyWwmw-XtC0S-A&6^*R#Uz_JvEe+-@l0XkU;EH$BkJ~vuk4cj z2O-FDP3`T0|ZIc;~j`TB%j&xxTry@ov=P~$xH zB&;$Zqd@*JEku1orS`ItxliBq#wrX}Rdp*LlTC;S=eho9ETlx$G*>AU=k(M$no5Xd z$?lZIFyl@Vb)G{m1@e6Q36lqk>ZJP4#O??TPa1v7-#@0WU3wyE z%$UG13G>ydF>jb_ShJIl_27d7eI^@pRhR z+PcxD?$Fjfk?b=O*F}unJKI9qk!Gm7H|~|}JXJSgo*_&9I$+@Stltuyx5V1i@} z=NRHLbj7~@XeH<75+CjRrlzLF-KJ6C7RzJL8W$eCvC@#1vSmfwwUQ#!?H}wdWlsSq z+|%dr`2MYWw?EHQRY?tFyDM3%BsiC9)1z5@yI-lswA&)4$&a>F0W%ke;p@i*Jd{lh zX$lT74`ZH!uz&hgk}I-exz#7$BB%9H1EC#7cfF6M`(JOKzm@l&5Iz{WWK1>Na7c8_ zCkeD3-I# zF%n(rciqDB)&59Y#c7iM{F>#Agxo1{dWXH#527KM?Y)gD*RUk-#*EBNXL|A3Aw15; zFZ_PN60Y}I#b6|Qk*xW18lHG_fV^dX|6LY zJ!IBpGti{tCr~Tbrq8C+yTORKFDIG7S}87NB;dA=FguA5)!JmOO#P#KPPtyk-wby= zUtM|7)z!sN^ak=b>az{_MHa2j)qnH^d$t@{DQXiX9&}JYhu8T1@`Z&-?HPS(`Hq58T^e3-$gDv)-zC3V zvw8`;YToC53Q~T|`<|&asEV0_1I4pI`@aE6u}{u|8Sk=-Ww(Q38qvixTF?NskzcT7 z|9dAkWh4o~JO)mo&6PnWg&tRx6Np5!S8Um>@QLffI^8gT`D zD=`p-?qZT&b_XZ+&FTLLSs8{Ba;C?b`uh6DMIlH|Dr$hf{?$EJIL~KmY_tu$r&xmA zGy0fI#)o-Spv*Rw8&cKGM$^iqsDKA_H7V4ye?<`n$$=yi3Ftpa7=yX`fw?h$NH}|b z=O*IYo$Oyy%SM47kex@N5cA1pZ!A4y%JFG40NDNBvkAwVLUjqWupb29`&*d_gn+&c zP8Nrw4oIYw(Ym~=k+YE$|EVUiCOZ8z#&jw(=Iz=3E7lQ=g}sTtcm=C5>2i=B_9{|1GNRVy0tZ=nrn z0nxkkmg2D4YrX8*xmtJ7EkyD*?GbE6K5#3&XqOhtN%u(zg=>IqwyPY7no=AfAIhcS zT)Lj|+r3xD2*wIlIL(&QX9rz(Cp>{*J?r!{$j9rv6*FgFJ~ieETJKUqf;p&I^C+Jh zhL2LHOTQdWYWy19v2ya-AeL5bfH~ssUo$EI?qRFOamGtN4EIuRO6)$0&p6>sjz{wr zySuw5kF?eo71E;DF^cElkYNurci1Mg>i^JUZ)<5;l!yyK`7-Czm>&(<&vJmV;fJtk z3g(2zIe?(Af-R^=F4+-_vSAOzk2hW5VD@FbZ>BuR(G@TPf#n%#T(gp#Kx7fSM&L{u z=$%R#>eGR4wt=?07aFoNasr0x`ydN-70h9%_Mk>x)lKjHAhUU)Ol6?`U}?#FZ@vVo zblt6vq^>~%=7CwsE-jj1bw&2AC-N(t%h6>G{&Y^+#PZ9C6hc}{(F8noBw8G zsmG7U8*%ORX;`7K4_KzzyWrnDAVJ<2j~P44t=9xn1d+pH@V@7<-}_J+wUGg5OYsAgq#d=+dn;;O9`*v@ux$7c~mMbBPabe z|3@TTk~!Xom~?ZY+ze%gv^PkKKhh{;cWZ!2Tyx27OyxDHeJRO%u*f9+DFzF#6?(CO zMxF^d3?G8qY$j`B8Vhfi7luYCLn=cCXNu4w5?#oj^DR!t8dqL}7sGqR#(}Htbxq(o z(j}Wv3s6@3P&wiZjzdjx4^xa0;`(_;0<*KT?#&r!fF_C2TmF~3w?BUh!zQ`|xXn2! zz>Du!h4&qsUIOlcrl!7N7`@6ez=>E??L^*a{0O*DUc4|DAj}v5dPM-4tHNh7kA(pR z4;&n698rIr1q8z>ZhmZQvid{=6yZTXXx*I=(IyAKl)ygRL~q5R!|Es5Rx(PpOq;6j z+U7rsYhfClgAhRb>%^q7*MW>P=jJ7#eir$A*N*%hq|+MrbFO`vk#|hm38ol zj||%{DETvY+b)p~GQ2B#ojRq)0_dd%KHrxga4r5cMKXN5^e#RN+FF}@I3H@w04t(q zEWuLZ*Uh4DKI3qaz@_ck1|CUp>dh9uWUg2XzVUT{Srs>E#RJD+@&$ z*5ECWLRa*GxLudOFU6oto^of9+FNjy>}Crds+w%-2TVL?o_W=rXB#GO8j1bJ3-vt2 zsTm`E+%lWme#Az~0w|%mw=4S4aT=1=gNl%c2KL1SRRwa83QziQH19{PN5uiIe^<&` z;D1bz6Hv%zrKN;LfmiZP1;(FGfzK}J!BYnTgQw%*d0A1kTy?$p73gMJU2zM3h=97Y zekwrcp(jx#B67$JIiCBU#>o--atFQuac6*CTB;)GWv5*}NGzsM89~^;6QL3mal|xy zAp;bak?RE8&vYO}ki7jt8*<9d6o5tp5u93a6=oC-Od=rZo<43Af6SG+S)dB+1Gxc6 z%3&gW5p~&IB}1g3NCO2){4P+Td}+Y+&Aa#s9^j$jOXBqGP*jo%iA~_cu_=M!6x>%t zG0O)?vqtPz@0(_O2nyf~m4+yQncEkEXkM#7|9-%vROx48Q0`KFyAx)c0?5XM1foot zucMg(RxXPP9}E966SQVr+`d=-3Urh2b~XgA(HuXMq(e4j7#5x1)weyuD}be9>n`+J z3frQ;4T~dE00?DICwDgg5|h04M0!tVcb5%uuVE$dUI9{JkmtZX={5zokw#4WLX(`9 z-IaqNHQd!S22I)9UK$#L&Apa>bVWizDEJ=&j%_sgd*xNP1cL`dI#FuBG|VIT~W?>txjqz(t^*8L;b)D*CK9hr6f?P+pje-&}~ z){?y{M?}Kk{BUPoqZa}~1ofs0(?5x3yE%H<@m)WuN*incZv3F;aKvMp;~qR& TPdE!t8~_?B+RFKg_n!Yh!VnPt literal 0 HcmV?d00001 diff --git a/client/systray/tailscale.svg b/client/systray/tailscale.svg new file mode 100644 index 000000000..9e6990271 --- /dev/null +++ b/client/systray/tailscale.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/cmd/tailscale/cli/configure_linux.go b/cmd/tailscale/cli/configure_linux.go index ccde06c72..9ba3b8e87 100644 --- a/cmd/tailscale/cli/configure_linux.go +++ b/cmd/tailscale/cli/configure_linux.go @@ -33,7 +33,7 @@ func systrayConfigCmd() *ffcli.Command { FlagSet: (func() *flag.FlagSet { fs := newFlagSet("systray") fs.StringVar(&systrayArgs.initSystem, "enable-startup", "", - "Install startup script for init system. Currently supported systems are [systemd].") + "Install startup script for init system. Currently supported systems are [systemd, freedesktop].") return fs })(), } diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 8cef97258..d83ac2710 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -157,6 +157,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep software.sslmate.com/src/go-pkcs12/internal/rc2 from software.sslmate.com/src/go-pkcs12 tailscale.com from tailscale.com/version 💣 tailscale.com/atomicfile from tailscale.com/cmd/tailscale/cli+ + L tailscale.com/client/freedesktop from tailscale.com/client/systray tailscale.com/client/local from tailscale.com/client/tailscale+ L tailscale.com/client/systray from tailscale.com/cmd/tailscale/cli tailscale.com/client/tailscale from tailscale.com/internal/client/tailscale