mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-31 16:23:44 +00:00
cmd/ts-browser-native-ext: add start of a browser extension
Updates #14689 Change-Id: Ia432ee53dcdee9b43a73adb2ab3be6a3ce235aa6
This commit is contained in:
parent
97a44d6453
commit
5722532444
154
cmd/ts-browser-native-ext/background.js
Normal file
154
cmd/ts-browser-native-ext/background.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
// Flag to track proxy status
|
||||||
|
let proxyEnabled = false;
|
||||||
|
|
||||||
|
|
||||||
|
// Function to change the popup icon
|
||||||
|
function setPopupIcon(active) {
|
||||||
|
const iconPath = active ? "online.png" : "offline.png";
|
||||||
|
|
||||||
|
chrome.action.setIcon({ path: iconPath }, () => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
console.error("Error setting icon to " + active + ":", chrome.runtime.lastError.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to enable the proxy
|
||||||
|
function enableProxy() {
|
||||||
|
if (disconnected) {
|
||||||
|
console.error("Cannot enable proxy, disconnected from native host");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message to port
|
||||||
|
if (lastProxyPort) {
|
||||||
|
nmPort.postMessage({ cmd: "get-status" });
|
||||||
|
} else {
|
||||||
|
nmPort.postMessage({ cmd: "up" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to disable the proxy
|
||||||
|
function disableProxy() {
|
||||||
|
setProxy(0);
|
||||||
|
|
||||||
|
if (disconnected) {
|
||||||
|
console.error("Cannot disable proxy, disconnected from native host");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message to port
|
||||||
|
//nmPort.postMessage({ cmd: "down" });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("starting ts-browser-ext");
|
||||||
|
|
||||||
|
console.log("Connecting to native messaging host...");
|
||||||
|
let nmPort = chrome.runtime.connectNative("com.tailscale.browserext.chrome");
|
||||||
|
let disconnected = false;
|
||||||
|
let portError = ""; // error.message if/when nmPort disconnected
|
||||||
|
|
||||||
|
nmPort.onDisconnect.addListener(() => {
|
||||||
|
disconnected = true;
|
||||||
|
const error = chrome.runtime.lastError;
|
||||||
|
if (error) {
|
||||||
|
console.error("Connection failed:", error.message);
|
||||||
|
portError = error.message;
|
||||||
|
} else {
|
||||||
|
console.error("Disconnected from native host");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
nmPort.onMessage.addListener((message) => {
|
||||||
|
console.log("message from backend: ", message);
|
||||||
|
|
||||||
|
let st = message.status;
|
||||||
|
if (st && st.running && st.proxyPort && proxyEnabled) {
|
||||||
|
setProxy(st.proxyPort);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var lastProxyPort = 0;
|
||||||
|
|
||||||
|
function setProxy(proxyPort) {
|
||||||
|
if (proxyPort) {
|
||||||
|
lastProxyPort = proxyPort;
|
||||||
|
console.log("Enabling proxy at port: " + proxyPort);
|
||||||
|
} else {
|
||||||
|
console.log("Disabling proxy...");
|
||||||
|
chrome.proxy.settings.set(
|
||||||
|
{
|
||||||
|
value: {
|
||||||
|
mode: "direct"
|
||||||
|
},
|
||||||
|
scope: "regular"
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
console.log("Proxy disabled.");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chrome.proxy.settings.set(
|
||||||
|
{
|
||||||
|
value: {
|
||||||
|
mode: "fixed_servers",
|
||||||
|
rules: {
|
||||||
|
singleProxy: {
|
||||||
|
scheme: "http",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: proxyPort
|
||||||
|
},
|
||||||
|
bypassList: ["<local>"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scope: "regular"
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
console.log("Proxy enabled: 127.0.0.1:" + proxyPort);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.storage.local.get("profileId", (result) => {
|
||||||
|
if (!result.profileId) {
|
||||||
|
const profileId = crypto.randomUUID();
|
||||||
|
chrome.storage.local.set({ profileId }, () => {
|
||||||
|
console.log("Generated profile ID:", profileId);
|
||||||
|
nmPort.postMessage({ cmd: "init", initID: profileId });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Profile ID already exists:", result.profileId);
|
||||||
|
nmPort.postMessage({ cmd: "init", initID: result.profileId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Listener for messages from the popup
|
||||||
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
if (message.command === "queryState") {
|
||||||
|
if (disconnected) {
|
||||||
|
sendResponse({ status: "Error", error: portError });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("bg: queryState, proxy=" + proxyEnabled);
|
||||||
|
sendResponse({ status: proxyEnabled ? "Connected" : "Disconnected" });
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.command === "toggleProxy") {
|
||||||
|
console.log("bg: toggleProxy, proxy=" + proxyEnabled);
|
||||||
|
proxyEnabled = !proxyEnabled;
|
||||||
|
if (proxyEnabled) {
|
||||||
|
enableProxy();
|
||||||
|
console.log("bg: toggleProxy on, now proxy=" + proxyEnabled);
|
||||||
|
sendResponse({ status: "Connected" });
|
||||||
|
console.log("bg: toggleProxy on, sent proxy=" + proxyEnabled);
|
||||||
|
} else {
|
||||||
|
disableProxy();
|
||||||
|
console.log("bg: toggleProxy off, now proxy=" + proxyEnabled);
|
||||||
|
sendResponse({ status: "Disconnected" });
|
||||||
|
console.log("bg: toggleProxy off, sent proxy=" + proxyEnabled);
|
||||||
|
}
|
||||||
|
setPopupIcon(proxyEnabled);
|
||||||
|
}
|
||||||
|
});
|
13
cmd/ts-browser-native-ext/chrome.txt
Normal file
13
cmd/ts-browser-native-ext/chrome.txt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
% pwd
|
||||||
|
/Users/bradfitz/Library/Application Support/Google/Chrome/NativeMessagingHosts
|
||||||
|
|
||||||
|
% cat com.tailscale.chrome-ext.json
|
||||||
|
{
|
||||||
|
"name": "com.tailscale.chrome-ext",
|
||||||
|
"description": "Tailscale Native Extension",
|
||||||
|
"path": "/Users/bradfitz/go/bin/ts-browser-native-ext",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": [
|
||||||
|
"chrome-extension://gdopnimobeboikkiagbnnbcijkjdjcad/"
|
||||||
|
]
|
||||||
|
}
|
BIN
cmd/ts-browser-native-ext/icon.png
Normal file
BIN
cmd/ts-browser-native-ext/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
23
cmd/ts-browser-native-ext/manifest.json
Normal file
23
cmd/ts-browser-native-ext/manifest.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Tailscale Extension",
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "A Tailscale client that runs as a browser extension, permitting use of different tailnets in differenet browser profiles, without affecting the system VPN or networking settings.",
|
||||||
|
"permissions": [
|
||||||
|
"proxy",
|
||||||
|
"background",
|
||||||
|
"storage",
|
||||||
|
"nativeMessaging"
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_icon": "icon.png"
|
||||||
|
},
|
||||||
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArAPB7I6tL6JJaivgzHLpDOmawSn4q8K4riQPWtXPL8N2ashAiGbsOuNW+7zJQUg+So1C/J2M32Wa1RzHExA/Gj4hekBjZvjY0zylTXQgnDJ/RVrQEENVq02Pfi5OpplIDwN5Yt7n8JQbYZP9NkOUUoumh0BFm4WLLal4GLt1S6QrwDctc1kxG1UKtcVgGi40aPz0efB0skn7lw1jzN2WGenqNY1x2BFQj/ol3zUMasb4rO3EdJWfD3kyjfDu5K4MvX4GZ3Stw3u25Z9cfNf6W1StrA/06JcYc/9AAjrfHjxrZGpDBGeKUe1KgU7iMX1J9SkaPooYJJbuiA1AdgTr9QIDAQAB"
|
||||||
|
}
|
BIN
cmd/ts-browser-native-ext/offline.png
Normal file
BIN
cmd/ts-browser-native-ext/offline.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
BIN
cmd/ts-browser-native-ext/online.png
Normal file
BIN
cmd/ts-browser-native-ext/online.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
13
cmd/ts-browser-native-ext/popup.html
Normal file
13
cmd/ts-browser-native-ext/popup.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Proxy Toggle</title>
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>Tailscale</h1>
|
||||||
|
<div id='state'></div>
|
||||||
|
<button id="button">Connect</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
cmd/ts-browser-native-ext/popup.js
Normal file
16
cmd/ts-browser-native-ext/popup.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
let btn = document.getElementById("button");
|
||||||
|
let st = document.getElementById("state");
|
||||||
|
|
||||||
|
let onState = (response) => {
|
||||||
|
console.log("popup: onState=" + response.status);
|
||||||
|
st.innerText = response.status;
|
||||||
|
btn.innerText = response.status === "Connected" ? "Disconnect" : "Connect";
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage({ command: "queryState" }, onState);
|
||||||
|
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
chrome.runtime.sendMessage({ command: "toggleProxy" }, onState);
|
||||||
|
});
|
||||||
|
})
|
488
cmd/ts-browser-native-ext/ts-browser-native-ext.go
Normal file
488
cmd/ts-browser-native-ext/ts-browser-native-ext.go
Normal file
@ -0,0 +1,488 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"log/syslog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/hostinfo"
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/net/proxymux"
|
||||||
|
"tailscale.com/net/socks5"
|
||||||
|
"tailscale.com/tsnet"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
installFlag = flag.String("install", "", "register the browser extension's backend with the given browser, one of: chrome, firefox")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
if *installFlag != "" {
|
||||||
|
if err := install(*installFlag); err != nil {
|
||||||
|
log.Fatalf("installation error: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if flag.NArg() == 0 {
|
||||||
|
fmt.Printf(`ts-browser-native-ext is the backend for the Tailscale browser extension,
|
||||||
|
run as a child process under your browser.
|
||||||
|
|
||||||
|
To register it once, run:
|
||||||
|
|
||||||
|
$ ts-browser-native-ext --install=chrome
|
||||||
|
`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hostinfo.SetApp("ts-browser-native-ext")
|
||||||
|
|
||||||
|
h := newHost(os.Stdin, os.Stdout)
|
||||||
|
|
||||||
|
if w, err := syslog.Dial("tcp", "localhost:5555", syslog.LOG_INFO, "browser"); err == nil {
|
||||||
|
log.Printf("syslog dialed")
|
||||||
|
h.logf = func(f string, a ...any) {
|
||||||
|
fmt.Fprintf(w, f, a...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("syslog: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logf("Starting readMessages loop")
|
||||||
|
err := h.readMessages()
|
||||||
|
h.logf("readMessage loop ended: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTargetDir(browser string) (string, error) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var dir string
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "darwin":
|
||||||
|
dir = filepath.Join(home, "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts")
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("TODO: implement support for installing on %q", runtime.GOOS)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return dir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func install(browser string) error {
|
||||||
|
switch browser {
|
||||||
|
case "chrome":
|
||||||
|
case "firefox":
|
||||||
|
return errors.New("TODO: firefox")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown browser %q", browser)
|
||||||
|
}
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
targetDir, err := getTargetDir(browser)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
binary, err := os.ReadFile(exe)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
targetBin := filepath.Join(targetDir, "ts-browser-native-ext")
|
||||||
|
targetJSON := filepath.Join(targetDir, "com.tailscale.browserext.chrome.json")
|
||||||
|
if err := os.WriteFile(targetBin, binary, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.SetFlags(0)
|
||||||
|
log.Printf("copied binary to %v", targetBin)
|
||||||
|
jsonConf := fmt.Appendf(nil, `{
|
||||||
|
"name": "com.tailscale.browserext.chrome",
|
||||||
|
"description": "Tailscale Native Extension",
|
||||||
|
"path": "%s",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": [
|
||||||
|
"chrome-extension://mldijmhffomelkfhfjcjekhjgaikhood/"
|
||||||
|
]
|
||||||
|
}`, targetBin)
|
||||||
|
if err := os.WriteFile(targetJSON, jsonConf, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("wrote registration to %v", targetJSON)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type host struct {
|
||||||
|
br *bufio.Reader
|
||||||
|
w io.Writer
|
||||||
|
logf logger.Logf
|
||||||
|
|
||||||
|
wmu sync.Mutex // guards writing to w
|
||||||
|
|
||||||
|
lenBuf [4]byte // owned by readMessages
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
ts *tsnet.Server
|
||||||
|
ln net.Listener
|
||||||
|
wantUp bool
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHost(r io.Reader, w io.Writer) *host {
|
||||||
|
h := &host{
|
||||||
|
br: bufio.NewReaderSize(r, 1<<20),
|
||||||
|
w: w,
|
||||||
|
logf: log.Printf,
|
||||||
|
}
|
||||||
|
h.ts = &tsnet.Server{
|
||||||
|
RunWebClient: true,
|
||||||
|
|
||||||
|
// late-binding, so caller can adjust h.logf.
|
||||||
|
Logf: func(f string, a ...any) {
|
||||||
|
h.logf(f, a...)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxMsgSize = 1 << 20
|
||||||
|
|
||||||
|
func (h *host) readMessages() error {
|
||||||
|
for {
|
||||||
|
msg, err := h.readMessage()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := h.handleMessage(msg); err != nil {
|
||||||
|
h.logf("error handling message %v: %v", msg, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *host) handleMessage(msg *request) error {
|
||||||
|
switch msg.Cmd {
|
||||||
|
case CmdInit:
|
||||||
|
return h.handleInit(msg)
|
||||||
|
case CmdGetStatus:
|
||||||
|
h.sendStatus()
|
||||||
|
case CmdUp:
|
||||||
|
return h.handleUp()
|
||||||
|
case CmdDown:
|
||||||
|
return h.handleDown()
|
||||||
|
default:
|
||||||
|
h.logf("unknown command %q", msg.Cmd)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *host) handleUp() error {
|
||||||
|
return h.setWantRunning(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *host) handleDown() error {
|
||||||
|
return h.setWantRunning(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *host) setWantRunning(want bool) error {
|
||||||
|
defer h.sendStatus()
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
if h.ts.Sys() == nil {
|
||||||
|
return fmt.Errorf("not init")
|
||||||
|
}
|
||||||
|
h.wantUp = want
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
lc, err := h.ts.LocalClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := lc.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||||
|
WantRunningSet: true,
|
||||||
|
Prefs: ipn.Prefs{
|
||||||
|
WantRunning: want,
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("EditPrefs to wantRunning=%v: %w", want, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *host) handleInit(msg *request) (ret error) {
|
||||||
|
defer func() {
|
||||||
|
var errMsg string
|
||||||
|
if ret != nil {
|
||||||
|
errMsg = ret.Error()
|
||||||
|
}
|
||||||
|
h.send(&reply{
|
||||||
|
Init: &initResult{Error: errMsg},
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
id := msg.InitID
|
||||||
|
if len(id) == 0 {
|
||||||
|
return fmt.Errorf("missing initID")
|
||||||
|
}
|
||||||
|
if len(id) > 60 {
|
||||||
|
return fmt.Errorf("initID too long")
|
||||||
|
}
|
||||||
|
for i := range len(id) {
|
||||||
|
b := id[i]
|
||||||
|
if b == '-' || (b >= 'a' && b <= 'f') || (b >= '0' && b <= '9') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return errors.New("invalid initID character")
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.ts.Sys() != nil {
|
||||||
|
return fmt.Errorf("already running")
|
||||||
|
}
|
||||||
|
u, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting current user: %w", err)
|
||||||
|
}
|
||||||
|
h.ts.Hostname = u.Username + "-browser-ext"
|
||||||
|
|
||||||
|
confDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting user config dir: %w", err)
|
||||||
|
}
|
||||||
|
h.ts.Dir = filepath.Join(confDir, "tailscale-browser-ext", id)
|
||||||
|
|
||||||
|
h.logf("Starting...")
|
||||||
|
if err := h.ts.Start(); err != nil {
|
||||||
|
return fmt.Errorf("starting tsnet.Server: %w", err)
|
||||||
|
}
|
||||||
|
h.logf("Started")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *host) send(msg *reply) error {
|
||||||
|
msgb, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("json encoding of message: %w", err)
|
||||||
|
}
|
||||||
|
h.logf("sent reply: %s", msgb)
|
||||||
|
if len(msgb) > maxMsgSize {
|
||||||
|
return fmt.Errorf("message too big (%v)", len(msgb))
|
||||||
|
}
|
||||||
|
binary.LittleEndian.PutUint32(h.lenBuf[:], uint32(len(msgb)))
|
||||||
|
h.wmu.Lock()
|
||||||
|
defer h.wmu.Unlock()
|
||||||
|
if _, err := h.w.Write(h.lenBuf[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := h.w.Write(msgb); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *host) getProxyListener() net.Listener {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
return h.getProxyListenerLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *host) getProxyListenerLocked() net.Listener {
|
||||||
|
if h.ln != nil {
|
||||||
|
return h.ln
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
h.ln, err = net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // TODO: be more graceful
|
||||||
|
}
|
||||||
|
socksListener, httpListener := proxymux.SplitSOCKSAndHTTP(h.ln)
|
||||||
|
|
||||||
|
hs := &http.Server{Handler: httpProxyHandler(h.userDial)}
|
||||||
|
go func() {
|
||||||
|
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpListener))
|
||||||
|
}()
|
||||||
|
ss := &socks5.Server{
|
||||||
|
Logf: logger.WithPrefix(h.logf, "socks5: "),
|
||||||
|
Dialer: h.userDial,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener))
|
||||||
|
}()
|
||||||
|
return h.ln
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *host) userDial(ctx context.Context, netw, addr string) (net.Conn, error) {
|
||||||
|
h.mu.Lock()
|
||||||
|
sys := h.ts.Sys()
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
if sys == nil {
|
||||||
|
h.logf("userDial to %v/%v without a tsnet.Server started", netw, addr)
|
||||||
|
return nil, fmt.Errorf("no tsnet.Server")
|
||||||
|
}
|
||||||
|
return sys.Dialer.Get().UserDial(ctx, netw, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *host) sendStatus() {
|
||||||
|
h.mu.Lock()
|
||||||
|
wantUp := h.wantUp
|
||||||
|
ln := h.getProxyListenerLocked()
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
if err := h.send(&reply{
|
||||||
|
Status: &status{
|
||||||
|
Running: wantUp,
|
||||||
|
ProxyPort: ln.Addr().(*net.TCPAddr).Port,
|
||||||
|
ProxyURL: "http://" + ln.Addr().String(),
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
h.logf("failed to send status: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cmd string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CmdInit Cmd = "init"
|
||||||
|
CmdUp Cmd = "up"
|
||||||
|
CmdDown Cmd = "down"
|
||||||
|
CmdGetStatus Cmd = "get-status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// request is a message from the browser extension.
|
||||||
|
type request struct {
|
||||||
|
// Cmd is the request type.
|
||||||
|
Cmd Cmd `json:"cmd"`
|
||||||
|
|
||||||
|
// InitID is the unique ID made by the extension (in its local storage) to
|
||||||
|
// distinguish between different browser profiles using the same extension.
|
||||||
|
// A given Go process will correspond to a single browser profile.
|
||||||
|
// This lets us store tsnet state in different directories.
|
||||||
|
// This string, coming from JavaScript, should not be trusted. It must be
|
||||||
|
// UUID-ish: hex and hyphens only, and too long.
|
||||||
|
InitID string `json:"initID,omitempty"`
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// reply is a message to the browser extension.
|
||||||
|
type reply struct {
|
||||||
|
Status *status `json:"status,omitempty"`
|
||||||
|
Init *initResult `json:"init,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type initResult struct {
|
||||||
|
Error string `json:"error"` // empty for none
|
||||||
|
}
|
||||||
|
|
||||||
|
type status struct {
|
||||||
|
ProxyPort int `json:"proxyPort"`
|
||||||
|
ProxyURL string `json:"proxyURL"`
|
||||||
|
Running bool `json:"running"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *host) readMessage() (*request, error) {
|
||||||
|
if _, err := io.ReadFull(h.br, h.lenBuf[:]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
msgSize := binary.LittleEndian.Uint32(h.lenBuf[:])
|
||||||
|
if msgSize > maxMsgSize {
|
||||||
|
return nil, fmt.Errorf("message size too big (%v)", msgSize)
|
||||||
|
}
|
||||||
|
msgb := make([]byte, msgSize)
|
||||||
|
if n, err := io.ReadFull(h.br, msgb); err != nil {
|
||||||
|
return nil, fmt.Errorf("read %v of %v bytes in message with error %v", n, msgSize, err)
|
||||||
|
}
|
||||||
|
msg := new(request)
|
||||||
|
if err := json.Unmarshal(msgb, msg); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid JSON decoding of message: %w", err)
|
||||||
|
}
|
||||||
|
h.logf("got command %q: %s", msg.Cmd, msgb)
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// httpProxyHandler returns an HTTP proxy http.Handler using the
|
||||||
|
// provided backend dialer.
|
||||||
|
func httpProxyHandler(dialer func(ctx context.Context, netw, addr string) (net.Conn, error)) http.Handler {
|
||||||
|
rp := &httputil.ReverseProxy{
|
||||||
|
Director: func(r *http.Request) {}, // no change
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: dialer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "CONNECT" {
|
||||||
|
backURL := r.RequestURI
|
||||||
|
if strings.HasPrefix(backURL, "/") || backURL == "*" {
|
||||||
|
http.Error(w, "bogus RequestURI; must be absolute URL or CONNECT", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rp.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CONNECT support:
|
||||||
|
|
||||||
|
dst := r.RequestURI
|
||||||
|
c, err := dialer(r.Context(), "tcp", dst)
|
||||||
|
if err != nil {
|
||||||
|
w.Header().Set("Tailscale-Connect-Error", err.Error())
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
cc, ccbuf, err := w.(http.Hijacker).Hijack()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer cc.Close()
|
||||||
|
|
||||||
|
io.WriteString(cc, "HTTP/1.1 200 OK\r\n\r\n")
|
||||||
|
|
||||||
|
var clientSrc io.Reader = ccbuf
|
||||||
|
if ccbuf.Reader.Buffered() == 0 {
|
||||||
|
// In the common case (with no
|
||||||
|
// buffered data), read directly from
|
||||||
|
// the underlying client connection to
|
||||||
|
// save some memory, letting the
|
||||||
|
// bufio.Reader/Writer get GC'ed.
|
||||||
|
clientSrc = cc
|
||||||
|
}
|
||||||
|
|
||||||
|
errc := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(cc, c)
|
||||||
|
errc <- err
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(c, clientSrc)
|
||||||
|
errc <- err
|
||||||
|
}()
|
||||||
|
<-errc
|
||||||
|
})
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user