From 572253244411d877100b6d9e3f2348f04e565e0d Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sun, 19 Jan 2025 12:37:42 -0800 Subject: [PATCH] cmd/ts-browser-native-ext: add start of a browser extension Updates #14689 Change-Id: Ia432ee53dcdee9b43a73adb2ab3be6a3ce235aa6 --- cmd/ts-browser-native-ext/background.js | 154 ++++++ cmd/ts-browser-native-ext/chrome.txt | 13 + cmd/ts-browser-native-ext/icon.png | Bin 0 -> 31174 bytes cmd/ts-browser-native-ext/manifest.json | 23 + cmd/ts-browser-native-ext/offline.png | Bin 0 -> 7507 bytes cmd/ts-browser-native-ext/online.png | Bin 0 -> 11971 bytes cmd/ts-browser-native-ext/popup.html | 13 + cmd/ts-browser-native-ext/popup.js | 16 + .../ts-browser-native-ext.go | 488 ++++++++++++++++++ 9 files changed, 707 insertions(+) create mode 100644 cmd/ts-browser-native-ext/background.js create mode 100644 cmd/ts-browser-native-ext/chrome.txt create mode 100644 cmd/ts-browser-native-ext/icon.png create mode 100644 cmd/ts-browser-native-ext/manifest.json create mode 100644 cmd/ts-browser-native-ext/offline.png create mode 100644 cmd/ts-browser-native-ext/online.png create mode 100644 cmd/ts-browser-native-ext/popup.html create mode 100644 cmd/ts-browser-native-ext/popup.js create mode 100644 cmd/ts-browser-native-ext/ts-browser-native-ext.go diff --git a/cmd/ts-browser-native-ext/background.js b/cmd/ts-browser-native-ext/background.js new file mode 100644 index 000000000..2da189c22 --- /dev/null +++ b/cmd/ts-browser-native-ext/background.js @@ -0,0 +1,154 @@ +// Flag to track proxy status +let proxyEnabled = false; + + +// Function to change the popup icon +function setPopupIcon(active) { + const iconPath = active ? "online.png" : "offline.png"; + + chrome.action.setIcon({ path: iconPath }, () => { + if (chrome.runtime.lastError) { + console.error("Error setting icon to " + active + ":", chrome.runtime.lastError.message); + } + }); +} + +// Function to enable the proxy +function enableProxy() { + if (disconnected) { + console.error("Cannot enable proxy, disconnected from native host"); + return; + } + + // Send message to port + if (lastProxyPort) { + nmPort.postMessage({ cmd: "get-status" }); + } else { + nmPort.postMessage({ cmd: "up" }); + } +} + +// Function to disable the proxy +function disableProxy() { + setProxy(0); + + if (disconnected) { + console.error("Cannot disable proxy, disconnected from native host"); + return; + } + + // Send message to port + //nmPort.postMessage({ cmd: "down" }); +} + +console.log("starting ts-browser-ext"); + +console.log("Connecting to native messaging host..."); +let nmPort = chrome.runtime.connectNative("com.tailscale.browserext.chrome"); +let disconnected = false; +let portError = ""; // error.message if/when nmPort disconnected + +nmPort.onDisconnect.addListener(() => { + disconnected = true; + const error = chrome.runtime.lastError; + if (error) { + console.error("Connection failed:", error.message); + portError = error.message; + } else { + console.error("Disconnected from native host"); + } +}); +nmPort.onMessage.addListener((message) => { + console.log("message from backend: ", message); + + let st = message.status; + if (st && st.running && st.proxyPort && proxyEnabled) { + setProxy(st.proxyPort); + } +}) + +var lastProxyPort = 0; + +function setProxy(proxyPort) { + if (proxyPort) { + lastProxyPort = proxyPort; + console.log("Enabling proxy at port: " + proxyPort); + } else { + console.log("Disabling proxy..."); + chrome.proxy.settings.set( + { + value: { + mode: "direct" + }, + scope: "regular" + }, + () => { + console.log("Proxy disabled."); + } + ); + return; + } + chrome.proxy.settings.set( + { + value: { + mode: "fixed_servers", + rules: { + singleProxy: { + scheme: "http", + host: "127.0.0.1", + port: proxyPort + }, + bypassList: [""] + } + }, + scope: "regular" + }, + () => { + console.log("Proxy enabled: 127.0.0.1:" + proxyPort); + } + ); +} + +chrome.storage.local.get("profileId", (result) => { + if (!result.profileId) { + const profileId = crypto.randomUUID(); + chrome.storage.local.set({ profileId }, () => { + console.log("Generated profile ID:", profileId); + nmPort.postMessage({ cmd: "init", initID: profileId }); + }); + } else { + console.log("Profile ID already exists:", result.profileId); + nmPort.postMessage({ cmd: "init", initID: result.profileId }); + } +}); + + +// Listener for messages from the popup +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.command === "queryState") { + if (disconnected) { + sendResponse({ status: "Error", error: portError }); + return; + } + console.log("bg: queryState, proxy=" + proxyEnabled); + sendResponse({ status: proxyEnabled ? "Connected" : "Disconnected" }); + return + } + + if (message.command === "toggleProxy") { + console.log("bg: toggleProxy, proxy=" + proxyEnabled); + proxyEnabled = !proxyEnabled; + if (proxyEnabled) { + enableProxy(); + console.log("bg: toggleProxy on, now proxy=" + proxyEnabled); + sendResponse({ status: "Connected" }); + console.log("bg: toggleProxy on, sent proxy=" + proxyEnabled); + } else { + disableProxy(); + console.log("bg: toggleProxy off, now proxy=" + proxyEnabled); + sendResponse({ status: "Disconnected" }); + console.log("bg: toggleProxy off, sent proxy=" + proxyEnabled); + } + setPopupIcon(proxyEnabled); + } +}); diff --git a/cmd/ts-browser-native-ext/chrome.txt b/cmd/ts-browser-native-ext/chrome.txt new file mode 100644 index 000000000..6c45dccde --- /dev/null +++ b/cmd/ts-browser-native-ext/chrome.txt @@ -0,0 +1,13 @@ +% pwd +/Users/bradfitz/Library/Application Support/Google/Chrome/NativeMessagingHosts + +% cat com.tailscale.chrome-ext.json +{ + "name": "com.tailscale.chrome-ext", + "description": "Tailscale Native Extension", + "path": "/Users/bradfitz/go/bin/ts-browser-native-ext", + "type": "stdio", + "allowed_origins": [ + "chrome-extension://gdopnimobeboikkiagbnnbcijkjdjcad/" + ] +} diff --git a/cmd/ts-browser-native-ext/icon.png b/cmd/ts-browser-native-ext/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..587f76ab3c8f0ce89a004bfb7c523c1825e4787c GIT binary patch literal 31174 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 NPR;N|Sk| z@ZXH1{}o8#Rrnf0Cm{X*Oa8AN3x*NUIs^aF@_4&GMR^_(m8|29(UpElt44ly~of|{C|HBtb;H|rN@CYa?t9lHP3VWLi>Lqp&H zEB7Js+6p6_gKE5U+uJ6cp67Yk6HxYX*_$^Y1G>@-VGWB&Zg6nW+SgYQ+PlJ+EA%Y) b^bXs)#>Q}h>%#Tl2sl|uMTuIFQSko(5Uh~G literal 0 HcmV?d00001 diff --git a/cmd/ts-browser-native-ext/manifest.json b/cmd/ts-browser-native-ext/manifest.json new file mode 100644 index 000000000..ded93dc8e --- /dev/null +++ b/cmd/ts-browser-native-ext/manifest.json @@ -0,0 +1,23 @@ +{ + "manifest_version": 3, + "name": "Tailscale Extension", + "version": "1.0", + "description": "A Tailscale client that runs as a browser extension, permitting use of different tailnets in differenet browser profiles, without affecting the system VPN or networking settings.", + "permissions": [ + "proxy", + "background", + "storage", + "nativeMessaging" + ], + "host_permissions": [ + "" + ], + "background": { + "service_worker": "background.js" + }, + "action": { + "default_popup": "popup.html", + "default_icon": "icon.png" + }, + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArAPB7I6tL6JJaivgzHLpDOmawSn4q8K4riQPWtXPL8N2ashAiGbsOuNW+7zJQUg+So1C/J2M32Wa1RzHExA/Gj4hekBjZvjY0zylTXQgnDJ/RVrQEENVq02Pfi5OpplIDwN5Yt7n8JQbYZP9NkOUUoumh0BFm4WLLal4GLt1S6QrwDctc1kxG1UKtcVgGi40aPz0efB0skn7lw1jzN2WGenqNY1x2BFQj/ol3zUMasb4rO3EdJWfD3kyjfDu5K4MvX4GZ3Stw3u25Z9cfNf6W1StrA/06JcYc/9AAjrfHjxrZGpDBGeKUe1KgU7iMX1J9SkaPooYJJbuiA1AdgTr9QIDAQAB" +} diff --git a/cmd/ts-browser-native-ext/offline.png b/cmd/ts-browser-native-ext/offline.png new file mode 100644 index 0000000000000000000000000000000000000000..09c2d1c15107fdc7741cfabba788c816650590bd GIT binary patch literal 7507 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 + + + Proxy Toggle + + + + +

Tailscale

+
+ + + diff --git a/cmd/ts-browser-native-ext/popup.js b/cmd/ts-browser-native-ext/popup.js new file mode 100644 index 000000000..9619fd21c --- /dev/null +++ b/cmd/ts-browser-native-ext/popup.js @@ -0,0 +1,16 @@ +document.addEventListener("DOMContentLoaded", () => { + let btn = document.getElementById("button"); + let st = document.getElementById("state"); + + let onState = (response) => { + console.log("popup: onState=" + response.status); + st.innerText = response.status; + btn.innerText = response.status === "Connected" ? "Disconnect" : "Connect"; + }; + + chrome.runtime.sendMessage({ command: "queryState" }, onState); + + btn.addEventListener("click", () => { + chrome.runtime.sendMessage({ command: "toggleProxy" }, onState); + }); +}) diff --git a/cmd/ts-browser-native-ext/ts-browser-native-ext.go b/cmd/ts-browser-native-ext/ts-browser-native-ext.go new file mode 100644 index 000000000..c534b2f53 --- /dev/null +++ b/cmd/ts-browser-native-ext/ts-browser-native-ext.go @@ -0,0 +1,488 @@ +package main + +import ( + "bufio" + "context" + "encoding/binary" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "log" + "log/syslog" + "net" + "net/http" + "net/http/httputil" + "os" + "os/user" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + + "tailscale.com/hostinfo" + "tailscale.com/ipn" + "tailscale.com/net/proxymux" + "tailscale.com/net/socks5" + "tailscale.com/tsnet" + "tailscale.com/types/logger" +) + +var ( + installFlag = flag.String("install", "", "register the browser extension's backend with the given browser, one of: chrome, firefox") +) + +func main() { + flag.Parse() + if *installFlag != "" { + if err := install(*installFlag); err != nil { + log.Fatalf("installation error: %v", err) + } + return + } + if flag.NArg() == 0 { + fmt.Printf(`ts-browser-native-ext is the backend for the Tailscale browser extension, +run as a child process under your browser. + +To register it once, run: + + $ ts-browser-native-ext --install=chrome +`) + return + } + + hostinfo.SetApp("ts-browser-native-ext") + + 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...) + } + } else { + log.Printf("syslog: %v", err) + } + + h.logf("Starting readMessages loop") + err := h.readMessages() + h.logf("readMessage loop ended: %v", err) +} + +func getTargetDir(browser string) (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + var dir string + switch runtime.GOOS { + case "darwin": + dir = filepath.Join(home, "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts") + default: + return "", fmt.Errorf("TODO: implement support for installing on %q", runtime.GOOS) + } + if err := os.MkdirAll(dir, 0755); err != nil { + return "", err + } + return dir, nil +} + +func install(browser string) error { + switch browser { + case "chrome": + case "firefox": + return errors.New("TODO: firefox") + default: + return fmt.Errorf("unknown browser %q", browser) + } + exe, err := os.Executable() + if err != nil { + return err + } + targetDir, err := getTargetDir(browser) + if err != nil { + return err + } + binary, err := os.ReadFile(exe) + if err != nil { + return err + } + targetBin := filepath.Join(targetDir, "ts-browser-native-ext") + targetJSON := filepath.Join(targetDir, "com.tailscale.browserext.chrome.json") + if err := os.WriteFile(targetBin, binary, 0755); err != nil { + return err + } + log.SetFlags(0) + log.Printf("copied binary to %v", targetBin) + jsonConf := fmt.Appendf(nil, `{ + "name": "com.tailscale.browserext.chrome", + "description": "Tailscale Native Extension", + "path": "%s", + "type": "stdio", + "allowed_origins": [ + "chrome-extension://mldijmhffomelkfhfjcjekhjgaikhood/" + ] + }`, targetBin) + if err := os.WriteFile(targetJSON, jsonConf, 0644); err != nil { + return err + } + log.Printf("wrote registration to %v", targetJSON) + return nil +} + +type host struct { + br *bufio.Reader + w io.Writer + logf logger.Logf + + wmu sync.Mutex // guards writing to w + + lenBuf [4]byte // owned by readMessages + + mu sync.Mutex + ts *tsnet.Server + ln net.Listener + wantUp bool + // ... +} + +func newHost(r io.Reader, w io.Writer) *host { + h := &host{ + br: bufio.NewReaderSize(r, 1<<20), + w: w, + logf: log.Printf, + } + h.ts = &tsnet.Server{ + RunWebClient: true, + + // late-binding, so caller can adjust h.logf. + Logf: func(f string, a ...any) { + h.logf(f, a...) + }, + } + return h +} + +const maxMsgSize = 1 << 20 + +func (h *host) readMessages() error { + for { + msg, err := h.readMessage() + if err != nil { + return err + } + if err := h.handleMessage(msg); err != nil { + h.logf("error handling message %v: %v", msg, err) + return err + } + } +} + +func (h *host) handleMessage(msg *request) error { + switch msg.Cmd { + case CmdInit: + return h.handleInit(msg) + case CmdGetStatus: + h.sendStatus() + case CmdUp: + return h.handleUp() + case CmdDown: + return h.handleDown() + default: + h.logf("unknown command %q", msg.Cmd) + } + return nil +} + +func (h *host) handleUp() error { + return h.setWantRunning(true) +} + +func (h *host) handleDown() error { + return h.setWantRunning(false) +} + +func (h *host) setWantRunning(want bool) error { + defer h.sendStatus() + h.mu.Lock() + defer h.mu.Unlock() + if h.ts.Sys() == nil { + return fmt.Errorf("not init") + } + h.wantUp = want + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + lc, err := h.ts.LocalClient() + if err != nil { + return err + } + if _, err := lc.EditPrefs(ctx, &ipn.MaskedPrefs{ + WantRunningSet: true, + Prefs: ipn.Prefs{ + WantRunning: want, + }, + }); err != nil { + return fmt.Errorf("EditPrefs to wantRunning=%v: %w", want, err) + } + return nil +} + +func (h *host) handleInit(msg *request) (ret error) { + defer func() { + var errMsg string + if ret != nil { + errMsg = ret.Error() + } + h.send(&reply{ + Init: &initResult{Error: errMsg}, + }) + }() + h.mu.Lock() + defer h.mu.Unlock() + + id := msg.InitID + if len(id) == 0 { + return fmt.Errorf("missing initID") + } + if len(id) > 60 { + return fmt.Errorf("initID too long") + } + for i := range len(id) { + b := id[i] + if b == '-' || (b >= 'a' && b <= 'f') || (b >= '0' && b <= '9') { + continue + } + return errors.New("invalid initID character") + } + + if h.ts.Sys() != nil { + return fmt.Errorf("already running") + } + u, err := user.Current() + if err != nil { + return fmt.Errorf("getting current user: %w", err) + } + h.ts.Hostname = u.Username + "-browser-ext" + + confDir, err := os.UserConfigDir() + if err != nil { + return fmt.Errorf("getting user config dir: %w", err) + } + h.ts.Dir = filepath.Join(confDir, "tailscale-browser-ext", id) + + h.logf("Starting...") + if err := h.ts.Start(); err != nil { + return fmt.Errorf("starting tsnet.Server: %w", err) + } + h.logf("Started") + + return nil +} + +func (h *host) send(msg *reply) error { + msgb, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("json encoding of message: %w", err) + } + h.logf("sent reply: %s", msgb) + if len(msgb) > maxMsgSize { + return fmt.Errorf("message too big (%v)", len(msgb)) + } + binary.LittleEndian.PutUint32(h.lenBuf[:], uint32(len(msgb))) + h.wmu.Lock() + defer h.wmu.Unlock() + if _, err := h.w.Write(h.lenBuf[:]); err != nil { + return err + } + if _, err := h.w.Write(msgb); err != nil { + return err + } + return nil +} + +func (h *host) getProxyListener() net.Listener { + h.mu.Lock() + defer h.mu.Unlock() + return h.getProxyListenerLocked() +} + +func (h *host) getProxyListenerLocked() net.Listener { + if h.ln != nil { + return h.ln + } + var err error + h.ln, err = net.Listen("tcp", "127.0.0.1:0") + if err != nil { + panic(err) // TODO: be more graceful + } + socksListener, httpListener := proxymux.SplitSOCKSAndHTTP(h.ln) + + hs := &http.Server{Handler: httpProxyHandler(h.userDial)} + go func() { + log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpListener)) + }() + ss := &socks5.Server{ + Logf: logger.WithPrefix(h.logf, "socks5: "), + Dialer: h.userDial, + } + go func() { + log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener)) + }() + return h.ln +} + +func (h *host) userDial(ctx context.Context, netw, addr string) (net.Conn, error) { + h.mu.Lock() + sys := h.ts.Sys() + h.mu.Unlock() + + if sys == nil { + h.logf("userDial to %v/%v without a tsnet.Server started", netw, addr) + return nil, fmt.Errorf("no tsnet.Server") + } + return sys.Dialer.Get().UserDial(ctx, netw, addr) +} + +func (h *host) sendStatus() { + h.mu.Lock() + wantUp := h.wantUp + ln := h.getProxyListenerLocked() + h.mu.Unlock() + + if err := h.send(&reply{ + Status: &status{ + Running: wantUp, + ProxyPort: ln.Addr().(*net.TCPAddr).Port, + ProxyURL: "http://" + ln.Addr().String(), + }, + }); err != nil { + h.logf("failed to send status: %v", err) + } +} + +type Cmd string + +const ( + CmdInit Cmd = "init" + CmdUp Cmd = "up" + CmdDown Cmd = "down" + CmdGetStatus Cmd = "get-status" +) + +// request is a message from the browser extension. +type request struct { + // Cmd is the request type. + Cmd Cmd `json:"cmd"` + + // InitID is the unique ID made by the extension (in its local storage) to + // distinguish between different browser profiles using the same extension. + // A given Go process will correspond to a single browser profile. + // This lets us store tsnet state in different directories. + // This string, coming from JavaScript, should not be trusted. It must be + // UUID-ish: hex and hyphens only, and too long. + InitID string `json:"initID,omitempty"` + + // ... +} + +// reply is a message to the browser extension. +type reply struct { + Status *status `json:"status,omitempty"` + Init *initResult `json:"init,omitempty"` +} + +type initResult struct { + Error string `json:"error"` // empty for none +} + +type status struct { + ProxyPort int `json:"proxyPort"` + ProxyURL string `json:"proxyURL"` + Running bool `json:"running"` +} + +func (h *host) readMessage() (*request, error) { + if _, err := io.ReadFull(h.br, h.lenBuf[:]); err != nil { + return nil, err + } + msgSize := binary.LittleEndian.Uint32(h.lenBuf[:]) + if msgSize > maxMsgSize { + return nil, fmt.Errorf("message size too big (%v)", msgSize) + } + msgb := make([]byte, msgSize) + if n, err := io.ReadFull(h.br, msgb); err != nil { + return nil, fmt.Errorf("read %v of %v bytes in message with error %v", n, msgSize, err) + } + msg := new(request) + if err := json.Unmarshal(msgb, msg); err != nil { + return nil, fmt.Errorf("invalid JSON decoding of message: %w", err) + } + h.logf("got command %q: %s", msg.Cmd, msgb) + return msg, nil +} + +// httpProxyHandler returns an HTTP proxy http.Handler using the +// provided backend dialer. +func httpProxyHandler(dialer func(ctx context.Context, netw, addr string) (net.Conn, error)) http.Handler { + rp := &httputil.ReverseProxy{ + Director: func(r *http.Request) {}, // no change + Transport: &http.Transport{ + DialContext: dialer, + }, + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "CONNECT" { + backURL := r.RequestURI + if strings.HasPrefix(backURL, "/") || backURL == "*" { + http.Error(w, "bogus RequestURI; must be absolute URL or CONNECT", 400) + return + } + rp.ServeHTTP(w, r) + return + } + + // CONNECT support: + + dst := r.RequestURI + c, err := dialer(r.Context(), "tcp", dst) + if err != nil { + w.Header().Set("Tailscale-Connect-Error", err.Error()) + http.Error(w, err.Error(), 500) + return + } + defer c.Close() + + cc, ccbuf, err := w.(http.Hijacker).Hijack() + if err != nil { + http.Error(w, err.Error(), 500) + return + } + defer cc.Close() + + io.WriteString(cc, "HTTP/1.1 200 OK\r\n\r\n") + + var clientSrc io.Reader = ccbuf + if ccbuf.Reader.Buffered() == 0 { + // In the common case (with no + // buffered data), read directly from + // the underlying client connection to + // save some memory, letting the + // bufio.Reader/Writer get GC'ed. + clientSrc = cc + } + + errc := make(chan error, 1) + go func() { + _, err := io.Copy(cc, c) + errc <- err + }() + go func() { + _, err := io.Copy(c, clientSrc) + errc <- err + }() + <-errc + }) +}