Full Code of tailscale/ts-browser-ext for AI

main da4818787d51 cached
15 files
90.9 KB
33.7k tokens
56 symbols
1 requests
Download .txt
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: ["<all_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": [
        "<all_urls>"
    ],
    "background": {
        "scripts": ["background.js"]
    },
    "action": {
        "default_popup": "popup.html",
        "default_icon": "icon.png"
    }
}


================================================
FILE: firefox/popup.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Proxy Toggle</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap"
      rel="stylesheet"
    />
    <script src="popup.js"></script>
    <style>
      body {
        background-color: #faf7f6;
        color: #575655;
        font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
          Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
        margin: 0;
        padding: 16px;
        width: 300px;
      }

      .header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 16px;
      }

      .logo {
        width: 120px;
      }

      /* Slider styles */
      .slider-container {
        position: relative;
        width: 44px;
        height: 24px;
      }

      .slider {
        position: absolute;
        cursor: pointer;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: #ccc;
        transition: 0.4s;
        border-radius: 24px;
      }

      .slider.no-transition {
        transition: none;
      }

      .slider.loading {
        filter: grayscale(100%);
        opacity: 0.7;
        cursor: wait;
      }

      .slider:before {
        position: absolute;
        content: "";
        height: 20px;
        width: 20px;
        left: 2px;
        bottom: 2px;
        background-color: #fcfcfc;
        transition: 0.4s;
        border-radius: 50%;
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
      }

      .slider.connected {
        background-color: #4c78c6;
      }

      .slider.connected:before {
        transform: translateX(20px);
      }

      #state {
        white-space: nowrap;
        margin-bottom: 16px;
        font-size: 14px;
      }

      button {
        background-color: #2e2d2d;
        color: #fff;
        border: none;
        padding: 8px 16px;
        border-radius: 6px;
        font-size: 14px;
        cursor: pointer;
        transition: background-color 0.2s;
      }

      button:hover {
        background-color: #1f1e1e;
      }

      .settings-button {
        padding: 6px 12px;
        font-size: 12px;
      }

      .controls {
        display: flex;
        gap: 8px;
        margin-top: 16px;
      }

      .status {
        padding: 8px;
        border-radius: 4px;
        background-color: #f5f5f5;
        margin-bottom: 16px;
      }
    </style>
  </head>
  <body>
    <div class="header">
      <div class="logo">
        <svg
          class="transition-colors duration-200"
          width="100%"
          height="100%"
          viewBox="0 0 110 20"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <ellipse
            cx="2.44719"
            cy="10.1796"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            cx="9.79094"
            cy="10.1796"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            opacity="0.2"
            cx="2.44719"
            cy="17.5077"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            opacity="0.2"
            cx="17.1269"
            cy="17.5077"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            cx="9.79094"
            cy="17.5077"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            cx="17.1269"
            cy="10.1796"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            opacity="0.2"
            cx="2.44719"
            cy="2.85924"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            opacity="0.2"
            cx="9.79094"
            cy="2.85924"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            opacity="0.2"
            cx="17.1269"
            cy="2.85924"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <path
            d="M34.3979 18.458C35.0907 18.458 35.6536 18.3933 36.3248 18.2637V15.7584C35.9134 15.9096 35.4588 15.9528 35.0258 15.9528C33.965 15.9528 33.5753 15.4344 33.5753 14.441V9.34402H36.3248V6.83875H33.5753V3.12403H30.5443V6.83875H28.5742V9.34402H30.5443V14.7217C30.5443 17.0974 31.8 18.458 34.3979 18.458Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M41.2747 18.458C42.8984 18.458 43.9809 17.9181 44.5222 17.0758C44.5655 17.443 44.6954 17.9397 44.8686 18.2421H47.5964C47.4449 17.7237 47.3366 16.903 47.3366 16.3631V10.4455C47.3366 8.005 45.583 6.62277 42.617 6.62277C40.3654 6.62277 38.6118 7.46507 37.6376 8.69611L39.3696 10.4023C40.149 9.5384 41.1448 9.08486 42.3572 9.08486C43.8294 9.08486 44.4789 9.58159 44.4789 10.3159C44.4789 10.9422 44.0459 11.3742 41.7077 11.3742C39.4562 11.3742 37.183 12.3028 37.183 14.8945C37.183 17.2918 38.9149 18.458 41.2747 18.458ZM41.8809 16.1687C40.7118 16.1687 40.1706 15.672 40.1706 14.7865C40.1706 14.009 40.8201 13.4907 41.9026 13.4907C43.6345 13.4907 44.1108 13.3827 44.4789 13.0155V13.9442C44.4789 15.1753 43.4397 16.1687 41.8809 16.1687Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M49.3069 5.39173H52.4677V2.5625H49.3069V5.39173ZM49.3718 18.2421H52.4028V6.83875H49.3718V18.2421Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M54.6109 18.2421H57.6418V2.90805H54.6109V18.2421Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M63.9416 18.458C67.2757 18.458 68.986 16.7087 68.986 14.8729C68.986 13.2099 68.1417 11.9789 65.3705 11.4821C63.4221 11.1366 62.2097 10.7046 62.2097 10.0351C62.2097 9.45201 62.9025 9.04166 64.0715 9.04166C65.1107 9.04166 65.9767 9.38722 66.6262 10.1431L68.553 8.52333C67.5788 7.31389 65.9767 6.62277 64.0715 6.62277C61.1489 6.62277 59.3303 8.17777 59.3303 10.0783C59.3303 12.1517 61.2354 13.0803 63.2922 13.4475C65.0025 13.7499 65.9551 14.0738 65.9551 14.8081C65.9551 15.4344 65.2839 15.9528 64.0066 15.9528C62.7509 15.9528 61.7767 15.3696 61.322 14.5058L58.7674 15.7152C59.3952 17.2702 61.5385 18.458 63.9416 18.458Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M75.7621 18.458C77.9271 18.458 79.4859 17.5942 80.6549 15.6504L78.2302 14.4194C77.7755 15.3265 77.0395 15.9528 75.7621 15.9528C73.8353 15.9528 72.7961 14.3978 72.7961 12.5188C72.7961 10.6399 73.9003 9.12805 75.7621 9.12805C76.9312 9.12805 77.7106 9.75437 78.1652 10.7046L80.6116 9.40882C79.7889 7.61625 78.1652 6.62277 75.7621 6.62277C71.8003 6.62277 69.7652 9.5168 69.7652 12.5188C69.7652 15.78 72.2333 18.458 75.7621 18.458Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M85.4829 18.458C87.1067 18.458 88.1891 17.9181 88.7304 17.0758C88.7737 17.443 88.9036 17.9397 89.0768 18.2421H91.8046C91.6531 17.7237 91.5448 16.903 91.5448 16.3631V10.4455C91.5448 8.005 89.7912 6.62277 86.8252 6.62277C84.5737 6.62277 82.8201 7.46507 81.8458 8.69611L83.5778 10.4023C84.3572 9.5384 85.353 9.08486 86.5654 9.08486C88.0376 9.08486 88.6871 9.58159 88.6871 10.3159C88.6871 10.9422 88.2541 11.3742 85.9159 11.3742C83.6644 11.3742 81.3912 12.3028 81.3912 14.8945C81.3912 17.2918 83.1231 18.458 85.4829 18.458ZM86.0891 16.1687C84.9201 16.1687 84.3788 15.672 84.3788 14.7865C84.3788 14.009 85.0283 13.4907 86.1108 13.4907C87.8427 13.4907 88.319 13.3827 88.6871 13.0155V13.9442C88.6871 15.1753 87.6479 16.1687 86.0891 16.1687Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M93.3263 18.2421H96.3573V2.90805H93.3263V18.2421Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M103.631 18.458C105.861 18.458 107.658 17.5726 108.654 15.996L106.359 14.5274C105.753 15.4776 104.952 15.996 103.631 15.996C102.138 15.996 101.055 15.1753 100.774 13.5771H109.39V12.5188C109.39 9.5168 107.55 6.62277 103.61 6.62277C99.8643 6.62277 97.8293 9.5384 97.8293 12.5404C97.8293 16.8167 101.055 18.458 103.631 18.458ZM100.882 11.2014C101.358 9.75437 102.354 9.08486 103.675 9.08486C105.168 9.08486 106.078 9.97034 106.381 11.2014H100.882Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
        </svg>
      </div>
      <div class="slider-container">
        <input type="checkbox" id="toggleSlider" class="slider" checked />
      </div>
    </div>
    <div id="state">Disconnected</div>
    <div class="controls">
      <button id="settingsButton" class="settings-button">Settings</button>
    </div>
  </body>
</html>


================================================
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
        ? `<b><a href='${status.browseToURL}'>Log in</a></b>`
        : "<b>Login required; no URL</b>";
      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 = `<b><a href="https://support.mozilla.org/en-US/kb/extensions-private-browsing#w_enabling-or-disabling-extensions-in-private-windows">Enable private browsing access.</a></b>`
      return;
    }

    if (msg.installCmd) {
      console.log("Received install command");
      stateDisplay.innerHTML = `<b>Installation needed. Run:</b><pre>${msg.installCmd}</pre>`;
      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": [
        "<all_urls>"
    ],
    "background": {
        "service_worker": "background.js"
    },
    "action": {
        "default_popup": "popup.html",
        "default_icon": "icon.png"
    }
}


================================================
FILE: popup.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Proxy Toggle</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap"
      rel="stylesheet"
    />
    <script src="popup.js"></script>
    <style>
      body {
        background-color: #faf7f6;
        color: #575655;
        font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
          Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
        margin: 0;
        padding: 16px;
        width: 300px;
      }

      .header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 16px;
      }

      .logo {
        width: 120px;
      }

      /* Slider styles */
      .slider-container {
        position: relative;
        width: 44px;
        height: 24px;
      }

      .slider {
        position: absolute;
        cursor: pointer;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: #ccc;
        transition: 0.4s;
        border-radius: 24px;
      }

      .slider.no-transition {
        transition: none;
      }

      .slider.loading {
        filter: grayscale(100%);
        opacity: 0.7;
        cursor: wait;
      }

      .slider:before {
        position: absolute;
        content: "";
        height: 20px;
        width: 20px;
        left: 2px;
        bottom: 2px;
        background-color: #fcfcfc;
        transition: 0.4s;
        border-radius: 50%;
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
      }

      .slider.connected {
        background-color: #4c78c6;
      }

      .slider.connected:before {
        transform: translateX(20px);
      }

      #state {
        white-space: nowrap;
        margin-bottom: 16px;
        font-size: 14px;
      }

      button {
        background-color: #2e2d2d;
        color: #fff;
        border: none;
        padding: 8px 16px;
        border-radius: 6px;
        font-size: 14px;
        cursor: pointer;
        transition: background-color 0.2s;
      }

      button:hover {
        background-color: #1f1e1e;
      }

      .settings-button {
        padding: 6px 12px;
        font-size: 12px;
      }

      .controls {
        display: flex;
        gap: 8px;
        margin-top: 16px;
      }

      .status {
        padding: 8px;
        border-radius: 4px;
        background-color: #f5f5f5;
        margin-bottom: 16px;
      }
    </style>
  </head>
  <body>
    <div class="header">
      <div class="logo">
        <svg
          class="transition-colors duration-200"
          width="100%"
          height="100%"
          viewBox="0 0 110 20"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <ellipse
            cx="2.44719"
            cy="10.1796"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            cx="9.79094"
            cy="10.1796"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            opacity="0.2"
            cx="2.44719"
            cy="17.5077"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            opacity="0.2"
            cx="17.1269"
            cy="17.5077"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            cx="9.79094"
            cy="17.5077"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            cx="17.1269"
            cy="10.1796"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            opacity="0.2"
            cx="2.44719"
            cy="2.85924"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            opacity="0.2"
            cx="9.79094"
            cy="2.85924"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <ellipse
            opacity="0.2"
            cx="17.1269"
            cy="2.85924"
            rx="2.44719"
            ry="2.44128"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></ellipse>
          <path
            d="M34.3979 18.458C35.0907 18.458 35.6536 18.3933 36.3248 18.2637V15.7584C35.9134 15.9096 35.4588 15.9528 35.0258 15.9528C33.965 15.9528 33.5753 15.4344 33.5753 14.441V9.34402H36.3248V6.83875H33.5753V3.12403H30.5443V6.83875H28.5742V9.34402H30.5443V14.7217C30.5443 17.0974 31.8 18.458 34.3979 18.458Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M41.2747 18.458C42.8984 18.458 43.9809 17.9181 44.5222 17.0758C44.5655 17.443 44.6954 17.9397 44.8686 18.2421H47.5964C47.4449 17.7237 47.3366 16.903 47.3366 16.3631V10.4455C47.3366 8.005 45.583 6.62277 42.617 6.62277C40.3654 6.62277 38.6118 7.46507 37.6376 8.69611L39.3696 10.4023C40.149 9.5384 41.1448 9.08486 42.3572 9.08486C43.8294 9.08486 44.4789 9.58159 44.4789 10.3159C44.4789 10.9422 44.0459 11.3742 41.7077 11.3742C39.4562 11.3742 37.183 12.3028 37.183 14.8945C37.183 17.2918 38.9149 18.458 41.2747 18.458ZM41.8809 16.1687C40.7118 16.1687 40.1706 15.672 40.1706 14.7865C40.1706 14.009 40.8201 13.4907 41.9026 13.4907C43.6345 13.4907 44.1108 13.3827 44.4789 13.0155V13.9442C44.4789 15.1753 43.4397 16.1687 41.8809 16.1687Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M49.3069 5.39173H52.4677V2.5625H49.3069V5.39173ZM49.3718 18.2421H52.4028V6.83875H49.3718V18.2421Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M54.6109 18.2421H57.6418V2.90805H54.6109V18.2421Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M63.9416 18.458C67.2757 18.458 68.986 16.7087 68.986 14.8729C68.986 13.2099 68.1417 11.9789 65.3705 11.4821C63.4221 11.1366 62.2097 10.7046 62.2097 10.0351C62.2097 9.45201 62.9025 9.04166 64.0715 9.04166C65.1107 9.04166 65.9767 9.38722 66.6262 10.1431L68.553 8.52333C67.5788 7.31389 65.9767 6.62277 64.0715 6.62277C61.1489 6.62277 59.3303 8.17777 59.3303 10.0783C59.3303 12.1517 61.2354 13.0803 63.2922 13.4475C65.0025 13.7499 65.9551 14.0738 65.9551 14.8081C65.9551 15.4344 65.2839 15.9528 64.0066 15.9528C62.7509 15.9528 61.7767 15.3696 61.322 14.5058L58.7674 15.7152C59.3952 17.2702 61.5385 18.458 63.9416 18.458Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M75.7621 18.458C77.9271 18.458 79.4859 17.5942 80.6549 15.6504L78.2302 14.4194C77.7755 15.3265 77.0395 15.9528 75.7621 15.9528C73.8353 15.9528 72.7961 14.3978 72.7961 12.5188C72.7961 10.6399 73.9003 9.12805 75.7621 9.12805C76.9312 9.12805 77.7106 9.75437 78.1652 10.7046L80.6116 9.40882C79.7889 7.61625 78.1652 6.62277 75.7621 6.62277C71.8003 6.62277 69.7652 9.5168 69.7652 12.5188C69.7652 15.78 72.2333 18.458 75.7621 18.458Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M85.4829 18.458C87.1067 18.458 88.1891 17.9181 88.7304 17.0758C88.7737 17.443 88.9036 17.9397 89.0768 18.2421H91.8046C91.6531 17.7237 91.5448 16.903 91.5448 16.3631V10.4455C91.5448 8.005 89.7912 6.62277 86.8252 6.62277C84.5737 6.62277 82.8201 7.46507 81.8458 8.69611L83.5778 10.4023C84.3572 9.5384 85.353 9.08486 86.5654 9.08486C88.0376 9.08486 88.6871 9.58159 88.6871 10.3159C88.6871 10.9422 88.2541 11.3742 85.9159 11.3742C83.6644 11.3742 81.3912 12.3028 81.3912 14.8945C81.3912 17.2918 83.1231 18.458 85.4829 18.458ZM86.0891 16.1687C84.9201 16.1687 84.3788 15.672 84.3788 14.7865C84.3788 14.009 85.0283 13.4907 86.1108 13.4907C87.8427 13.4907 88.319 13.3827 88.6871 13.0155V13.9442C88.6871 15.1753 87.6479 16.1687 86.0891 16.1687Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M93.3263 18.2421H96.3573V2.90805H93.3263V18.2421Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
          <path
            d="M103.631 18.458C105.861 18.458 107.658 17.5726 108.654 15.996L106.359 14.5274C105.753 15.4776 104.952 15.996 103.631 15.996C102.138 15.996 101.055 15.1753 100.774 13.5771H109.39V12.5188C109.39 9.5168 107.55 6.62277 103.61 6.62277C99.8643 6.62277 97.8293 9.5384 97.8293 12.5404C97.8293 16.8167 101.055 18.458 103.631 18.458ZM100.882 11.2014C101.358 9.75437 102.354 9.08486 103.675 9.08486C105.168 9.08486 106.078 9.97034 106.381 11.2014H100.882Z"
            fill="#1F1E1E"
            data-darkreader-inline-fill=""
            style="
              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);
            "
          ></path>
        </svg>
      </div>
      <label class="slider-container">
        <input
          type="checkbox"
          id="toggleSlider"
          class="slider-input"
          style="display: none"
          checked
        />
        <span class="slider connected no-transition"></span>
      </label>
    </div>
    <div id="state" class="status"></div>
    <div class="controls">
      <button id="settingsButton" class="settings-button">Settings</button>
    </div>
  </body>
</html>


================================================
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
        ? `<b><a href='#login'>Log in</a></b>`
        : "<b>Login required; no URL</b>";
      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 = `<b>Installation needed. Run:</b><pre>${msg.installCmd}</pre>`;
      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
	})
}
Download .txt
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
Download .txt
SYMBOL INDEX (56 symbols across 5 files)

FILE: background.js
  function setPopupIcon (line 5) | function setPopupIcon(base) {
  function enableProxy (line 22) | function enableProxy() {
  function disableProxy (line 35) | function disableProxy() {
  function browserByte (line 84) | function browserByte() {
  function sendPopupStatus (line 94) | function sendPopupStatus() {
  function sendToPopup (line 111) | function sendToPopup(v) {
  function connectToNativeHost (line 123) | function connectToNativeHost() {
  function setProxy (line 174) | function setProxy(proxyPort) {
  function maybeSendInit (line 219) | function maybeSendInit() {

FILE: firefox/background.js
  function setPopupIcon (line 5) | function setPopupIcon(base) {
  function enableProxy (line 17) | function enableProxy() {
  function disableProxy (line 30) | function disableProxy() {
  function browserByte (line 79) | function browserByte() {
  function sendPopupStatus (line 86) | function sendPopupStatus() {
  function sendToPopup (line 112) | function sendToPopup(v) {
  function connectToNativeHost (line 124) | function connectToNativeHost() {
  function setProxy (line 175) | function setProxy(proxyPort) {
  function proxyHandler (line 204) | function proxyHandler(port) {
  function maybeSendInit (line 218) | function maybeSendInit() {

FILE: firefox/popup.js
  function updateSliderState (line 14) | function updateSliderState() {
  function updateStatus (line 28) | function updateStatus(status) {

FILE: popup.js
  function browseToURL (line 3) | function browseToURL() {
  function updateSliderState (line 20) | function updateSliderState() {
  function updateStatus (line 34) | function updateStatus(status) {

FILE: ts-browser-ext.go
  function main (line 43) | func main() {
  function getTargetDir (line 96) | func getTargetDir(browserByte string) (string, error) {
  function uninstall (line 124) | func uninstall() error {
  function install (line 145) | func install(installArg string) error {
  type host (line 213) | type host struct
    method readMessages (line 255) | func (h *host) readMessages() error {
    method handleMessage (line 268) | func (h *host) handleMessage(msg *request) error {
    method handleUp (line 284) | func (h *host) handleUp() error {
    method handleDown (line 288) | func (h *host) handleDown() error {
    method setWantRunning (line 292) | func (h *host) setWantRunning(want bool) error {
    method handleInit (line 318) | func (h *host) handleInit(msg *request) (ret error) {
    method watchIPNBus (line 394) | func (h *host) watchIPNBus(wc *tailscale.IPNBusWatcher) {
    method updateFromWatcher (line 404) | func (h *host) updateFromWatcher(wc *tailscale.IPNBusWatcher) bool {
    method send (line 433) | func (h *host) send(msg *reply) error {
    method getProxyListener (line 454) | func (h *host) getProxyListener() net.Listener {
    method getProxyListenerLocked (line 460) | func (h *host) getProxyListenerLocked() net.Listener {
    method userDial (line 485) | func (h *host) userDial(ctx context.Context, netw, addr string) (net.C...
    method sendStatus (line 498) | func (h *host) sendStatus() {
    method readMessage (line 578) | func (h *host) readMessage() (*request, error) {
    method httpProxyHandler (line 600) | func (h *host) httpProxyHandler() http.Handler {
  function newHost (line 236) | func newHost(r io.Reader, w io.Writer) *host {
  constant maxMsgSize (line 253) | maxMsgSize = 1 << 20
  type Cmd (line 521) | type Cmd
  constant CmdInit (line 524) | CmdInit      Cmd = "init"
  constant CmdUp (line 525) | CmdUp        Cmd = "up"
  constant CmdDown (line 526) | CmdDown      Cmd = "down"
  constant CmdGetStatus (line 527) | CmdGetStatus Cmd = "get-status"
  type request (line 531) | type request struct
  type reply (line 547) | type reply struct
  type procRunningResult (line 559) | type procRunningResult struct
  type initResult (line 565) | type initResult struct
  type status (line 569) | type status struct
Condensed preview — 15 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (98K chars).
[
  {
    "path": "LICENSE",
    "chars": 1510,
    "preview": "BSD 3-Clause License\n\nCopyright (c) 2020 Tailscale Inc & AUTHORS.\n\nRedistribution and use in source and binary forms, wi"
  },
  {
    "path": "PATENTS",
    "chars": 1377,
    "preview": "Additional IP Rights Grant (Patents)\n\n\"This implementation\" means the copyrightable works distributed by\nTailscale Inc. "
  },
  {
    "path": "README.md",
    "chars": 3531,
    "preview": "# Tailscale Browser Extension (Experiment)\n\n[![status: experimental](https://img.shields.io/badge/status-experimental-bl"
  },
  {
    "path": "background.js",
    "chars": 6561,
    "preview": "let proxyEnabled = false;\n\n// setPopupIcon sets the icon. It takes either a boolean (for online/offline)\n// or the base "
  },
  {
    "path": "chrome.txt",
    "chars": 374,
    "preview": "% pwd\n/Users/bradfitz/Library/Application Support/Google/Chrome/NativeMessagingHosts\n\n% cat com.tailscale.chrome-ext.jso"
  },
  {
    "path": "firefox/background.js",
    "chars": 6547,
    "preview": "let proxyEnabled = false;\n\n// setPopupIcon sets the icon. It takes either a boolean (for online/offline)\n// or the base "
  },
  {
    "path": "firefox/manifest.json",
    "chars": 751,
    "preview": "{\n    \"manifest_version\": 3,\n    \"name\": \"Tailscale Extension\",\n    \"version\": \"1.0\",\n    \"description\": \"A Tailscale cl"
  },
  {
    "path": "firefox/popup.html",
    "chars": 11865,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Proxy Toggle</title>\n    <link rel=\"preconnect\" "
  },
  {
    "path": "firefox/popup.js",
    "chars": 3738,
    "preview": "var lastStatus;\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n  const toggleSlider = document.getElementById(\"t"
  },
  {
    "path": "go.mod",
    "chars": 2644,
    "preview": "module github.com/tailscale/ts-browser-ext\n\ngo 1.26.3\n\nrequire (\n\tgithub.com/gorilla/csrf v1.7.3\n\ttailscale.com v1.98.2\n"
  },
  {
    "path": "go.sum",
    "chars": 22506,
    "preview": "9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q=\n9fans.net/go v0.0.8-0."
  },
  {
    "path": "manifest.json",
    "chars": 620,
    "preview": "{\n    \"manifest_version\": 3,\n    \"name\": \"Tailscale Extension\",\n    \"version\": \"1.0\",\n    \"description\": \"A Tailscale cl"
  },
  {
    "path": "popup.html",
    "chars": 12019,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Proxy Toggle</title>\n    <link rel=\"preconnect\" "
  },
  {
    "path": "popup.js",
    "chars": 3441,
    "preview": "var lastStatus;\n\nfunction browseToURL() {\n  if (lastStatus && lastStatus.browseToURL) {\n    chrome.tabs.create({ url: la"
  },
  {
    "path": "ts-browser-ext.go",
    "chars": 15558,
    "preview": "package main\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"l"
  }
]

About this extraction

This page contains the full source code of the tailscale/ts-browser-ext GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 15 files (90.9 KB), approximately 33.7k tokens, and a symbol index with 56 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!