Repository: tailscale/ts-browser-ext Branch: main Commit: da4818787d51 Files: 15 Total size: 90.9 KB Directory structure: gitextract_z5qjsgc3/ ├── LICENSE ├── PATENTS ├── README.md ├── background.js ├── chrome.txt ├── firefox/ │ ├── background.js │ ├── manifest.json │ ├── popup.html │ └── popup.js ├── go.mod ├── go.sum ├── manifest.json ├── popup.html ├── popup.js └── ts-browser-ext.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2020 Tailscale Inc & AUTHORS. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: PATENTS ================================================ Additional IP Rights Grant (Patents) "This implementation" means the copyrightable works distributed by Tailscale Inc. as part of the Tailscale project. Tailscale Inc. hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, transfer and otherwise run, modify and propagate the contents of this implementation of Tailscale, where such license applies only to those patent claims, both currently owned or controlled by Tailscale Inc. and acquired in the future, licensable by Tailscale Inc. that are necessarily infringed by this implementation of Tailscale. This grant does not include claims that would be infringed only as a consequence of further modification of this implementation. If you or your agent or exclusive licensee institute or order or agree to the institution of patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that this implementation of Tailscale or any code incorporated within this implementation of Tailscale constitutes direct or contributory patent infringement, or inducement of patent infringement, then any patent rights granted to you under this License for this implementation of Tailscale shall terminate as of the date such litigation is filed. ================================================ FILE: README.md ================================================ # Tailscale Browser Extension (Experiment) [![status: experimental](https://img.shields.io/badge/status-experimental-blue)](https://tailscale.com/kb/1167/release-stages/#experimental) The [Tailscale](https://tailscale.com/) Browser Extension lets you access your tailnet resources using a browser extension, without necessarily installing Tailscale system-wide. In particular, ... * you can **simultaneously use a different tailnet per browser profile** * separate out your personal tailnet in its own browser profile * you don't need to be root/admin to install it * it doesn't interfere with your other OS VPN(s) and route tables and is purely scoped to one browser profile ## How it works Ideally it would work purely with WASM/WASI, but browser extensions don't have enough APIs, so it regrettably has to use Native Messaging ([Chrome](https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging), [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging)) where a native binary (using [`tsnet`](https://tailscale.com/kb/1244/tsnet)) runs as a child process under the browser and communicates with the browser extension with JSON messages back and forth. The child process then runs an HTTP/SOCKS5 proxy on `localhost:0` (with the kernel picking a random free port) and the browser extension uses the browser proxy API to send all web traffic through the child's proxy, which then sends it out over Tailscale, an exit node, or the Internet as normal. ## Status As of 2025-02-25, this is **barely just starting to work** and is not meant for end users yet. It's barely meant for developers at this point. | Browser | OS | Status | | -------- | ------- | ---- | | Chrome | macOS | Works | | Chrome | Linux | Works in theory, untested | | Chrome | Windows | Registry install work not yet done | | Firefox | macOS | Mostly works | | Firefox | Linux | Mostly works in theory, untested | | Firefox | Windows | Registry install work not yet done | | Safari | * | not possible; no support for Native Messaging | ## Developer instructions To log out, for now you need to remove & re-add the extension. ### Chrome 1. Open the Extensions page (`chrome://extensions`) or Extensions... > Manage Extensions... 2. Toggle "Developer mode" on. 3. Click "Load unpacked". 4. Navigate to the directory where you cloned this repo and select it. 5. Pin the extension to the toolbar. 6. Click the extension icon. 7. Follow the instructions in the popup to run the printed `go run ...` command, which builds and registers the native messaging backend. 8. Click the extension icon again and select "Log in". ### Firefox 1. Open the Debugging page (`about:debugging#/runtime/this-firefox`). 2. Click "Load Temporary Add-on...". 3. Navigate to the `firefox/` subdirectory of this repo and select its `manifest.json`. 4. Open the Add-ons Manager (`about:addons`), select the Tailscale extension, and under "Run in Private Windows" choose "Allow" if you want it to be active in private browsing. 5. Pin the extension to the toolbar. 6. Click the extension icon. 7. Follow the instructions in the popup to run the printed `go run ...` command, which builds and registers the native messaging backend. 8. Click the extension icon again and select "Log in". Temporary add-ons in Firefox are removed when the browser restarts, so you'll need to reload it from `about:debugging` each session. ## End user instructions Don't use it yet. It's too rough. See status above. ================================================ FILE: background.js ================================================ let proxyEnabled = false; // setPopupIcon sets the icon. It takes either a boolean (for online/offline) // or the base name of the png file. function setPopupIcon(base) { if (typeof base === "boolean") { base = base ? "online" : "offline"; } let iconPath = base + ".png"; console.log("set icon path to: " + iconPath); chrome.action.setIcon({ path: iconPath }, () => { if (chrome.runtime.lastError) { console.error( "Error setting icon to " + iconPath + ":", chrome.runtime.lastError.message ); } }); } function enableProxy() { if (deadPort) { console.error("Cannot enable proxy, disconnected from native host"); return; } if (lastProxyPort) { nmPort.postMessage({ cmd: "get-status" }); } else { nmPort.postMessage({ cmd: "up" }); } } function disableProxy() { console.log("disableProxy called"); if (nmPort && !deadPort) { console.log("Sending down command to native host"); nmPort.postMessage({ cmd: "down" }); } else { console.log( "Cannot send down command - nmPort:", !!nmPort, "deadPort:", deadPort ); } proxyEnabled = false; lastProxyPort = 0; console.log( "Proxy disabled, proxyEnabled:", proxyEnabled, "lastProxyPort:", lastProxyPort ); } console.log("starting ts-browser-ext"); let popupPort = null; chrome.runtime.onConnect.addListener((port) => { if (port.name != "popup") { return; } popupPort = port; console.log("Popup connected"); port.onMessage.addListener((msg) => { console.log("Message from popup:", msg); }); port.onDisconnect.addListener(() => { console.log("Popup disconnected"); popupPort = null; }); sendPopupStatus(); }); // browserByte returns either "F" for Firefox or "C" for chrome. // Other browsers return "?". function browserByte() { if (typeof chrome !== "undefined") { if (typeof browser !== "undefined") { return "F"; // Firefox supports both `chrome` and `browser` } return "C"; } return "?"; } function sendPopupStatus() { if (deadPort) { setPopupIcon("need-install"); console.log("sendPopupStatus... no nmPort"); sendToPopup({ installCmd: "go run github.com/tailscale/ts-browser-ext@main --install=" + browserByte() + chrome.runtime.id, }); return; } setPopupIcon(proxyEnabled ? "online" : "offline"); sendToPopup({ status: lastStatus }); } function sendToPopup(v) { if (popupPort) { popupPort.postMessage(v); } } let nmPort = null; // even non-null if lacking permission let deadPort = true; let portError = null; connectToNativeHost(); function connectToNativeHost() { if (nmPort && !deadPort) { return; } console.log("Connecting to native messaging host..."); nmPort = chrome.runtime.connectNative("com.tailscale.browserext.chrome"); nmPort.onDisconnect.addListener(() => { deadPort = true; setPopupIcon("need-install"); disableProxy(); const error = chrome.runtime.lastError; if (error) { console.error("Connection failed:", error.message); portError = error.message; setTimeout(connectToNativeHost, 1000); } else { console.error("Disconnected from native host"); } }); nmPort.onMessage.addListener((message) => { console.log("got message: " + JSON.stringify(message)); if (deadPort) { console.log("connected to native backend"); deadPort = false; } if (message.procRunning) { if (message.procRunning.port) { setProxy(message.procRunning.port); } else if (message.procRunning.errror) { console.log( "procRunning error from backend: " + message.procRunning.err ); disableProxy(); } } if (message.init && message.init.error) { console.log("init error from backend: " + message.init.err); disableProxy(); } if (message.status) { lastStatus = message.status; } maybeSendInit(); sendPopupStatus(); }); } var lastProxyPort = 0; var lastStatus = {}; // last Go status function setProxy(proxyPort) { if (proxyPort) { proxyEnabled = true; lastProxyPort = proxyPort; console.log("Enabling proxy at port: " + proxyPort); } else { proxyEnabled = false; 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: ["localhost", "127.*"], }, }, scope: "regular", }, () => { console.log("Proxy enabled: 127.0.0.1:" + proxyPort); } ); } var profileID = ""; var didInit = false; function maybeSendInit() { if (!profileID || didInit || deadPort) { return; } nmPort.postMessage({ cmd: "init", initID: profileID }); didInit = true; } chrome.storage.local.get("profileId", (result) => { if (!result.profileId) { const profileId = crypto.randomUUID(); chrome.storage.local.set({ profileId }, () => { console.log("Generated profile ID:", profileId); profileID = profileId; maybeSendInit(); }); } else { console.log("Profile ID already exists:", result.profileId); profileID = result.profileId; maybeSendInit(); } }); // Listener for messages from the popup chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { console.log("bg: Received message:", message); if (message.command === "toggleProxy") { console.log("bg: toggleProxy received, current proxy=" + proxyEnabled); proxyEnabled = !proxyEnabled; if (proxyEnabled) { console.log("bg: Enabling proxy"); enableProxy(); console.log("bg: toggleProxy on, now proxy=" + proxyEnabled); sendResponse({ status: lastStatus }); console.log("bg: toggleProxy on, sent status response"); } else { console.log("bg: Disabling proxy"); disableProxy(); console.log("bg: toggleProxy off, now proxy=" + proxyEnabled); sendResponse({ status: "Disconnected" }); console.log("bg: toggleProxy off, sent disconnected response"); } setPopupIcon(proxyEnabled); return true; // Keep the message channel open for the async response } }); ================================================ FILE: chrome.txt ================================================ % 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/" ] } ================================================ FILE: firefox/background.js ================================================ let proxyEnabled = false; // setPopupIcon sets the icon. It takes either a boolean (for online/offline) // or the base name of the png file. function setPopupIcon(base) { if (typeof base === "boolean") { base = base ? "online" : "offline"; } let iconPath = base + ".png"; console.log("set icon path to: " + iconPath); browser.action.setIcon({ path: iconPath }).catch((error) => { console.error("Error setting icon to " + iconPath + ":", error.message); }); } function enableProxy() { if (deadPort) { console.error("Cannot enable proxy, disconnected from native host"); return; } if (lastProxyPort) { nmPort.postMessage({ cmd: "get-status" }); } else { nmPort.postMessage({ cmd: "up" }); } } function disableProxy() { console.log("disableProxy called"); if (nmPort && !deadPort) { console.log("Sending down command to native host"); nmPort.postMessage({ cmd: "down" }); } else { console.log( "Cannot send down command - nmPort:", !!nmPort, "deadPort:", deadPort ); } proxyEnabled = false; lastProxyPort = 0; console.log( "Proxy disabled, proxyEnabled:", proxyEnabled, "lastProxyPort:", lastProxyPort ); } console.log("starting ts-browser-ext"); let popupPort = null; browser.runtime.onConnect.addListener((port) => { if (port.name != "popup") { return; } popupPort = port; console.log("Popup connected"); port.onMessage.addListener((msg) => { console.log("Message from popup:", msg); }); port.onDisconnect.addListener(() => { console.log("Popup disconnected"); popupPort = null; }); sendPopupStatus(); }); // browserByte returns either "F" for Firefox or "C" for chrome. // Other browsers return "?". function browserByte() { if (typeof browser !== "undefined") { return "F"; } return "?"; } function sendPopupStatus() { // firefox requires that extensions settings proxies have private browsing access browser.extension.isAllowedIncognitoAccess().then(isAllowed => { if (!isAllowed) { sendToPopup({ needsIncognitoPermission: true }); } }); if (deadPort) { setPopupIcon("need-install"); console.log("sendPopupStatus... no nmPort"); sendToPopup({ installCmd: "go run github.com/tailscale/ts-browser-ext@main --install=" + browserByte() + browser.runtime.id, }); return; } setPopupIcon(proxyEnabled ? "online" : "offline"); sendToPopup({ status: lastStatus }); } function sendToPopup(v) { if (popupPort) { popupPort.postMessage(v); } } let nmPort = null; // even non-null if lacking permission let deadPort = true; let portError = null; connectToNativeHost(); function connectToNativeHost() { if (nmPort && !deadPort) { return; } console.log("Connecting to native messaging host..."); nmPort = browser.runtime.connectNative("com.tailscale.browserext.firefox"); nmPort.onDisconnect.addListener(() => { deadPort = true; setPopupIcon("need-install"); disableProxy(); const error = browser.runtime.lastError; if (error) { console.error("Connection failed:", error.message); portError = error.message; setTimeout(connectToNativeHost, 1000); } else { console.error("Disconnected from native host"); } }); nmPort.onMessage.addListener((message) => { console.log("got message: " + JSON.stringify(message)); if (deadPort) { console.log("connected to native backend"); deadPort = false; } if (message.procRunning) { if (message.procRunning.port) { setProxy(message.procRunning.port); } else if (message.procRunning.errror) { console.log( "procRunning error from backend: " + message.procRunning.err ); disableProxy(); } } if (message.init && message.init.error) { console.log("init error from backend: " + message.init.err); disableProxy(); } if (message.status) { lastStatus = message.status; } maybeSendInit(); sendPopupStatus(); }); } var lastProxyPort = 0; var lastStatus = {}; // last Go status function setProxy(proxyPort) { const handleProxyRequest = proxyHandler(proxyPort) if (proxyPort) { proxyEnabled = true; lastProxyPort = proxyPort; console.log("Enabling proxy at port: " + proxyPort); } else { proxyEnabled = false; console.log("Disabling proxy..."); browser.proxy.onRequest.removeListener(handleProxyRequest) browser.proxy.settings .set({ value: { mode: "direct", }, scope: "regular", }) .then(() => { console.log("Proxy disabled."); }); return; } browser.proxy.onRequest.addListener(handleProxyRequest, { urls: [""] }) } var profileID = ""; var didInit = false; // firefox has unique behaviour where only socks proxies can handle domain resolution function proxyHandler(port) { return function handleProxyRequest(requestInfo) { const url = new URL(requestInfo.url) // we need to use http for 100.100.100.100 if (url.hostname == '100.100.100.100') { return { type: "http", host: "127.0.0.1", port: port }; } // use socks for everything else return { type: "socks", host: "127.0.0.1", port: port, proxyDNS: true, bypassList: ["localhost", "127.*"] }; } } function maybeSendInit() { if (!profileID || didInit || deadPort) { return; } nmPort.postMessage({ cmd: "init", initID: profileID }); didInit = true; } browser.storage.local.get("profileId").then((result) => { if (!result.profileId) { const profileId = crypto.randomUUID(); browser.storage.local.set({ profileId }).then(() => { console.log("Generated profile ID:", profileId); profileID = profileId; maybeSendInit(); }); } else { console.log("Profile ID already exists:", result.profileId); profileID = result.profileId; maybeSendInit(); } }); // Listener for messages from the popup browser.runtime.onMessage.addListener((message, sender) => { console.log("bg: Received message:", message); if (message.command === "toggleProxy") { console.log("bg: toggleProxy received, current proxy=" + proxyEnabled); proxyEnabled = !proxyEnabled; if (proxyEnabled) { console.log("bg: Enabling proxy"); enableProxy(); } else { console.log("bg: Disabling proxy"); disableProxy(); } return Promise.resolve({ status: lastStatus }); } }); ================================================ FILE: firefox/manifest.json ================================================ { "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.", "browser_specific_settings": { "gecko": { "id": "browser-ext@tailscale.com", "strict_min_version": "50.0" } }, "permissions": [ "proxy", "storage", "nativeMessaging" ], "host_permissions": [ "" ], "background": { "scripts": ["background.js"] }, "action": { "default_popup": "popup.html", "default_icon": "icon.png" } } ================================================ FILE: firefox/popup.html ================================================ Proxy Toggle
Disconnected
================================================ FILE: firefox/popup.js ================================================ var lastStatus; document.addEventListener("DOMContentLoaded", () => { const toggleSlider = document.getElementById("toggleSlider"); const slider = document.querySelector(".slider"); const settingsButton = document.getElementById("settingsButton"); const stateDisplay = document.getElementById("state"); let isConnected = false; let isLoading = true; let hasReceivedInitialState = false; const port = browser.runtime.connect({ name: "popup" }); function updateSliderState() { if (isLoading) { slider.className = "slider loading"; toggleSlider.checked = true; // Assume connected while loading return; } // Only remove no-transition after we've received and applied the initial state if (hasReceivedInitialState) { slider.classList.remove("no-transition"); } slider.className = `slider ${isConnected ? "connected" : ""}`; toggleSlider.checked = isConnected; } function updateStatus(status) { isLoading = false; hasReceivedInitialState = true; if (status.error) { if (status.error === "State: Stopped") { stateDisplay.textContent = "Disconnected"; isConnected = false; updateSliderState(); return; } stateDisplay.textContent = `Error: ${status.error}`; return; } if (status.needsLogin) { stateDisplay.innerHTML = status.browseToURL ? `Log in` : "Login required; no URL"; return; } if (typeof status === "string" && status === "Disconnected") { stateDisplay.textContent = "Disconnected"; isConnected = false; updateSliderState(); return; } if (status.running !== undefined) { stateDisplay.textContent = status.running ? `Connected as ${status.tailnet || "Not connected"}` : "Disconnected"; isConnected = status.running; updateSliderState(); } } port.onMessage.addListener((msg) => { console.log("Received from background:", JSON.stringify(msg)); // firefox requires that extensions settings proxies have private browsing access if (msg.needsIncognitoPermission) { console.log("Private browsing permission needed") stateDisplay.innerHTML = `Enable private browsing access.` return; } if (msg.installCmd) { console.log("Received install command"); stateDisplay.innerHTML = `Installation needed. Run:
${msg.installCmd}
`; toggleSlider.disabled = true; settingsButton.hidden = true; return; } if (msg.error) { console.log("Error from background:", msg); stateDisplay.textContent = msg.error; toggleSlider.disabled = true; settingsButton.hidden = true; return; } if (msg.status) { console.log("Received status update:", msg.status); updateStatus(msg.status); } }); toggleSlider.addEventListener("change", () => { console.log("Toggle slider changed, current state:", isConnected); browser.runtime.sendMessage({ command: "toggleProxy" }).then((response) => { console.log("Received response from background:", response); if (response && response.status) { updateStatus(response.status); } }); console.log("Sent toggleProxy command to background"); }); settingsButton.addEventListener("click", () => { console.log("Settings button clicked"); browser.tabs.create({ url: "http://100.100.100.100" }); }); window.addEventListener("beforeunload", () => { port.disconnect(); }); }); ================================================ FILE: go.mod ================================================ module github.com/tailscale/ts-browser-ext go 1.26.3 require ( github.com/gorilla/csrf v1.7.3 tailscale.com v1.98.2 ) require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/akutz/memconn v0.1.0 // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect github.com/coder/websocket v1.8.12 // indirect github.com/creachadair/msync v0.7.1 // indirect github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gaissmai/bart v0.26.1 // indirect github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/jsimonetti/rtnetlink v1.4.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect github.com/mdlayher/socket v0.5.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/pires/go-proxyproto v0.8.1 // indirect github.com/safchain/ethtool v0.3.0 // indirect github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd // indirect github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect github.com/tailscale/wireguard-go v0.0.0-20260427181203-e3ac4a0afb4e // indirect github.com/x448/float16 v0.8.4 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/term v0.42.0 // indirect golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.12.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect ) ================================================ FILE: go.sum ================================================ 9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q= 9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k= github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg= github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y= github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE= github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM= github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok= github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U= github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02 h1:bXAPYSbdYbS5VTy92NIUbeDI1qyggi+JYh5op9IFlcQ= github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c= github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/creachadair/mds v0.25.9 h1:080Hr8laN2h+l3NeVCGMBpXtIPnl9mz8e4HLraGPqtA= github.com/creachadair/mds v0.25.9/go.mod h1:4hatI3hRM+qhzuAmqPRFvaBM8mONkS7nsLxkcuTYUIs= github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7uho= github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008= github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc= github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8= github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo= github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I= github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo= github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I= github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/csrf v1.7.3 h1:BHWt6FTLZAb2HtWT5KDBf6qgpZzvtbp9QWDRKZMXJC0= github.com/gorilla/csrf v1.7.3/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk= github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g= github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg= github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o= github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c= github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE= github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc= github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89/go.mod h1:wn16Km1EZOX4UEAyaZa3dBwfFGOJ7neck40NcwosJUw= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM= github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA= github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo= github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14= github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= github.com/tailscale/wireguard-go v0.0.0-20260427181203-e3ac4a0afb4e h1:GexFR7ak1iz26fxg8HWCpOEqAOL8UEZJ7J3JxeCalDs= github.com/tailscale/wireguard-go v0.0.0-20260427181203-e3ac4a0afb4e/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg= github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 h1:Zy8IV/+FMLxy6j6p87vk/vQGKcdnbprwjTxc8UiUtsA= gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= tailscale.com v1.98.2 h1:HP5gt0qyLKtJoDV7PMUvPpiXjMFk4nzXMbm7JdjttMY= tailscale.com v1.98.2/go.mod h1:U23ZwbZlKJMNU7CScy+lCVVlece/S5n09q0nyudncBI= ================================================ FILE: manifest.json ================================================ { "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" } } ================================================ FILE: popup.html ================================================ Proxy Toggle
================================================ FILE: popup.js ================================================ 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"); const settingsButton = document.getElementById("settingsButton"); const stateDisplay = document.getElementById("state"); let isConnected = false; let isLoading = true; let hasReceivedInitialState = false; const port = chrome.runtime.connect({ name: "popup" }); function updateSliderState() { if (isLoading) { slider.className = "slider loading"; toggleSlider.checked = true; // Assume connected while loading return; } // Only remove no-transition after we've received and applied the initial state if (hasReceivedInitialState) { slider.classList.remove("no-transition"); } slider.className = `slider ${isConnected ? "connected" : ""}`; toggleSlider.checked = isConnected; } function updateStatus(status) { isLoading = false; hasReceivedInitialState = true; if (status.error) { if (status.error === "State: Stopped") { stateDisplay.textContent = "Disconnected"; isConnected = false; updateSliderState(); return; } stateDisplay.textContent = `Error: ${status.error}`; return; } if (status.needsLogin) { stateDisplay.innerHTML = status.browseToURL ? `Log in` : "Login required; no URL"; return; } if (typeof status === "string" && status === "Disconnected") { stateDisplay.textContent = "Disconnected"; isConnected = false; updateSliderState(); return; } if (status.running !== undefined) { stateDisplay.textContent = status.running ? `Connected as ${status.tailnet || "Not connected"}` : "Disconnected"; isConnected = status.running; updateSliderState(); } } port.onMessage.addListener((msg) => { console.log("Received from background:", JSON.stringify(msg)); if (msg.installCmd) { console.log("Received install command"); stateDisplay.innerHTML = `Installation needed. Run:
${msg.installCmd}
`; toggleSlider.disabled = true; settingsButton.hidden = true; return; } if (msg.error) { console.log("Error from background:", msg); stateDisplay.textContent = msg.error; toggleSlider.disabled = true; settingsButton.hidden = true; return; } if (msg.status) { console.log("Received status update:", msg.status); updateStatus(msg.status); } }); toggleSlider.addEventListener("change", () => { console.log("Toggle slider changed, current state:", isConnected); chrome.runtime.sendMessage({ command: "toggleProxy" }, (response) => { console.log("Received response from background:", response); if (response && response.status) { updateStatus(response.status); } }); console.log("Sent toggleProxy command to background"); }); settingsButton.addEventListener("click", () => { console.log("Settings button clicked"); chrome.tabs.create({ url: "http://100.100.100.100" }); }); window.addEventListener("beforeunload", () => { port.disconnect(); }); }); ================================================ FILE: ts-browser-ext.go ================================================ 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" "regexp" "runtime" "strings" "sync" "time" "github.com/gorilla/csrf" "tailscale.com/client/tailscale" "tailscale.com/client/web" "tailscale.com/hostinfo" "tailscale.com/ipn" "tailscale.com/net/proxymux" "tailscale.com/net/socks5" "tailscale.com/tsnet" "tailscale.com/types/logger" "tailscale.com/types/netmap" ) var ( installFlag = flag.String("install", "", "register the browser extension; string is 'C' (Chrome) or 'F' (Firefox) followed by extension ID") uninstallFlag = flag.Bool("uninstall", false, "unregister the browser extension") ) func main() { flag.Parse() if *installFlag != "" { if err := install(*installFlag); err != nil { log.Fatalf("installation error: %v", err) } return } if *uninstallFlag { if err := uninstall(); err != nil { log.Fatalf("uninstallation error: %v", err) } return } if flag.NArg() == 0 { fmt.Printf(`ts-browser-ext is the backend for the Tailscale browser extension, running as a child process HTTP/SOCKS5 under your browser. To register it once, run: $ ts-browser-ext --install=chrome `) return } hostinfo.SetApp("ts-browser-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...) } log.SetOutput(w) } else { log.Printf("syslog: %v", err) } ln := h.getProxyListener() port := ln.Addr().(*net.TCPAddr).Port h.logf("Proxy listening on localhost:%v", port) h.send(&reply{ProcRunning: &procRunningResult{ Port: port, Pid: os.Getpid(), }}) h.logf("Starting readMessages loop") err := h.readMessages() h.logf("readMessage loop ended: %v", err) } func getTargetDir(browserByte string) (string, error) { home, err := os.UserHomeDir() if err != nil { return "", err } var dir string switch runtime.GOOS { case "linux": if browserByte == "C" { dir = filepath.Join(home, ".config", "google-chrome", "NativeMessagingHosts") } else if browserByte == "F" { dir = filepath.Join(home, ".mozilla", "native-messaging-hosts") } case "darwin": if browserByte == "C" { dir = filepath.Join(home, "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts") } else if browserByte == "F" { dir = filepath.Join(home, "Library", "Application Support", "Mozilla", "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 uninstall() error { for _, browserByte := range []string{"C", "F"} { targetDir, err := getTargetDir(browserByte) if err != nil { return err } targetBin := filepath.Join(targetDir, "ts-browser-ext") targetJSON := filepath.Join(targetDir, "com.tailscale.browserext.chrome.json") if browserByte == "F" { targetJSON = filepath.Join(targetDir, "com.tailscale.browserext.firefox.json") } if err := os.Remove(targetBin); err != nil && !os.IsNotExist(err) { return err } if err := os.Remove(targetJSON); err != nil && !os.IsNotExist(err) { return err } } return nil } func install(installArg string) error { browserByte, extension := installArg[0:1], installArg[1:] switch browserByte { case "C": extensionRE := regexp.MustCompile(`^[a-z0-9]{32}$`) if !extensionRE.MatchString(extension) { return fmt.Errorf("invalid extension ID %q", extension) } case "F": default: return fmt.Errorf("unknown browser prefix byte %q", browserByte) } exe, err := os.Executable() if err != nil { return err } targetDir, err := getTargetDir(browserByte) if err != nil { return err } binary, err := os.ReadFile(exe) if err != nil { return err } targetBin := filepath.Join(targetDir, "ts-browser-ext") if err := os.WriteFile(targetBin, binary, 0755); err != nil { return err } log.SetFlags(0) log.Printf("copied binary to %v", targetBin) var targetJSON string var jsonConf []byte switch browserByte { case "C": targetJSON = filepath.Join(targetDir, "com.tailscale.browserext.chrome.json") jsonConf = fmt.Appendf(nil, `{ "name": "com.tailscale.browserext.chrome", "description": "Tailscale Browser Extension", "path": "%s", "type": "stdio", "allowed_origins": [ "chrome-extension://%s/" ] }`, targetBin, extension) case "F": targetJSON = filepath.Join(targetDir, "com.tailscale.browserext.firefox.json") jsonConf = fmt.Appendf(nil, `{ "name": "com.tailscale.browserext.firefox", "description": "Tailscale Browser Extension", "path": "%s", "type": "stdio", "allowed_extensions": [ "browser-ext@tailscale.com" ] }`, targetBin) default: return fmt.Errorf("unknown browser prefix byte %q", browserByte) } 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 watchDead bool lastNetmap *netmap.NetworkMap lastState ipn.State lastBrowseToURL string ctx context.Context // for IPN bus; canceled by cancelCtx cancelCtx context.CancelFunc ts *tsnet.Server ws *web.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() if h.cancelCtx != nil { h.cancelCtx() } h.ctx, h.cancelCtx = context.WithCancel(context.Background()) 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") lc, err := h.ts.LocalClient() if err != nil { return fmt.Errorf("getting local client: %w", err) } wc, err := lc.WatchIPNBus(h.ctx, ipn.NotifyInitialState|ipn.NotifyRateLimit) if err != nil { return fmt.Errorf("watching IPN bus: %w", err) } go h.watchIPNBus(wc) h.ws, err = web.NewServer(web.ServerOpts{ Mode: web.LoginServerMode, // TODO: manage? LocalClient: lc, }) if err != nil { return fmt.Errorf("NewServer: %w", err) } return nil } func (h *host) watchIPNBus(wc *tailscale.IPNBusWatcher) { h.mu.Lock() h.watchDead = false h.mu.Unlock() for h.updateFromWatcher(wc) { // Keep going. } } func (h *host) updateFromWatcher(wc *tailscale.IPNBusWatcher) bool { n, err := wc.Next() defer h.sendStatus() h.mu.Lock() defer h.mu.Unlock() if err != nil { log.Printf("watchIPNBus: %v", err) h.watchDead = true return false } if n.NetMap != nil { h.lastNetmap = n.NetMap } if n.State != nil { h.lastState = *n.State } if n.BrowseToURL != nil { h.lastBrowseToURL = *n.BrowseToURL // TODO: pop a browser for Tailscale SSH check mode etc, even // if already logged in. } return true } 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: h.httpProxyHandler()} 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() { st := &status{} h.mu.Lock() st.Running = h.lastState == ipn.Running if nm := h.lastNetmap; nm != nil { st.Tailnet = nm.Domain } if h.lastState == ipn.NeedsLogin { st.NeedsLogin = true st.BrowseToURL = h.lastBrowseToURL } else if !st.Running { st.Error = "State: " + h.lastState.String() } if h.watchDead { st.Error = "WatchIPNBus stopped" } h.mu.Unlock() if err := h.send(&reply{Status: st}); 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 { // ProcRunning is set on the first message when the Go process starts up. // It's the message that makes the browser recognize that the native // messaging port is up. ProcRunning *procRunningResult `json:"procRunning,omitempty"` // Status is sent in response to a [CmdGetStatus] [request.Cmd]. Status *status `json:"status,omitempty"` Init *initResult `json:"init,omitempty"` } type procRunningResult struct { Port int `json:"port"` // HTTP+SOCKS5 localhost proxy port Pid int `json:"pid"` Error string `json:"error"` } type initResult struct { Error string `json:"error"` // empty for none } type status struct { Running bool `json:"running"` Tailnet string `json:"tailnet"` Error string `json:"error,omitempty"` NeedsLogin bool `json:"needsLogin,omitempty"` // true if the user needs to log in BrowseToURL string `json:"browseToURL"` } 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 (h *host) httpProxyHandler() http.Handler { rp := &httputil.ReverseProxy{ Director: func(r *http.Request) {}, // no change Transport: &http.Transport{ DialContext: h.userDial, }, } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Host == "100.100.100.100" { h.ws.ServeHTTP(w, csrf.PlaintextHTTPRequest(r)) return } 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 := h.userDial(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 }) }