From 7cfeb9906c8b832f3757d6b2fe52696dfae7de3a Mon Sep 17 00:00:00 2001
From: shrijayan <81805145+shrijayan@users.noreply.github.com>
Date: Mon, 13 Apr 2026 17:56:31 +0530
Subject: [PATCH 1/2] all: add Windows support for install, syslog, and login
link
Add Windows native messaging host registration via the registry,
split syslog setup into platform-specific files since log/syslog
is unavailable on Windows, handle .exe binary suffix, and fix the
popup login link to actually open the auth URL.
Signed-off-by: shrijayan <81805145+shrijayan@users.noreply.github.com>
---
popup.js | 12 +++++++++---
registry_other.go | 6 ++++++
registry_windows.go | 27 +++++++++++++++++++++++++++
syslog_other.go | 24 ++++++++++++++++++++++++
syslog_windows.go | 7 +++++++
ts-browser-ext.go | 42 +++++++++++++++++++++++++++---------------
6 files changed, 100 insertions(+), 18 deletions(-)
create mode 100644 registry_other.go
create mode 100644 registry_windows.go
create mode 100644 syslog_other.go
create mode 100644 syslog_windows.go
diff --git a/popup.js b/popup.js
index 9d8b28b..a583209 100644
--- a/popup.js
+++ b/popup.js
@@ -45,9 +45,15 @@ document.addEventListener("DOMContentLoaded", () => {
return;
}
if (status.needsLogin) {
- stateDisplay.innerHTML = status.browseToURL
- ? `Log in`
- : "Login required; no URL";
+ if (status.browseToURL) {
+ stateDisplay.innerHTML = `Log in`;
+ stateDisplay.querySelector("a").addEventListener("click", (e) => {
+ e.preventDefault();
+ chrome.tabs.create({ url: status.browseToURL });
+ });
+ } else {
+ stateDisplay.innerHTML = "Login required; no URL";
+ }
return;
}
if (typeof status === "string" && status === "Disconnected") {
diff --git a/registry_other.go b/registry_other.go
new file mode 100644
index 0000000..f633555
--- /dev/null
+++ b/registry_other.go
@@ -0,0 +1,6 @@
+//go:build !windows
+
+package main
+
+func installRegistry(browserByte, jsonPath string) error { return nil }
+func uninstallRegistry(browserByte string) error { return nil }
diff --git a/registry_windows.go b/registry_windows.go
new file mode 100644
index 0000000..b1d974d
--- /dev/null
+++ b/registry_windows.go
@@ -0,0 +1,27 @@
+package main
+
+import "golang.org/x/sys/windows/registry"
+
+func registryKeyPath(browserByte string) string {
+ if browserByte == "F" {
+ return `Software\Mozilla\NativeMessagingHosts\com.tailscale.browserext.firefox`
+ }
+ return `Software\Google\Chrome\NativeMessagingHosts\com.tailscale.browserext.chrome`
+}
+
+func installRegistry(browserByte, jsonPath string) error {
+ key, _, err := registry.CreateKey(registry.CURRENT_USER, registryKeyPath(browserByte), registry.SET_VALUE)
+ if err != nil {
+ return err
+ }
+ defer key.Close()
+ return key.SetStringValue("", jsonPath)
+}
+
+func uninstallRegistry(browserByte string) error {
+ err := registry.DeleteKey(registry.CURRENT_USER, registryKeyPath(browserByte))
+ if err == registry.ErrNotExist {
+ return nil
+ }
+ return err
+}
diff --git a/syslog_other.go b/syslog_other.go
new file mode 100644
index 0000000..dc098ef
--- /dev/null
+++ b/syslog_other.go
@@ -0,0 +1,24 @@
+//go:build !windows
+
+package main
+
+import (
+ "fmt"
+ "log"
+ "log/syslog"
+
+ "tailscale.com/types/logger"
+)
+
+func trySetSyslog(logf *logger.Logf) {
+ w, err := syslog.Dial("tcp", "localhost:5555", syslog.LOG_INFO, "browser")
+ if err != nil {
+ log.Printf("syslog: %v", err)
+ return
+ }
+ log.Printf("syslog dialed")
+ *logf = func(f string, a ...any) {
+ fmt.Fprintf(w, f, a...)
+ }
+ log.SetOutput(w)
+}
diff --git a/syslog_windows.go b/syslog_windows.go
new file mode 100644
index 0000000..075bd6c
--- /dev/null
+++ b/syslog_windows.go
@@ -0,0 +1,7 @@
+package main
+
+import "tailscale.com/types/logger"
+
+func trySetSyslog(logf *logger.Logf) {
+ // syslog is not available on Windows; use default logging.
+}
diff --git a/ts-browser-ext.go b/ts-browser-ext.go
index fb8cc54..26486cd 100644
--- a/ts-browser-ext.go
+++ b/ts-browser-ext.go
@@ -10,7 +10,6 @@ import (
"fmt"
"io"
"log"
- "log/syslog"
"net"
"net/http"
"net/http/httputil"
@@ -70,15 +69,7 @@ To register it once, run:
h := newHost(os.Stdin, os.Stdout)
- if w, err := syslog.Dial("tcp", "localhost:5555", syslog.LOG_INFO, "browser"); err == nil {
- log.Printf("syslog dialed")
- h.logf = func(f string, a ...any) {
- fmt.Fprintf(w, f, a...)
- }
- log.SetOutput(w)
- } else {
- log.Printf("syslog: %v", err)
- }
+ trySetSyslog(&h.logf)
ln := h.getProxyListener()
port := ln.Addr().(*net.TCPAddr).Port
@@ -112,8 +103,10 @@ func getTargetDir(browserByte string) (string, error) {
} else if browserByte == "F" {
dir = filepath.Join(home, "Library", "Application Support", "Mozilla", "NativeMessagingHosts")
}
+ case "windows":
+ dir = filepath.Join(home, "AppData", "Local", "TailscaleBrowserExt")
default:
- return "", fmt.Errorf("TODO: implement support for installing on %q", runtime.GOOS)
+ return "", fmt.Errorf("unsupported OS %q", runtime.GOOS)
}
if err := os.MkdirAll(dir, 0755); err != nil {
return "", err
@@ -127,7 +120,11 @@ func uninstall() error {
if err != nil {
return err
}
- targetBin := filepath.Join(targetDir, "ts-browser-ext")
+ binName := "ts-browser-ext"
+ if runtime.GOOS == "windows" {
+ binName += ".exe"
+ }
+ targetBin := filepath.Join(targetDir, binName)
targetJSON := filepath.Join(targetDir, "com.tailscale.browserext.chrome.json")
if browserByte == "F" {
targetJSON = filepath.Join(targetDir, "com.tailscale.browserext.firefox.json")
@@ -138,6 +135,9 @@ func uninstall() error {
if err := os.Remove(targetJSON); err != nil && !os.IsNotExist(err) {
return err
}
+ if err := uninstallRegistry(browserByte); err != nil {
+ return fmt.Errorf("removing registry key: %w", err)
+ }
}
return nil
}
@@ -167,13 +167,21 @@ func install(installArg string) error {
if err != nil {
return err
}
- targetBin := filepath.Join(targetDir, "ts-browser-ext")
+ binName := "ts-browser-ext"
+ if runtime.GOOS == "windows" {
+ binName += ".exe"
+ }
+ targetBin := filepath.Join(targetDir, binName)
if err := os.WriteFile(targetBin, binary, 0755); err != nil {
return err
}
log.SetFlags(0)
log.Printf("copied binary to %v", targetBin)
+ // Use forward slashes in the JSON path so it works on all platforms
+ // (Chrome/Firefox on Windows accept forward slashes).
+ jsonBinPath := filepath.ToSlash(targetBin)
+
var targetJSON string
var jsonConf []byte
@@ -188,7 +196,7 @@ func install(installArg string) error {
"allowed_origins": [
"chrome-extension://%s/"
]
- }`, targetBin, extension)
+ }`, jsonBinPath, extension)
case "F":
targetJSON = filepath.Join(targetDir, "com.tailscale.browserext.firefox.json")
jsonConf = fmt.Appendf(nil, `{
@@ -199,7 +207,7 @@ func install(installArg string) error {
"allowed_extensions": [
"browser-ext@tailscale.com"
]
- }`, targetBin)
+ }`, jsonBinPath)
default:
return fmt.Errorf("unknown browser prefix byte %q", browserByte)
}
@@ -207,6 +215,10 @@ func install(installArg string) error {
return err
}
log.Printf("wrote registration to %v", targetJSON)
+
+ if err := installRegistry(browserByte, targetJSON); err != nil {
+ return fmt.Errorf("writing registry: %w", err)
+ }
return nil
}
From f3fb3fec27d977ef81620668de522f4793cbfc8f Mon Sep 17 00:00:00 2001
From: shrijayan <81805145+shrijayan@users.noreply.github.com>
Date: Mon, 13 Apr 2026 17:56:41 +0530
Subject: [PATCH 2/2] all: clean up dead code, add build tag, extract
binaryName helper
Remove unused browseToURL function and lastStatus variable from
popup.js, add explicit //go:build windows tag to registry_windows.go,
and extract binaryName() to deduplicate the platform-conditional
binary naming in install/uninstall.
Signed-off-by: shrijayan <81805145+shrijayan@users.noreply.github.com>
---
popup.js | 8 --------
registry_windows.go | 2 ++
ts-browser-ext.go | 19 +++++++++----------
3 files changed, 11 insertions(+), 18 deletions(-)
diff --git a/popup.js b/popup.js
index a583209..9f0bd95 100644
--- a/popup.js
+++ b/popup.js
@@ -1,11 +1,3 @@
-var lastStatus;
-
-function browseToURL() {
- if (lastStatus && lastStatus.browseToURL) {
- chrome.tabs.create({ url: lastStatus.browseToURL });
- }
-}
-
document.addEventListener("DOMContentLoaded", () => {
const toggleSlider = document.getElementById("toggleSlider");
const slider = document.querySelector(".slider");
diff --git a/registry_windows.go b/registry_windows.go
index b1d974d..3ab1d7d 100644
--- a/registry_windows.go
+++ b/registry_windows.go
@@ -1,3 +1,5 @@
+//go:build windows
+
package main
import "golang.org/x/sys/windows/registry"
diff --git a/ts-browser-ext.go b/ts-browser-ext.go
index 26486cd..4b790ea 100644
--- a/ts-browser-ext.go
+++ b/ts-browser-ext.go
@@ -114,17 +114,20 @@ func getTargetDir(browserByte string) (string, error) {
return dir, nil
}
+func binaryName() string {
+ if runtime.GOOS == "windows" {
+ return "ts-browser-ext.exe"
+ }
+ return "ts-browser-ext"
+}
+
func uninstall() error {
for _, browserByte := range []string{"C", "F"} {
targetDir, err := getTargetDir(browserByte)
if err != nil {
return err
}
- binName := "ts-browser-ext"
- if runtime.GOOS == "windows" {
- binName += ".exe"
- }
- targetBin := filepath.Join(targetDir, binName)
+ targetBin := filepath.Join(targetDir, binaryName())
targetJSON := filepath.Join(targetDir, "com.tailscale.browserext.chrome.json")
if browserByte == "F" {
targetJSON = filepath.Join(targetDir, "com.tailscale.browserext.firefox.json")
@@ -167,11 +170,7 @@ func install(installArg string) error {
if err != nil {
return err
}
- binName := "ts-browser-ext"
- if runtime.GOOS == "windows" {
- binName += ".exe"
- }
- targetBin := filepath.Join(targetDir, binName)
+ targetBin := filepath.Join(targetDir, binaryName())
if err := os.WriteFile(targetBin, binary, 0755); err != nil {
return err
}