[
  {
    "path": "LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2020 Tailscale Inc & AUTHORS.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "PATENTS",
    "content": "Additional IP Rights Grant (Patents)\n\n\"This implementation\" means the copyrightable works distributed by\nTailscale Inc. as part of the Tailscale project.\n\nTailscale Inc. hereby grants to You a perpetual, worldwide,\nnon-exclusive, no-charge, royalty-free, irrevocable (except as stated\nin this section) patent license to make, have made, use, offer to\nsell, sell, import, transfer and otherwise run, modify and propagate\nthe contents of this implementation of Tailscale, where such license\napplies only to those patent claims, both currently owned or\ncontrolled by Tailscale Inc. and acquired in the future, licensable\nby Tailscale Inc. that are necessarily infringed by this\nimplementation of Tailscale.  This grant does not include claims that\nwould be infringed only as a consequence of further modification of\nthis implementation.  If you or your agent or exclusive licensee\ninstitute or order or agree to the institution of patent litigation\nagainst any entity (including a cross-claim or counterclaim in a\nlawsuit) alleging that this implementation of Tailscale or any code\nincorporated within this implementation of Tailscale constitutes\ndirect or contributory patent infringement, or inducement of patent\ninfringement, then any patent rights granted to you under this License\nfor this implementation of Tailscale shall terminate as of the date\nsuch litigation is filed.\n"
  },
  {
    "path": "README.md",
    "content": "# Tailscale Browser Extension (Experiment)\n\n[![status: experimental](https://img.shields.io/badge/status-experimental-blue)](https://tailscale.com/kb/1167/release-stages/#experimental)\n\nThe [Tailscale](https://tailscale.com/) Browser Extension lets you access your tailnet resources\nusing a browser extension, without necessarily installing Tailscale\nsystem-wide.\n\nIn particular, ...\n\n* you can **simultaneously use a different tailnet per browser profile**\n  * separate out your personal tailnet in its own browser profile\n* you don't need to be root/admin to install it\n* it doesn't interfere with your other OS VPN(s) and route tables and is purely scoped to one browser profile\n\n## How it works\n\nIdeally it would work purely with WASM/WASI, but browser extensions\ndon't have enough APIs, so it regrettably has to use Native Messaging\n([Chrome](https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging),\n[Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging))\nwhere a native binary (using\n[`tsnet`](https://tailscale.com/kb/1244/tsnet)) runs as a child\nprocess under the browser and communicates with the browser extension\nwith JSON messages back and forth.\n\nThe child process then runs an HTTP/SOCKS5 proxy on `localhost:0`\n(with the kernel picking a random free port) and the browser extension\nuses the browser proxy API to send all web traffic through the child's\nproxy, which then sends it out over Tailscale, an exit node, or the\nInternet as normal.\n\n## Status\n\nAs of 2025-02-25, this is **barely just starting to work** and is not\nmeant for end users yet. It's barely meant for developers at this\npoint.\n\n| Browser    | OS | Status |\n| -------- | ------- | ---- |\n| Chrome  | macOS | Works |\n| Chrome  | Linux | Works in theory, untested |\n| Chrome  | Windows | Registry install work not yet done |\n| Firefox  | macOS | Mostly works |\n| Firefox  | Linux | Mostly works in theory, untested |\n| Firefox  | Windows | Registry install work not yet done |\n| Safari  | * | not possible; no support for Native Messaging |\n\n## Developer instructions\n\nTo log out, for now you need to remove & re-add the extension.\n\n### Chrome\n\n1. Open the Extensions page (`chrome://extensions`) or Extensions... > Manage Extensions...\n2. Toggle \"Developer mode\" on.\n3. Click \"Load unpacked\".\n4. Navigate to the directory where you cloned this repo and select it.\n5. Pin the extension to the toolbar.\n6. Click the extension icon.\n7. Follow the instructions in the popup to run the printed `go run ...` command, which builds and registers the native messaging backend.\n8. Click the extension icon again and select \"Log in\".\n\n### Firefox\n\n1. Open the Debugging page (`about:debugging#/runtime/this-firefox`).\n2. Click \"Load Temporary Add-on...\".\n3. Navigate to the `firefox/` subdirectory of this repo and select its `manifest.json`.\n4. 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.\n5. Pin the extension to the toolbar.\n6. Click the extension icon.\n7. Follow the instructions in the popup to run the printed `go run ...` command, which builds and registers the native messaging backend.\n8. Click the extension icon again and select \"Log in\".\n\nTemporary add-ons in Firefox are removed when the browser restarts, so you'll need to reload it from `about:debugging` each session.\n\n## End user instructions\n\nDon't use it yet. It's too rough. See status above.\n"
  },
  {
    "path": "background.js",
    "content": "let proxyEnabled = false;\n\n// setPopupIcon sets the icon. It takes either a boolean (for online/offline)\n// or the base name of the png file.\nfunction setPopupIcon(base) {\n  if (typeof base === \"boolean\") {\n    base = base ? \"online\" : \"offline\";\n  }\n  let iconPath = base + \".png\";\n  console.log(\"set icon path to: \" + iconPath);\n\n  chrome.action.setIcon({ path: iconPath }, () => {\n    if (chrome.runtime.lastError) {\n      console.error(\n        \"Error setting icon to \" + iconPath + \":\",\n        chrome.runtime.lastError.message\n      );\n    }\n  });\n}\n\nfunction enableProxy() {\n  if (deadPort) {\n    console.error(\"Cannot enable proxy, disconnected from native host\");\n    return;\n  }\n\n  if (lastProxyPort) {\n    nmPort.postMessage({ cmd: \"get-status\" });\n  } else {\n    nmPort.postMessage({ cmd: \"up\" });\n  }\n}\n\nfunction disableProxy() {\n  console.log(\"disableProxy called\");\n  if (nmPort && !deadPort) {\n    console.log(\"Sending down command to native host\");\n    nmPort.postMessage({ cmd: \"down\" });\n  } else {\n    console.log(\n      \"Cannot send down command - nmPort:\",\n      !!nmPort,\n      \"deadPort:\",\n      deadPort\n    );\n  }\n  proxyEnabled = false;\n  lastProxyPort = 0;\n  console.log(\n    \"Proxy disabled, proxyEnabled:\",\n    proxyEnabled,\n    \"lastProxyPort:\",\n    lastProxyPort\n  );\n}\n\nconsole.log(\"starting ts-browser-ext\");\n\nlet popupPort = null;\n\nchrome.runtime.onConnect.addListener((port) => {\n  if (port.name != \"popup\") {\n    return;\n  }\n  popupPort = port;\n\n  console.log(\"Popup connected\");\n\n  port.onMessage.addListener((msg) => {\n    console.log(\"Message from popup:\", msg);\n  });\n\n  port.onDisconnect.addListener(() => {\n    console.log(\"Popup disconnected\");\n    popupPort = null;\n  });\n\n  sendPopupStatus();\n});\n\n// browserByte returns either \"F\" for Firefox or \"C\" for chrome.\n// Other browsers return \"?\".\nfunction browserByte() {\n  if (typeof chrome !== \"undefined\") {\n    if (typeof browser !== \"undefined\") {\n      return \"F\"; // Firefox supports both `chrome` and `browser`\n    }\n    return \"C\";\n  }\n  return \"?\";\n}\n\nfunction sendPopupStatus() {\n  if (deadPort) {\n    setPopupIcon(\"need-install\");\n    console.log(\"sendPopupStatus... no nmPort\");\n    sendToPopup({\n      installCmd:\n        \"go run github.com/tailscale/ts-browser-ext@main --install=\" +\n        browserByte() +\n        chrome.runtime.id,\n    });\n    return;\n  }\n  setPopupIcon(proxyEnabled ? \"online\" : \"offline\");\n\n  sendToPopup({ status: lastStatus });\n}\n\nfunction sendToPopup(v) {\n  if (popupPort) {\n    popupPort.postMessage(v);\n  }\n}\n\nlet nmPort = null; // even non-null if lacking permission\nlet deadPort = true;\nlet portError = null;\n\nconnectToNativeHost();\n\nfunction connectToNativeHost() {\n  if (nmPort && !deadPort) {\n    return;\n  }\n  console.log(\"Connecting to native messaging host...\");\n  nmPort = chrome.runtime.connectNative(\"com.tailscale.browserext.chrome\");\n\n  nmPort.onDisconnect.addListener(() => {\n    deadPort = true;\n    setPopupIcon(\"need-install\");\n    disableProxy();\n    const error = chrome.runtime.lastError;\n    if (error) {\n      console.error(\"Connection failed:\", error.message);\n      portError = error.message;\n      setTimeout(connectToNativeHost, 1000);\n    } else {\n      console.error(\"Disconnected from native host\");\n    }\n  });\n  nmPort.onMessage.addListener((message) => {\n    console.log(\"got message: \" + JSON.stringify(message));\n    if (deadPort) {\n      console.log(\"connected to native backend\");\n      deadPort = false;\n    }\n    if (message.procRunning) {\n      if (message.procRunning.port) {\n        setProxy(message.procRunning.port);\n      } else if (message.procRunning.errror) {\n        console.log(\n          \"procRunning error from backend: \" + message.procRunning.err\n        );\n        disableProxy();\n      }\n    }\n    if (message.init && message.init.error) {\n      console.log(\"init error from backend: \" + message.init.err);\n      disableProxy();\n    }\n    if (message.status) {\n      lastStatus = message.status;\n    }\n    maybeSendInit();\n    sendPopupStatus();\n  });\n}\n\nvar lastProxyPort = 0;\nvar lastStatus = {}; // last Go status\n\nfunction setProxy(proxyPort) {\n  if (proxyPort) {\n    proxyEnabled = true;\n    lastProxyPort = proxyPort;\n    console.log(\"Enabling proxy at port: \" + proxyPort);\n  } else {\n    proxyEnabled = false;\n    console.log(\"Disabling proxy...\");\n    chrome.proxy.settings.set(\n      {\n        value: {\n          mode: \"direct\",\n        },\n        scope: \"regular\",\n      },\n      () => {\n        console.log(\"Proxy disabled.\");\n      }\n    );\n    return;\n  }\n  chrome.proxy.settings.set(\n    {\n      value: {\n        mode: \"fixed_servers\",\n        rules: {\n          singleProxy: {\n            scheme: \"http\",\n            host: \"127.0.0.1\",\n            port: proxyPort,\n          },\n          bypassList: [\"localhost\", \"127.*\"],\n        },\n      },\n      scope: \"regular\",\n    },\n    () => {\n      console.log(\"Proxy enabled: 127.0.0.1:\" + proxyPort);\n    }\n  );\n}\n\nvar profileID = \"\";\nvar didInit = false;\n\nfunction maybeSendInit() {\n  if (!profileID || didInit || deadPort) {\n    return;\n  }\n  nmPort.postMessage({ cmd: \"init\", initID: profileID });\n  didInit = true;\n}\n\nchrome.storage.local.get(\"profileId\", (result) => {\n  if (!result.profileId) {\n    const profileId = crypto.randomUUID();\n    chrome.storage.local.set({ profileId }, () => {\n      console.log(\"Generated profile ID:\", profileId);\n      profileID = profileId;\n      maybeSendInit();\n    });\n  } else {\n    console.log(\"Profile ID already exists:\", result.profileId);\n    profileID = result.profileId;\n    maybeSendInit();\n  }\n});\n\n// Listener for messages from the popup\nchrome.runtime.onMessage.addListener((message, sender, sendResponse) => {\n  console.log(\"bg: Received message:\", message);\n  if (message.command === \"toggleProxy\") {\n    console.log(\"bg: toggleProxy received, current proxy=\" + proxyEnabled);\n    proxyEnabled = !proxyEnabled;\n    if (proxyEnabled) {\n      console.log(\"bg: Enabling proxy\");\n      enableProxy();\n      console.log(\"bg: toggleProxy on, now proxy=\" + proxyEnabled);\n      sendResponse({ status: lastStatus });\n      console.log(\"bg: toggleProxy on, sent status response\");\n    } else {\n      console.log(\"bg: Disabling proxy\");\n      disableProxy();\n      console.log(\"bg: toggleProxy off, now proxy=\" + proxyEnabled);\n      sendResponse({ status: \"Disconnected\" });\n      console.log(\"bg: toggleProxy off, sent disconnected response\");\n    }\n    setPopupIcon(proxyEnabled);\n    return true; // Keep the message channel open for the async response\n  }\n});\n"
  },
  {
    "path": "chrome.txt",
    "content": "% pwd\n/Users/bradfitz/Library/Application Support/Google/Chrome/NativeMessagingHosts\n\n% cat com.tailscale.chrome-ext.json\n{\n  \"name\": \"com.tailscale.chrome-ext\",\n  \"description\": \"Tailscale Native Extension\",\n  \"path\": \"/Users/bradfitz/go/bin/ts-browser-native-ext\",\n  \"type\": \"stdio\",\n  \"allowed_origins\": [\n    \"chrome-extension://gdopnimobeboikkiagbnnbcijkjdjcad/\"\n  ]\n}\n"
  },
  {
    "path": "firefox/background.js",
    "content": "let proxyEnabled = false;\n\n// setPopupIcon sets the icon. It takes either a boolean (for online/offline)\n// or the base name of the png file.\nfunction setPopupIcon(base) {\n  if (typeof base === \"boolean\") {\n    base = base ? \"online\" : \"offline\";\n  }\n  let iconPath = base + \".png\";\n  console.log(\"set icon path to: \" + iconPath);\n\n  browser.action.setIcon({ path: iconPath }).catch((error) => {\n    console.error(\"Error setting icon to \" + iconPath + \":\", error.message);\n  });\n}\n\nfunction enableProxy() {\n  if (deadPort) {\n    console.error(\"Cannot enable proxy, disconnected from native host\");\n    return;\n  }\n\n  if (lastProxyPort) {\n    nmPort.postMessage({ cmd: \"get-status\" });\n  } else {\n    nmPort.postMessage({ cmd: \"up\" });\n  }\n}\n\nfunction disableProxy() {\n  console.log(\"disableProxy called\");\n  if (nmPort && !deadPort) {\n    console.log(\"Sending down command to native host\");\n    nmPort.postMessage({ cmd: \"down\" });\n  } else {\n    console.log(\n      \"Cannot send down command - nmPort:\",\n      !!nmPort,\n      \"deadPort:\",\n      deadPort\n    );\n  }\n  proxyEnabled = false;\n  lastProxyPort = 0;\n  console.log(\n    \"Proxy disabled, proxyEnabled:\",\n    proxyEnabled,\n    \"lastProxyPort:\",\n    lastProxyPort\n  );\n}\n\nconsole.log(\"starting ts-browser-ext\");\n\nlet popupPort = null;\n\nbrowser.runtime.onConnect.addListener((port) => {\n  if (port.name != \"popup\") {\n    return;\n  }\n  popupPort = port;\n\n  console.log(\"Popup connected\");\n\n  port.onMessage.addListener((msg) => {\n    console.log(\"Message from popup:\", msg);\n  });\n\n  port.onDisconnect.addListener(() => {\n    console.log(\"Popup disconnected\");\n    popupPort = null;\n  });\n\n  sendPopupStatus();\n});\n\n// browserByte returns either \"F\" for Firefox or \"C\" for chrome.\n// Other browsers return \"?\".\nfunction browserByte() {\n  if (typeof browser !== \"undefined\") {\n    return \"F\";\n  }\n  return \"?\";\n}\n\nfunction sendPopupStatus() {\n  // firefox requires that extensions settings proxies have private browsing access\n  browser.extension.isAllowedIncognitoAccess().then(isAllowed => {\n    if (!isAllowed) {\n          sendToPopup({\n        needsIncognitoPermission: true\n      });\n    }\n  });\n\n  if (deadPort) {\n    setPopupIcon(\"need-install\");\n    console.log(\"sendPopupStatus... no nmPort\");\n    sendToPopup({\n      installCmd:\n        \"go run github.com/tailscale/ts-browser-ext@main --install=\" +\n        browserByte() +\n        browser.runtime.id,\n    });\n    return;\n  }\n  setPopupIcon(proxyEnabled ? \"online\" : \"offline\");\n\n  sendToPopup({ status: lastStatus });\n}\n\nfunction sendToPopup(v) {\n  if (popupPort) {\n    popupPort.postMessage(v);\n  }\n}\n\nlet nmPort = null; // even non-null if lacking permission\nlet deadPort = true;\nlet portError = null;\n\nconnectToNativeHost();\n\nfunction connectToNativeHost() {\n  if (nmPort && !deadPort) {\n    return;\n  }\n  console.log(\"Connecting to native messaging host...\");\n  nmPort = browser.runtime.connectNative(\"com.tailscale.browserext.firefox\");\n\n  nmPort.onDisconnect.addListener(() => {\n    deadPort = true;\n    setPopupIcon(\"need-install\");\n    disableProxy();\n    const error = browser.runtime.lastError;\n    if (error) {\n      console.error(\"Connection failed:\", error.message);\n      portError = error.message;\n      setTimeout(connectToNativeHost, 1000);\n    } else {\n      console.error(\"Disconnected from native host\");\n    }\n  });\n  nmPort.onMessage.addListener((message) => {\n    console.log(\"got message: \" + JSON.stringify(message));\n    if (deadPort) {\n      console.log(\"connected to native backend\");\n      deadPort = false;\n    }\n    if (message.procRunning) {\n      if (message.procRunning.port) {\n        setProxy(message.procRunning.port);\n      } else if (message.procRunning.errror) {\n        console.log(\n          \"procRunning error from backend: \" + message.procRunning.err\n        );\n        disableProxy();\n      }\n    }\n    if (message.init && message.init.error) {\n      console.log(\"init error from backend: \" + message.init.err);\n      disableProxy();\n    }\n    if (message.status) {\n      lastStatus = message.status;\n    }\n    maybeSendInit();\n    sendPopupStatus();\n  });\n}\n\nvar lastProxyPort = 0;\nvar lastStatus = {}; // last Go status\n\nfunction setProxy(proxyPort) {\n  const handleProxyRequest = proxyHandler(proxyPort)\n  if (proxyPort) {\n    proxyEnabled = true;\n    lastProxyPort = proxyPort;\n    console.log(\"Enabling proxy at port: \" + proxyPort);\n  } else {\n    proxyEnabled = false;\n    console.log(\"Disabling proxy...\");\n    browser.proxy.onRequest.removeListener(handleProxyRequest)\n    browser.proxy.settings\n      .set({\n        value: {\n          mode: \"direct\",\n        },\n        scope: \"regular\",\n      })\n      .then(() => {\n        console.log(\"Proxy disabled.\");\n      });\n    return;\n  }\n  browser.proxy.onRequest.addListener(handleProxyRequest, { urls: [\"<all_urls>\"] })\n}\n\nvar profileID = \"\";\nvar didInit = false;\n\n// firefox has unique behaviour where only socks proxies can handle domain resolution\nfunction proxyHandler(port) {\n  return function handleProxyRequest(requestInfo) {\n    const url = new URL(requestInfo.url)\n\n    // we need to use http for 100.100.100.100\n    if (url.hostname == '100.100.100.100') {\n      return { type: \"http\", host: \"127.0.0.1\", port: port };\n    }\n\n    // use socks for everything else\n    return { type: \"socks\", host: \"127.0.0.1\", port: port, proxyDNS: true, bypassList: [\"localhost\", \"127.*\"] };\n  }\n}\n\nfunction maybeSendInit() {\n  if (!profileID || didInit || deadPort) {\n    return;\n  }\n  nmPort.postMessage({ cmd: \"init\", initID: profileID });\n  didInit = true;\n}\n\nbrowser.storage.local.get(\"profileId\").then((result) => {\n  if (!result.profileId) {\n    const profileId = crypto.randomUUID();\n    browser.storage.local.set({ profileId }).then(() => {\n      console.log(\"Generated profile ID:\", profileId);\n      profileID = profileId;\n      maybeSendInit();\n    });\n  } else {\n    console.log(\"Profile ID already exists:\", result.profileId);\n    profileID = result.profileId;\n    maybeSendInit();\n  }\n});\n\n// Listener for messages from the popup\nbrowser.runtime.onMessage.addListener((message, sender) => {\n  console.log(\"bg: Received message:\", message);\n  if (message.command === \"toggleProxy\") {\n    console.log(\"bg: toggleProxy received, current proxy=\" + proxyEnabled);\n    proxyEnabled = !proxyEnabled;\n    if (proxyEnabled) {\n      console.log(\"bg: Enabling proxy\");\n      enableProxy();\n    } else {\n      console.log(\"bg: Disabling proxy\");\n      disableProxy();\n    }\n    return Promise.resolve({ status: lastStatus });\n  }\n});\n"
  },
  {
    "path": "firefox/manifest.json",
    "content": "{\n    \"manifest_version\": 3,\n    \"name\": \"Tailscale Extension\",\n    \"version\": \"1.0\",\n    \"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.\",\n    \"browser_specific_settings\": {\n        \"gecko\": {\n          \"id\": \"browser-ext@tailscale.com\",\n          \"strict_min_version\": \"50.0\"\n        }\n      },\n     \"permissions\": [\n        \"proxy\",\n        \"storage\",\n        \"nativeMessaging\"\n    ],\n    \"host_permissions\": [\n        \"<all_urls>\"\n    ],\n    \"background\": {\n        \"scripts\": [\"background.js\"]\n    },\n    \"action\": {\n        \"default_popup\": \"popup.html\",\n        \"default_icon\": \"icon.png\"\n    }\n}\n"
  },
  {
    "path": "firefox/popup.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Proxy Toggle</title>\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n    <link\n      href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap\"\n      rel=\"stylesheet\"\n    />\n    <script src=\"popup.js\"></script>\n    <style>\n      body {\n        background-color: #faf7f6;\n        color: #575655;\n        font-family: \"Inter\", -apple-system, BlinkMacSystemFont, \"Segoe UI\",\n          Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n        margin: 0;\n        padding: 16px;\n        width: 300px;\n      }\n\n      .header {\n        display: flex;\n        justify-content: space-between;\n        align-items: center;\n        margin-bottom: 16px;\n      }\n\n      .logo {\n        width: 120px;\n      }\n\n      /* Slider styles */\n      .slider-container {\n        position: relative;\n        width: 44px;\n        height: 24px;\n      }\n\n      .slider {\n        position: absolute;\n        cursor: pointer;\n        top: 0;\n        left: 0;\n        right: 0;\n        bottom: 0;\n        background-color: #ccc;\n        transition: 0.4s;\n        border-radius: 24px;\n      }\n\n      .slider.no-transition {\n        transition: none;\n      }\n\n      .slider.loading {\n        filter: grayscale(100%);\n        opacity: 0.7;\n        cursor: wait;\n      }\n\n      .slider:before {\n        position: absolute;\n        content: \"\";\n        height: 20px;\n        width: 20px;\n        left: 2px;\n        bottom: 2px;\n        background-color: #fcfcfc;\n        transition: 0.4s;\n        border-radius: 50%;\n        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n      }\n\n      .slider.connected {\n        background-color: #4c78c6;\n      }\n\n      .slider.connected:before {\n        transform: translateX(20px);\n      }\n\n      #state {\n        white-space: nowrap;\n        margin-bottom: 16px;\n        font-size: 14px;\n      }\n\n      button {\n        background-color: #2e2d2d;\n        color: #fff;\n        border: none;\n        padding: 8px 16px;\n        border-radius: 6px;\n        font-size: 14px;\n        cursor: pointer;\n        transition: background-color 0.2s;\n      }\n\n      button:hover {\n        background-color: #1f1e1e;\n      }\n\n      .settings-button {\n        padding: 6px 12px;\n        font-size: 12px;\n      }\n\n      .controls {\n        display: flex;\n        gap: 8px;\n        margin-top: 16px;\n      }\n\n      .status {\n        padding: 8px;\n        border-radius: 4px;\n        background-color: #f5f5f5;\n        margin-bottom: 16px;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"header\">\n      <div class=\"logo\">\n        <svg\n          class=\"transition-colors duration-200\"\n          width=\"100%\"\n          height=\"100%\"\n          viewBox=\"0 0 110 20\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n        >\n          <ellipse\n            cx=\"2.44719\"\n            cy=\"10.1796\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            cx=\"9.79094\"\n            cy=\"10.1796\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            opacity=\"0.2\"\n            cx=\"2.44719\"\n            cy=\"17.5077\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            opacity=\"0.2\"\n            cx=\"17.1269\"\n            cy=\"17.5077\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            cx=\"9.79094\"\n            cy=\"17.5077\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            cx=\"17.1269\"\n            cy=\"10.1796\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            opacity=\"0.2\"\n            cx=\"2.44719\"\n            cy=\"2.85924\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            opacity=\"0.2\"\n            cx=\"9.79094\"\n            cy=\"2.85924\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            opacity=\"0.2\"\n            cx=\"17.1269\"\n            cy=\"2.85924\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <path\n            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\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            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\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            d=\"M49.3069 5.39173H52.4677V2.5625H49.3069V5.39173ZM49.3718 18.2421H52.4028V6.83875H49.3718V18.2421Z\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            d=\"M54.6109 18.2421H57.6418V2.90805H54.6109V18.2421Z\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            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\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            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\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            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\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            d=\"M93.3263 18.2421H96.3573V2.90805H93.3263V18.2421Z\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            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\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n        </svg>\n      </div>\n      <div class=\"slider-container\">\n        <input type=\"checkbox\" id=\"toggleSlider\" class=\"slider\" checked />\n      </div>\n    </div>\n    <div id=\"state\">Disconnected</div>\n    <div class=\"controls\">\n      <button id=\"settingsButton\" class=\"settings-button\">Settings</button>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "firefox/popup.js",
    "content": "var lastStatus;\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n  const toggleSlider = document.getElementById(\"toggleSlider\");\n  const slider = document.querySelector(\".slider\");\n  const settingsButton = document.getElementById(\"settingsButton\");\n  const stateDisplay = document.getElementById(\"state\");\n  let isConnected = false;\n  let isLoading = true;\n  let hasReceivedInitialState = false;\n\n  const port = browser.runtime.connect({ name: \"popup\" });\n\n  function updateSliderState() {\n    if (isLoading) {\n      slider.className = \"slider loading\";\n      toggleSlider.checked = true; // Assume connected while loading\n      return;\n    }\n    // Only remove no-transition after we've received and applied the initial state\n    if (hasReceivedInitialState) {\n      slider.classList.remove(\"no-transition\");\n    }\n    slider.className = `slider ${isConnected ? \"connected\" : \"\"}`;\n    toggleSlider.checked = isConnected;\n  }\n\n  function updateStatus(status) {\n    isLoading = false;\n    hasReceivedInitialState = true;\n    if (status.error) {\n      if (status.error === \"State: Stopped\") {\n        stateDisplay.textContent = \"Disconnected\";\n        isConnected = false;\n        updateSliderState();\n        return;\n      }\n      stateDisplay.textContent = `Error: ${status.error}`;\n      return;\n    }\n    if (status.needsLogin) {\n      stateDisplay.innerHTML = status.browseToURL\n        ? `<b><a href='${status.browseToURL}'>Log in</a></b>`\n        : \"<b>Login required; no URL</b>\";\n      return;\n    }\n    if (typeof status === \"string\" && status === \"Disconnected\") {\n      stateDisplay.textContent = \"Disconnected\";\n      isConnected = false;\n      updateSliderState();\n      return;\n    }\n    if (status.running !== undefined) {\n      stateDisplay.textContent = status.running\n        ? `Connected as ${status.tailnet || \"Not connected\"}`\n        : \"Disconnected\";\n      isConnected = status.running;\n      updateSliderState();\n    }\n  }\n\n  port.onMessage.addListener((msg) => {\n    console.log(\"Received from background:\", JSON.stringify(msg));\n\n    // firefox requires that extensions settings proxies have private browsing access\n    if (msg.needsIncognitoPermission) {\n      console.log(\"Private browsing permission needed\")\n      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>`\n      return;\n    }\n\n    if (msg.installCmd) {\n      console.log(\"Received install command\");\n      stateDisplay.innerHTML = `<b>Installation needed. Run:</b><pre>${msg.installCmd}</pre>`;\n      toggleSlider.disabled = true;\n      settingsButton.hidden = true;\n      return;\n    }\n    if (msg.error) {\n      console.log(\"Error from background:\", msg);\n      stateDisplay.textContent = msg.error;\n      toggleSlider.disabled = true;\n      settingsButton.hidden = true;\n      return;\n    }\n    if (msg.status) {\n      console.log(\"Received status update:\", msg.status);\n      updateStatus(msg.status);\n    }\n  });\n\n  toggleSlider.addEventListener(\"change\", () => {\n    console.log(\"Toggle slider changed, current state:\", isConnected);\n    browser.runtime.sendMessage({ command: \"toggleProxy\" }).then((response) => {\n      console.log(\"Received response from background:\", response);\n      if (response && response.status) {\n        updateStatus(response.status);\n      }\n    });\n    console.log(\"Sent toggleProxy command to background\");\n  });\n\n  settingsButton.addEventListener(\"click\", () => {\n    console.log(\"Settings button clicked\");\n    browser.tabs.create({ url: \"http://100.100.100.100\" });\n  });\n\n  window.addEventListener(\"beforeunload\", () => {\n    port.disconnect();\n  });\n});\n"
  },
  {
    "path": "go.mod",
    "content": "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)\n\nrequire (\n\tfilippo.io/edwards25519 v1.2.0 // indirect\n\tgithub.com/akutz/memconn v0.1.0 // indirect\n\tgithub.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect\n\tgithub.com/coder/websocket v1.8.12 // indirect\n\tgithub.com/creachadair/msync v0.7.1 // indirect\n\tgithub.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect\n\tgithub.com/fxamacker/cbor/v2 v2.9.0 // indirect\n\tgithub.com/gaissmai/bart v0.26.1 // indirect\n\tgithub.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect\n\tgithub.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect\n\tgithub.com/google/btree v1.1.3 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/gorilla/securecookie v1.1.2 // indirect\n\tgithub.com/hdevalence/ed25519consensus v0.2.0 // indirect\n\tgithub.com/huin/goupnp v1.3.0 // indirect\n\tgithub.com/jsimonetti/rtnetlink v1.4.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect\n\tgithub.com/mdlayher/socket v0.5.0 // indirect\n\tgithub.com/mitchellh/go-ps v1.0.0 // indirect\n\tgithub.com/pires/go-proxyproto v0.8.1 // indirect\n\tgithub.com/safchain/ethtool v0.3.0 // indirect\n\tgithub.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect\n\tgithub.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect\n\tgithub.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd // indirect\n\tgithub.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect\n\tgithub.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect\n\tgithub.com/tailscale/wireguard-go v0.0.0-20260427181203-e3ac4a0afb4e // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgo4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect\n\tgo4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/oauth2 v0.36.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/term v0.42.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgolang.org/x/time v0.12.0 // indirect\n\tgolang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect\n\tgolang.zx2c4.com/wireguard/windows v0.5.3 // indirect\n\tgvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q=\n9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM=\nfilippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=\nfilippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=\nfilippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=\nfilippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=\ngithub.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=\ngithub.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=\ngithub.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=\ngithub.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=\ngithub.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=\ngithub.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=\ngithub.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=\ngithub.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=\ngithub.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=\ngithub.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=\ngithub.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=\ngithub.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=\ngithub.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=\ngithub.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02 h1:bXAPYSbdYbS5VTy92NIUbeDI1qyggi+JYh5op9IFlcQ=\ngithub.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=\ngithub.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=\ngithub.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=\ngithub.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=\ngithub.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=\ngithub.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=\ngithub.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=\ngithub.com/creachadair/mds v0.25.9 h1:080Hr8laN2h+l3NeVCGMBpXtIPnl9mz8e4HLraGPqtA=\ngithub.com/creachadair/mds v0.25.9/go.mod h1:4hatI3hRM+qhzuAmqPRFvaBM8mONkS7nsLxkcuTYUIs=\ngithub.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7uho=\ngithub.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008=\ngithub.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=\ngithub.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=\ngithub.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=\ngithub.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8=\ngithub.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=\ngithub.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=\ngithub.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=\ngithub.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=\ngithub.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=\ngithub.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=\ngithub.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=\ngithub.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=\ngithub.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=\ngithub.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I=\ngithub.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=\ngithub.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=\ngithub.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=\ngithub.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=\ngithub.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=\ngithub.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I=\ngithub.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=\ngithub.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=\ngithub.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=\ngithub.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/csrf v1.7.3 h1:BHWt6FTLZAb2HtWT5KDBf6qgpZzvtbp9QWDRKZMXJC0=\ngithub.com/gorilla/csrf v1.7.3/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=\ngithub.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=\ngithub.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=\ngithub.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=\ngithub.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=\ngithub.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=\ngithub.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=\ngithub.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=\ngithub.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=\ngithub.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=\ngithub.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=\ngithub.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=\ngithub.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=\ngithub.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=\ngithub.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=\ngithub.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=\ngithub.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=\ngithub.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=\ngithub.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=\ngithub.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=\ngithub.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=\ngithub.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=\ngithub.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=\ngithub.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=\ngithub.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=\ngithub.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=\ngithub.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=\ngithub.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=\ngithub.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=\ngithub.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=\ngithub.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=\ngithub.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=\ngithub.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=\ngithub.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=\ngithub.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=\ngithub.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=\ngithub.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=\ngithub.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE=\ngithub.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=\ngithub.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc=\ngithub.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89/go.mod h1:wn16Km1EZOX4UEAyaZa3dBwfFGOJ7neck40NcwosJUw=\ngithub.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=\ngithub.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=\ngithub.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM=\ngithub.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=\ngithub.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=\ngithub.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=\ngithub.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=\ngithub.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=\ngithub.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=\ngithub.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=\ngithub.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=\ngithub.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=\ngithub.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=\ngithub.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=\ngithub.com/tailscale/wireguard-go v0.0.0-20260427181203-e3ac4a0afb4e h1:GexFR7ak1iz26fxg8HWCpOEqAOL8UEZJ7J3JxeCalDs=\ngithub.com/tailscale/wireguard-go v0.0.0-20260427181203-e3ac4a0afb4e/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=\ngithub.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=\ngithub.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=\ngithub.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=\ngithub.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=\ngithub.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg=\ngithub.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=\ngithub.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=\ngithub.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngo4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=\ngo4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=\ngo4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=\ngo4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=\ngolang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=\ngolang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=\ngolang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=\ngolang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=\ngolang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=\ngolang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=\ngolang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=\ngolang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=\ngolang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngolang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=\ngolang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=\ngolang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=\ngolang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=\ngolang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=\ngolang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=\ngolang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=\ngolang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 h1:Zy8IV/+FMLxy6j6p87vk/vQGKcdnbprwjTxc8UiUtsA=\ngvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=\nhonnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=\nhonnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=\nhowett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=\nhowett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=\nsoftware.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=\nsoftware.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=\ntailscale.com v1.98.2 h1:HP5gt0qyLKtJoDV7PMUvPpiXjMFk4nzXMbm7JdjttMY=\ntailscale.com v1.98.2/go.mod h1:U23ZwbZlKJMNU7CScy+lCVVlece/S5n09q0nyudncBI=\n"
  },
  {
    "path": "manifest.json",
    "content": "{\n    \"manifest_version\": 3,\n    \"name\": \"Tailscale Extension\",\n    \"version\": \"1.0\",\n    \"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.\",\n    \"permissions\": [\n        \"proxy\",\n        \"background\",\n        \"storage\",\n        \"nativeMessaging\"\n    ],\n    \"host_permissions\": [\n        \"<all_urls>\"\n    ],\n    \"background\": {\n        \"service_worker\": \"background.js\"\n    },\n    \"action\": {\n        \"default_popup\": \"popup.html\",\n        \"default_icon\": \"icon.png\"\n    }\n}\n"
  },
  {
    "path": "popup.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Proxy Toggle</title>\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n    <link\n      href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap\"\n      rel=\"stylesheet\"\n    />\n    <script src=\"popup.js\"></script>\n    <style>\n      body {\n        background-color: #faf7f6;\n        color: #575655;\n        font-family: \"Inter\", -apple-system, BlinkMacSystemFont, \"Segoe UI\",\n          Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n        margin: 0;\n        padding: 16px;\n        width: 300px;\n      }\n\n      .header {\n        display: flex;\n        justify-content: space-between;\n        align-items: center;\n        margin-bottom: 16px;\n      }\n\n      .logo {\n        width: 120px;\n      }\n\n      /* Slider styles */\n      .slider-container {\n        position: relative;\n        width: 44px;\n        height: 24px;\n      }\n\n      .slider {\n        position: absolute;\n        cursor: pointer;\n        top: 0;\n        left: 0;\n        right: 0;\n        bottom: 0;\n        background-color: #ccc;\n        transition: 0.4s;\n        border-radius: 24px;\n      }\n\n      .slider.no-transition {\n        transition: none;\n      }\n\n      .slider.loading {\n        filter: grayscale(100%);\n        opacity: 0.7;\n        cursor: wait;\n      }\n\n      .slider:before {\n        position: absolute;\n        content: \"\";\n        height: 20px;\n        width: 20px;\n        left: 2px;\n        bottom: 2px;\n        background-color: #fcfcfc;\n        transition: 0.4s;\n        border-radius: 50%;\n        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n      }\n\n      .slider.connected {\n        background-color: #4c78c6;\n      }\n\n      .slider.connected:before {\n        transform: translateX(20px);\n      }\n\n      #state {\n        white-space: nowrap;\n        margin-bottom: 16px;\n        font-size: 14px;\n      }\n\n      button {\n        background-color: #2e2d2d;\n        color: #fff;\n        border: none;\n        padding: 8px 16px;\n        border-radius: 6px;\n        font-size: 14px;\n        cursor: pointer;\n        transition: background-color 0.2s;\n      }\n\n      button:hover {\n        background-color: #1f1e1e;\n      }\n\n      .settings-button {\n        padding: 6px 12px;\n        font-size: 12px;\n      }\n\n      .controls {\n        display: flex;\n        gap: 8px;\n        margin-top: 16px;\n      }\n\n      .status {\n        padding: 8px;\n        border-radius: 4px;\n        background-color: #f5f5f5;\n        margin-bottom: 16px;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"header\">\n      <div class=\"logo\">\n        <svg\n          class=\"transition-colors duration-200\"\n          width=\"100%\"\n          height=\"100%\"\n          viewBox=\"0 0 110 20\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n        >\n          <ellipse\n            cx=\"2.44719\"\n            cy=\"10.1796\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            cx=\"9.79094\"\n            cy=\"10.1796\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            opacity=\"0.2\"\n            cx=\"2.44719\"\n            cy=\"17.5077\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            opacity=\"0.2\"\n            cx=\"17.1269\"\n            cy=\"17.5077\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            cx=\"9.79094\"\n            cy=\"17.5077\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            cx=\"17.1269\"\n            cy=\"10.1796\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            opacity=\"0.2\"\n            cx=\"2.44719\"\n            cy=\"2.85924\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            opacity=\"0.2\"\n            cx=\"9.79094\"\n            cy=\"2.85924\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <ellipse\n            opacity=\"0.2\"\n            cx=\"17.1269\"\n            cy=\"2.85924\"\n            rx=\"2.44719\"\n            ry=\"2.44128\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></ellipse>\n          <path\n            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\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            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\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            d=\"M49.3069 5.39173H52.4677V2.5625H49.3069V5.39173ZM49.3718 18.2421H52.4028V6.83875H49.3718V18.2421Z\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            d=\"M54.6109 18.2421H57.6418V2.90805H54.6109V18.2421Z\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            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\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            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\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            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\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            d=\"M93.3263 18.2421H96.3573V2.90805H93.3263V18.2421Z\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n          <path\n            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\"\n            fill=\"#1F1E1E\"\n            data-darkreader-inline-fill=\"\"\n            style=\"\n              --darkreader-inline-fill: var(--darkreader-text-1f1e1e, #d5d1cc);\n            \"\n          ></path>\n        </svg>\n      </div>\n      <label class=\"slider-container\">\n        <input\n          type=\"checkbox\"\n          id=\"toggleSlider\"\n          class=\"slider-input\"\n          style=\"display: none\"\n          checked\n        />\n        <span class=\"slider connected no-transition\"></span>\n      </label>\n    </div>\n    <div id=\"state\" class=\"status\"></div>\n    <div class=\"controls\">\n      <button id=\"settingsButton\" class=\"settings-button\">Settings</button>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "popup.js",
    "content": "var lastStatus;\n\nfunction browseToURL() {\n  if (lastStatus && lastStatus.browseToURL) {\n    chrome.tabs.create({ url: lastStatus.browseToURL });\n  }\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n  const toggleSlider = document.getElementById(\"toggleSlider\");\n  const slider = document.querySelector(\".slider\");\n  const settingsButton = document.getElementById(\"settingsButton\");\n  const stateDisplay = document.getElementById(\"state\");\n  let isConnected = false;\n  let isLoading = true;\n  let hasReceivedInitialState = false;\n\n  const port = chrome.runtime.connect({ name: \"popup\" });\n\n  function updateSliderState() {\n    if (isLoading) {\n      slider.className = \"slider loading\";\n      toggleSlider.checked = true; // Assume connected while loading\n      return;\n    }\n    // Only remove no-transition after we've received and applied the initial state\n    if (hasReceivedInitialState) {\n      slider.classList.remove(\"no-transition\");\n    }\n    slider.className = `slider ${isConnected ? \"connected\" : \"\"}`;\n    toggleSlider.checked = isConnected;\n  }\n\n  function updateStatus(status) {\n    isLoading = false;\n    hasReceivedInitialState = true;\n    if (status.error) {\n      if (status.error === \"State: Stopped\") {\n        stateDisplay.textContent = \"Disconnected\";\n        isConnected = false;\n        updateSliderState();\n        return;\n      }\n      stateDisplay.textContent = `Error: ${status.error}`;\n      return;\n    }\n    if (status.needsLogin) {\n      stateDisplay.innerHTML = status.browseToURL\n        ? `<b><a href='#login'>Log in</a></b>`\n        : \"<b>Login required; no URL</b>\";\n      return;\n    }\n    if (typeof status === \"string\" && status === \"Disconnected\") {\n      stateDisplay.textContent = \"Disconnected\";\n      isConnected = false;\n      updateSliderState();\n      return;\n    }\n    if (status.running !== undefined) {\n      stateDisplay.textContent = status.running\n        ? `Connected as ${status.tailnet || \"Not connected\"}`\n        : \"Disconnected\";\n      isConnected = status.running;\n      updateSliderState();\n    }\n  }\n\n  port.onMessage.addListener((msg) => {\n    console.log(\"Received from background:\", JSON.stringify(msg));\n    if (msg.installCmd) {\n      console.log(\"Received install command\");\n      stateDisplay.innerHTML = `<b>Installation needed. Run:</b><pre>${msg.installCmd}</pre>`;\n      toggleSlider.disabled = true;\n      settingsButton.hidden = true;\n      return;\n    }\n    if (msg.error) {\n      console.log(\"Error from background:\", msg);\n      stateDisplay.textContent = msg.error;\n      toggleSlider.disabled = true;\n      settingsButton.hidden = true;\n      return;\n    }\n    if (msg.status) {\n      console.log(\"Received status update:\", msg.status);\n      updateStatus(msg.status);\n    }\n  });\n\n  toggleSlider.addEventListener(\"change\", () => {\n    console.log(\"Toggle slider changed, current state:\", isConnected);\n    chrome.runtime.sendMessage({ command: \"toggleProxy\" }, (response) => {\n      console.log(\"Received response from background:\", response);\n      if (response && response.status) {\n        updateStatus(response.status);\n      }\n    });\n    console.log(\"Sent toggleProxy command to background\");\n  });\n\n  settingsButton.addEventListener(\"click\", () => {\n    console.log(\"Settings button clicked\");\n    chrome.tabs.create({ url: \"http://100.100.100.100\" });\n  });\n\n  window.addEventListener(\"beforeunload\", () => {\n    port.disconnect();\n  });\n});\n"
  },
  {
    "path": "ts-browser-ext.go",
    "content": "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\"log/syslog\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gorilla/csrf\"\n\t\"tailscale.com/client/tailscale\"\n\t\"tailscale.com/client/web\"\n\t\"tailscale.com/hostinfo\"\n\t\"tailscale.com/ipn\"\n\t\"tailscale.com/net/proxymux\"\n\t\"tailscale.com/net/socks5\"\n\t\"tailscale.com/tsnet\"\n\t\"tailscale.com/types/logger\"\n\t\"tailscale.com/types/netmap\"\n)\n\nvar (\n\tinstallFlag   = flag.String(\"install\", \"\", \"register the browser extension; string is 'C' (Chrome) or 'F' (Firefox) followed by extension ID\")\n\tuninstallFlag = flag.Bool(\"uninstall\", false, \"unregister the browser extension\")\n)\n\nfunc main() {\n\tflag.Parse()\n\tif *installFlag != \"\" {\n\t\tif err := install(*installFlag); err != nil {\n\t\t\tlog.Fatalf(\"installation error: %v\", err)\n\t\t}\n\t\treturn\n\t}\n\tif *uninstallFlag {\n\t\tif err := uninstall(); err != nil {\n\t\t\tlog.Fatalf(\"uninstallation error: %v\", err)\n\t\t}\n\t\treturn\n\t}\n\n\tif flag.NArg() == 0 {\n\t\tfmt.Printf(`ts-browser-ext is the backend for the Tailscale browser extension,\nrunning as a child process HTTP/SOCKS5 under your browser.\n\nTo register it once, run:\n\n     $ ts-browser-ext --install=chrome\n`)\n\t\treturn\n\t}\n\n\thostinfo.SetApp(\"ts-browser-ext\")\n\n\th := newHost(os.Stdin, os.Stdout)\n\n\tif w, err := syslog.Dial(\"tcp\", \"localhost:5555\", syslog.LOG_INFO, \"browser\"); err == nil {\n\t\tlog.Printf(\"syslog dialed\")\n\t\th.logf = func(f string, a ...any) {\n\t\t\tfmt.Fprintf(w, f, a...)\n\t\t}\n\t\tlog.SetOutput(w)\n\t} else {\n\t\tlog.Printf(\"syslog: %v\", err)\n\t}\n\n\tln := h.getProxyListener()\n\tport := ln.Addr().(*net.TCPAddr).Port\n\th.logf(\"Proxy listening on localhost:%v\", port)\n\n\th.send(&reply{ProcRunning: &procRunningResult{\n\t\tPort: port,\n\t\tPid:  os.Getpid(),\n\t}})\n\th.logf(\"Starting readMessages loop\")\n\terr := h.readMessages()\n\th.logf(\"readMessage loop ended: %v\", err)\n}\n\nfunc getTargetDir(browserByte string) (string, error) {\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvar dir string\n\tswitch runtime.GOOS {\n\tcase \"linux\":\n\t\tif browserByte == \"C\" {\n\t\t\tdir = filepath.Join(home, \".config\", \"google-chrome\", \"NativeMessagingHosts\")\n\t\t} else if browserByte == \"F\" {\n\t\t\tdir = filepath.Join(home, \".mozilla\", \"native-messaging-hosts\")\n\t\t}\n\tcase \"darwin\":\n\t\tif browserByte == \"C\" {\n\t\t\tdir = filepath.Join(home, \"Library\", \"Application Support\", \"Google\", \"Chrome\", \"NativeMessagingHosts\")\n\t\t} else if browserByte == \"F\" {\n\t\t\tdir = filepath.Join(home, \"Library\", \"Application Support\", \"Mozilla\", \"NativeMessagingHosts\")\n\t\t}\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"TODO: implement support for installing on %q\", runtime.GOOS)\n\t}\n\tif err := os.MkdirAll(dir, 0755); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn dir, nil\n}\n\nfunc uninstall() error {\n\tfor _, browserByte := range []string{\"C\", \"F\"} {\n\t\ttargetDir, err := getTargetDir(browserByte)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttargetBin := filepath.Join(targetDir, \"ts-browser-ext\")\n\t\ttargetJSON := filepath.Join(targetDir, \"com.tailscale.browserext.chrome.json\")\n\t\tif browserByte == \"F\" {\n\t\t\ttargetJSON = filepath.Join(targetDir, \"com.tailscale.browserext.firefox.json\")\n\t\t}\n\t\tif err := os.Remove(targetBin); err != nil && !os.IsNotExist(err) {\n\t\t\treturn err\n\t\t}\n\t\tif err := os.Remove(targetJSON); err != nil && !os.IsNotExist(err) {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc install(installArg string) error {\n\tbrowserByte, extension := installArg[0:1], installArg[1:]\n\tswitch browserByte {\n\tcase \"C\":\n\t\textensionRE := regexp.MustCompile(`^[a-z0-9]{32}$`)\n\t\tif !extensionRE.MatchString(extension) {\n\t\t\treturn fmt.Errorf(\"invalid extension ID %q\", extension)\n\t\t}\n\tcase \"F\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unknown browser prefix byte %q\", browserByte)\n\t}\n\n\texe, err := os.Executable()\n\tif err != nil {\n\t\treturn err\n\t}\n\ttargetDir, err := getTargetDir(browserByte)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbinary, err := os.ReadFile(exe)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttargetBin := filepath.Join(targetDir, \"ts-browser-ext\")\n\tif err := os.WriteFile(targetBin, binary, 0755); err != nil {\n\t\treturn err\n\t}\n\tlog.SetFlags(0)\n\tlog.Printf(\"copied binary to %v\", targetBin)\n\n\tvar targetJSON string\n\tvar jsonConf []byte\n\n\tswitch browserByte {\n\tcase \"C\":\n\t\ttargetJSON = filepath.Join(targetDir, \"com.tailscale.browserext.chrome.json\")\n\t\tjsonConf = fmt.Appendf(nil, `{\n\t\t\"name\": \"com.tailscale.browserext.chrome\",\n\t\t\"description\": \"Tailscale Browser Extension\",\n\t\t\"path\": \"%s\",\n\t\t\"type\": \"stdio\",\n\t\t\"allowed_origins\": [\n\t\t\t\"chrome-extension://%s/\"\n\t\t]\n\t  }`, targetBin, extension)\n\tcase \"F\":\n\t\ttargetJSON = filepath.Join(targetDir, \"com.tailscale.browserext.firefox.json\")\n\t\tjsonConf = fmt.Appendf(nil, `{\n\t\t\"name\": \"com.tailscale.browserext.firefox\",\n\t\t\"description\": \"Tailscale Browser Extension\",\n\t\t\"path\": \"%s\",\n\t\t\"type\": \"stdio\",\n\t\t\"allowed_extensions\": [\n\t\t\t\"browser-ext@tailscale.com\"\n\t\t]\n\t  }`, targetBin)\n\tdefault:\n\t\treturn fmt.Errorf(\"unknown browser prefix byte %q\", browserByte)\n\t}\n\tif err := os.WriteFile(targetJSON, jsonConf, 0644); err != nil {\n\t\treturn err\n\t}\n\tlog.Printf(\"wrote registration to %v\", targetJSON)\n\treturn nil\n}\n\ntype host struct {\n\tbr   *bufio.Reader\n\tw    io.Writer\n\tlogf logger.Logf\n\n\twmu sync.Mutex // guards writing to w\n\n\tlenBuf [4]byte // owned by readMessages\n\n\tmu              sync.Mutex\n\twatchDead       bool\n\tlastNetmap      *netmap.NetworkMap\n\tlastState       ipn.State\n\tlastBrowseToURL string\n\tctx             context.Context // for IPN bus; canceled by cancelCtx\n\tcancelCtx       context.CancelFunc\n\tts              *tsnet.Server\n\tws              *web.Server\n\tln              net.Listener\n\twantUp          bool\n\t// ...\n}\n\nfunc newHost(r io.Reader, w io.Writer) *host {\n\th := &host{\n\t\tbr:   bufio.NewReaderSize(r, 1<<20),\n\t\tw:    w,\n\t\tlogf: log.Printf,\n\t}\n\th.ts = &tsnet.Server{\n\t\tRunWebClient: true,\n\n\t\t// late-binding, so caller can adjust h.logf.\n\t\tLogf: func(f string, a ...any) {\n\t\t\th.logf(f, a...)\n\t\t},\n\t}\n\treturn h\n}\n\nconst maxMsgSize = 1 << 20\n\nfunc (h *host) readMessages() error {\n\tfor {\n\t\tmsg, err := h.readMessage()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := h.handleMessage(msg); err != nil {\n\t\t\th.logf(\"error handling message %v: %v\", msg, err)\n\t\t\treturn err\n\t\t}\n\t}\n}\n\nfunc (h *host) handleMessage(msg *request) error {\n\tswitch msg.Cmd {\n\tcase CmdInit:\n\t\treturn h.handleInit(msg)\n\tcase CmdGetStatus:\n\t\th.sendStatus()\n\tcase CmdUp:\n\t\treturn h.handleUp()\n\tcase CmdDown:\n\t\treturn h.handleDown()\n\tdefault:\n\t\th.logf(\"unknown command %q\", msg.Cmd)\n\t}\n\treturn nil\n}\n\nfunc (h *host) handleUp() error {\n\treturn h.setWantRunning(true)\n}\n\nfunc (h *host) handleDown() error {\n\treturn h.setWantRunning(false)\n}\n\nfunc (h *host) setWantRunning(want bool) error {\n\tdefer h.sendStatus()\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\tif h.ts.Sys() == nil {\n\t\treturn fmt.Errorf(\"not init\")\n\t}\n\th.wantUp = want\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\tlc, err := h.ts.LocalClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := lc.EditPrefs(ctx, &ipn.MaskedPrefs{\n\t\tWantRunningSet: true,\n\t\tPrefs: ipn.Prefs{\n\t\t\tWantRunning: want,\n\t\t},\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"EditPrefs to wantRunning=%v: %w\", want, err)\n\t}\n\treturn nil\n}\n\nfunc (h *host) handleInit(msg *request) (ret error) {\n\tdefer func() {\n\t\tvar errMsg string\n\t\tif ret != nil {\n\t\t\terrMsg = ret.Error()\n\t\t}\n\t\th.send(&reply{\n\t\t\tInit: &initResult{Error: errMsg},\n\t\t})\n\t}()\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\n\tif h.cancelCtx != nil {\n\t\th.cancelCtx()\n\t}\n\th.ctx, h.cancelCtx = context.WithCancel(context.Background())\n\n\tid := msg.InitID\n\tif len(id) == 0 {\n\t\treturn fmt.Errorf(\"missing initID\")\n\t}\n\tif len(id) > 60 {\n\t\treturn fmt.Errorf(\"initID too long\")\n\t}\n\tfor i := range len(id) {\n\t\tb := id[i]\n\t\tif b == '-' || (b >= 'a' && b <= 'f') || (b >= '0' && b <= '9') {\n\t\t\tcontinue\n\t\t}\n\t\treturn errors.New(\"invalid initID character\")\n\t}\n\n\tif h.ts.Sys() != nil {\n\t\treturn fmt.Errorf(\"already running\")\n\t}\n\tu, err := user.Current()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting current user: %w\", err)\n\t}\n\th.ts.Hostname = u.Username + \"-browser-ext\"\n\n\tconfDir, err := os.UserConfigDir()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting user config dir: %w\", err)\n\t}\n\th.ts.Dir = filepath.Join(confDir, \"tailscale-browser-ext\", id)\n\n\th.logf(\"Starting...\")\n\tif err := h.ts.Start(); err != nil {\n\t\treturn fmt.Errorf(\"starting tsnet.Server: %w\", err)\n\t}\n\th.logf(\"Started\")\n\n\tlc, err := h.ts.LocalClient()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting local client: %w\", err)\n\t}\n\n\twc, err := lc.WatchIPNBus(h.ctx, ipn.NotifyInitialState|ipn.NotifyRateLimit)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"watching IPN bus: %w\", err)\n\t}\n\tgo h.watchIPNBus(wc)\n\n\th.ws, err = web.NewServer(web.ServerOpts{\n\t\tMode:        web.LoginServerMode, // TODO: manage?\n\t\tLocalClient: lc,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"NewServer: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (h *host) watchIPNBus(wc *tailscale.IPNBusWatcher) {\n\th.mu.Lock()\n\th.watchDead = false\n\th.mu.Unlock()\n\n\tfor h.updateFromWatcher(wc) {\n\t\t// Keep going.\n\t}\n}\n\nfunc (h *host) updateFromWatcher(wc *tailscale.IPNBusWatcher) bool {\n\tn, err := wc.Next()\n\n\tdefer h.sendStatus()\n\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\n\tif err != nil {\n\t\tlog.Printf(\"watchIPNBus: %v\", err)\n\t\th.watchDead = true\n\t\treturn false\n\t}\n\n\tif n.NetMap != nil {\n\t\th.lastNetmap = n.NetMap\n\t}\n\tif n.State != nil {\n\t\th.lastState = *n.State\n\t}\n\n\tif n.BrowseToURL != nil {\n\t\th.lastBrowseToURL = *n.BrowseToURL\n\t\t// TODO: pop a browser for Tailscale SSH check mode etc, even\n\t\t// if already logged in.\n\t}\n\treturn true\n}\n\nfunc (h *host) send(msg *reply) error {\n\tmsgb, err := json.Marshal(msg)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"json encoding of message: %w\", err)\n\t}\n\th.logf(\"sent reply: %s\", msgb)\n\tif len(msgb) > maxMsgSize {\n\t\treturn fmt.Errorf(\"message too big (%v)\", len(msgb))\n\t}\n\tbinary.LittleEndian.PutUint32(h.lenBuf[:], uint32(len(msgb)))\n\th.wmu.Lock()\n\tdefer h.wmu.Unlock()\n\tif _, err := h.w.Write(h.lenBuf[:]); err != nil {\n\t\treturn err\n\t}\n\tif _, err := h.w.Write(msgb); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (h *host) getProxyListener() net.Listener {\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\treturn h.getProxyListenerLocked()\n}\n\nfunc (h *host) getProxyListenerLocked() net.Listener {\n\tif h.ln != nil {\n\t\treturn h.ln\n\t}\n\tvar err error\n\th.ln, err = net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\tpanic(err) // TODO: be more graceful\n\t}\n\tsocksListener, httpListener := proxymux.SplitSOCKSAndHTTP(h.ln)\n\n\ths := &http.Server{Handler: h.httpProxyHandler()}\n\tgo func() {\n\t\tlog.Fatalf(\"HTTP proxy exited: %v\", hs.Serve(httpListener))\n\t}()\n\tss := &socks5.Server{\n\t\tLogf:   logger.WithPrefix(h.logf, \"socks5: \"),\n\t\tDialer: h.userDial,\n\t}\n\tgo func() {\n\t\tlog.Fatalf(\"SOCKS5 server exited: %v\", ss.Serve(socksListener))\n\t}()\n\treturn h.ln\n}\n\nfunc (h *host) userDial(ctx context.Context, netw, addr string) (net.Conn, error) {\n\th.mu.Lock()\n\tsys := h.ts.Sys()\n\th.mu.Unlock()\n\n\tif sys == nil {\n\t\th.logf(\"userDial to %v/%v without a tsnet.Server started\", netw, addr)\n\t\treturn nil, fmt.Errorf(\"no tsnet.Server\")\n\t}\n\n\treturn sys.Dialer.Get().UserDial(ctx, netw, addr)\n}\n\nfunc (h *host) sendStatus() {\n\tst := &status{}\n\th.mu.Lock()\n\tst.Running = h.lastState == ipn.Running\n\tif nm := h.lastNetmap; nm != nil {\n\t\tst.Tailnet = nm.Domain\n\t}\n\tif h.lastState == ipn.NeedsLogin {\n\t\tst.NeedsLogin = true\n\t\tst.BrowseToURL = h.lastBrowseToURL\n\t} else if !st.Running {\n\t\tst.Error = \"State: \" + h.lastState.String()\n\t}\n\tif h.watchDead {\n\t\tst.Error = \"WatchIPNBus stopped\"\n\t}\n\th.mu.Unlock()\n\n\tif err := h.send(&reply{Status: st}); err != nil {\n\t\th.logf(\"failed to send status: %v\", err)\n\t}\n}\n\ntype Cmd string\n\nconst (\n\tCmdInit      Cmd = \"init\"\n\tCmdUp        Cmd = \"up\"\n\tCmdDown      Cmd = \"down\"\n\tCmdGetStatus Cmd = \"get-status\"\n)\n\n// request is a message from the browser extension.\ntype request struct {\n\t// Cmd is the request type.\n\tCmd Cmd `json:\"cmd\"`\n\n\t// InitID is the unique ID made by the extension (in its local storage) to\n\t// distinguish between different browser profiles using the same extension.\n\t// A given Go process will correspond to a single browser profile.\n\t// This lets us store tsnet state in different directories.\n\t// This string, coming from JavaScript, should not be trusted. It must be\n\t// UUID-ish: hex and hyphens only, and too long.\n\tInitID string `json:\"initID,omitempty\"`\n\n\t// ...\n}\n\n// reply is a message to the browser extension.\ntype reply struct {\n\t// ProcRunning is set on the first message when the Go process starts up.\n\t// It's the message that makes the browser recognize that the native\n\t// messaging port is up.\n\tProcRunning *procRunningResult `json:\"procRunning,omitempty\"`\n\n\t// Status is sent in response to a [CmdGetStatus] [request.Cmd].\n\tStatus *status `json:\"status,omitempty\"`\n\n\tInit *initResult `json:\"init,omitempty\"`\n}\n\ntype procRunningResult struct {\n\tPort  int    `json:\"port\"` // HTTP+SOCKS5 localhost proxy port\n\tPid   int    `json:\"pid\"`\n\tError string `json:\"error\"`\n}\n\ntype initResult struct {\n\tError string `json:\"error\"` // empty for none\n}\n\ntype status struct {\n\tRunning bool   `json:\"running\"`\n\tTailnet string `json:\"tailnet\"`\n\tError   string `json:\"error,omitempty\"`\n\n\tNeedsLogin  bool   `json:\"needsLogin,omitempty\"` // true if the user needs to log in\n\tBrowseToURL string `json:\"browseToURL\"`\n}\n\nfunc (h *host) readMessage() (*request, error) {\n\tif _, err := io.ReadFull(h.br, h.lenBuf[:]); err != nil {\n\t\treturn nil, err\n\t}\n\tmsgSize := binary.LittleEndian.Uint32(h.lenBuf[:])\n\tif msgSize > maxMsgSize {\n\t\treturn nil, fmt.Errorf(\"message size too big (%v)\", msgSize)\n\t}\n\tmsgb := make([]byte, msgSize)\n\tif n, err := io.ReadFull(h.br, msgb); err != nil {\n\t\treturn nil, fmt.Errorf(\"read %v of %v bytes in message with error %v\", n, msgSize, err)\n\t}\n\tmsg := new(request)\n\tif err := json.Unmarshal(msgb, msg); err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid JSON decoding of message: %w\", err)\n\t}\n\th.logf(\"got command %q: %s\", msg.Cmd, msgb)\n\treturn msg, nil\n}\n\n// httpProxyHandler returns an HTTP proxy http.Handler using the\n// provided backend dialer.\nfunc (h *host) httpProxyHandler() http.Handler {\n\trp := &httputil.ReverseProxy{\n\t\tDirector: func(r *http.Request) {}, // no change\n\t\tTransport: &http.Transport{\n\t\t\tDialContext: h.userDial,\n\t\t},\n\t}\n\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Host == \"100.100.100.100\" {\n\t\t\th.ws.ServeHTTP(w, csrf.PlaintextHTTPRequest(r))\n\t\t\treturn\n\t\t}\n\n\t\tif r.Method != \"CONNECT\" {\n\t\t\tbackURL := r.RequestURI\n\t\t\tif strings.HasPrefix(backURL, \"/\") || backURL == \"*\" {\n\t\t\t\thttp.Error(w, \"bogus RequestURI; must be absolute URL or CONNECT\", 400)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trp.ServeHTTP(w, r)\n\t\t\treturn\n\t\t}\n\n\t\t// CONNECT support:\n\n\t\tdst := r.RequestURI\n\t\tc, err := h.userDial(r.Context(), \"tcp\", dst)\n\t\tif err != nil {\n\t\t\tw.Header().Set(\"Tailscale-Connect-Error\", err.Error())\n\t\t\thttp.Error(w, err.Error(), 500)\n\t\t\treturn\n\t\t}\n\t\tdefer c.Close()\n\n\t\tcc, ccbuf, err := w.(http.Hijacker).Hijack()\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), 500)\n\t\t\treturn\n\t\t}\n\t\tdefer cc.Close()\n\n\t\tio.WriteString(cc, \"HTTP/1.1 200 OK\\r\\n\\r\\n\")\n\n\t\tvar clientSrc io.Reader = ccbuf\n\t\tif ccbuf.Reader.Buffered() == 0 {\n\t\t\t// In the common case (with no\n\t\t\t// buffered data), read directly from\n\t\t\t// the underlying client connection to\n\t\t\t// save some memory, letting the\n\t\t\t// bufio.Reader/Writer get GC'ed.\n\t\t\tclientSrc = cc\n\t\t}\n\n\t\terrc := make(chan error, 1)\n\t\tgo func() {\n\t\t\t_, err := io.Copy(cc, c)\n\t\t\terrc <- err\n\t\t}()\n\t\tgo func() {\n\t\t\t_, err := io.Copy(c, clientSrc)\n\t\t\terrc <- err\n\t\t}()\n\t\t<-errc\n\t})\n}\n"
  }
]