feat: add tsrun bootstrap service and deploy
Deploy tsrun / deploy (push) Failing after 15s

This commit is contained in:
2026-07-22 06:23:21 +00:00 Unverified
commit e588b9a527
35 changed files with 2276 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
.direnv/
.envrc
.git/
secrets/tsrun.manifest.json.example
last.log
tsrun
tsrun.exe
tsrun-upx.exe
tsrun-linux
tsrun-linux-upx
result
result-*
+36
View File
@@ -0,0 +1,36 @@
name: Deploy tsrun
on:
push:
branches: [main]
jobs:
deploy:
steps:
- uses: actions/checkout@v4
- name: Write embedded manifest
run: |
mkdir -p secrets
printf '%s' '${{ secrets.TSRUN_MANIFEST_JSON }}' > secrets/tsrun.manifest.json
- name: Login to registry
run: |
printf '%s' '${{ secrets.REGISTRY_PASSWORD }}' | docker login git.astrxl.dev -u '${{ secrets.REGISTRY_USERNAME }}' --password-stdin
- name: Build and push
run: |
TAG=${{ gitea.sha }}
IMAGE=git.astrxl.dev/astral/tsrun
docker buildx create --use --name tsrun-bootstrap-builder || docker buildx use tsrun-bootstrap-builder
docker buildx build --platform linux/amd64 -t $IMAGE:$TAG -t $IMAGE:latest --push .
- name: Configure kubectl
run: |
mkdir -p ~/.kube
printf '%s' '${{ secrets.KUBECONFIG_B64 }}' | base64 -d > ~/.kube/config
- name: Rollout
run: |
kubectl set image deployment/tsrun tsrun=git.astrxl.dev/astral/tsrun:${{ gitea.sha }} -n default
kubectl rollout status deployment/tsrun -n default --timeout=180s
+15
View File
@@ -0,0 +1,15 @@
.direnv/
.envrc
secrets/tsrun.manifest.json
internal/embedded/blob_gen.go
internal/runtime/full/wintun.dll
server.js
node_modules/
last.log
tsrun
tsrun.exe
tsrun-upx.exe
tsrun-linux
tsrun-linux-upx
result
result-*
+32
View File
@@ -0,0 +1,32 @@
FROM golang:1.26-alpine AS go-builder
WORKDIR /app
RUN apk add --no-cache bash curl unzip upx
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN test -f secrets/tsrun.manifest.json
RUN curl -fsSL "https://www.wintun.net/builds/wintun-0.14.1.zip" -o /tmp/wintun.zip \
&& unzip -p /tmp/wintun.zip wintun/bin/amd64/wintun.dll > internal/runtime/full/wintun.dll
RUN go run ./cmd/buildblob -in secrets/tsrun.manifest.json -out internal/embedded/blob_gen.go
RUN GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w -buildid=' -o /out/tsrun-linux ./cmd/tsrun
RUN GOOS=windows GOARCH=amd64 go build -trimpath -ldflags='-s -w -buildid=' -o /out/tsrun.exe ./cmd/tsrun
RUN cp /out/tsrun-linux /out/tsrun-linux-upx && upx --best --lzma /out/tsrun-linux-upx
RUN cp /out/tsrun.exe /out/tsrun-upx.exe && upx --best --lzma /out/tsrun-upx.exe
FROM oven/bun:1.3.14-alpine AS bun-builder
WORKDIR /app
COPY server ./server
RUN bun build --target bun --outfile /out/server.js ./server/index.ts
FROM oven/bun:1.3.14-alpine
WORKDIR /app
COPY --from=bun-builder /out/server.js ./server.js
COPY --from=go-builder /out/tsrun-linux ./dist/tsrun-linux
COPY --from=go-builder /out/tsrun-linux-upx ./dist/tsrun-linux-upx
COPY --from=go-builder /out/tsrun.exe ./dist/tsrun.exe
COPY --from=go-builder /out/tsrun-upx.exe ./dist/tsrun-upx.exe
EXPOSE 3000
CMD ["bun", "./server.js"]
+79
View File
@@ -0,0 +1,79 @@
# tsrun
Single-binary temporary Tailscale client.
## Build-time manifest
Use the build script. If `secrets/tsrun.manifest.json` does not exist yet, the
script will copy the example manifest into place, stop, and tell you to fill in
your real credentials before rerunning it.
```bash
./build-windows.sh
```
Manual blob generation is still available if needed:
```bash
go run ./cmd/buildblob -in secrets/tsrun.manifest.json -out internal/embedded/blob_gen.go
```
For Windows full-client mode, the build script will download `wintun.dll` once
and reuse it on later runs. Manual placement is still supported at:
```text
internal/runtime/full/wintun.dll
```
It will be embedded into `tsrun.exe` and extracted at runtime automatically.
## Run
```bash
go run ./cmd/tsrun
go run ./cmd/tsrun --proxy
go run ./cmd/tsrun clear
```
Default mode now supports:
- Windows: elevated Administrator shell required
- Linux: root required
`--proxy` remains the low-privilege fallback.
`clear` removes Tailscale DNS/router state and attempts to delete the local
TUN/interface state left behind by a bad run.
## Bootstrap Server
The repo also contains a Bun bootstrap server that serves the built artifacts:
1. `/` auto-detect bootstrap script
2. `/linux` Linux UPX binary
3. `/windows` Windows UPX binary
4. `/linux-raw` raw Linux binary
5. `/windows-raw` raw Windows binary
Builds and deploys follow the GitOps Bun service pattern from `deploy-docs`:
1. multi-stage Dockerfile
2. `.gitea/workflows/deploy.yml`
3. `k8s/` deployment, service, ingress, kustomization
Current deployment targets:
1. image: `git.astrxl.dev/astral/tsrun`
2. namespace: `default`
3. ingress host: `tsr.dgnr.us`
## Linux release build
```bash
./build-linux.sh
```
## Windows release build
```bash
./build-windows.sh
```
Executable
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
MANIFEST_PATH="$ROOT_DIR/secrets/tsrun.manifest.json"
EXAMPLE_MANIFEST_PATH="$ROOT_DIR/secrets/tsrun.manifest.json.example"
LINUX_OUTPUT="$ROOT_DIR/tsrun-linux"
LINUX_UPX_OUTPUT="$ROOT_DIR/tsrun-linux-upx"
INSIDE_NIX_FLAG="${1:-}"
if [[ "$INSIDE_NIX_FLAG" != "--inside-nix" ]]; then
exec nix develop -c bash "$0" --inside-nix
fi
ensure_manifest() {
if [[ ! -f "$MANIFEST_PATH" ]]; then
cp "$EXAMPLE_MANIFEST_PATH" "$MANIFEST_PATH"
printf 'Created %s from the example manifest.\n' "$MANIFEST_PATH"
printf 'Fill in your OAuth credentials and user passwords, then rerun ./build-linux.sh\n'
exit 1
fi
if grep -q 'tskey-client-xxxxxxxx\|replace-me' "$MANIFEST_PATH"; then
printf 'Edit %s with your real OAuth credentials and user passwords, then rerun ./build-linux.sh\n' "$MANIFEST_PATH"
exit 1
fi
}
build() {
printf 'Generating embedded credential blob...\n'
go run ./cmd/buildblob -in "$MANIFEST_PATH" -out internal/embedded/blob_gen.go
printf 'Building tsrun-linux...\n'
env GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w -buildid=' -o "$LINUX_OUTPUT" ./cmd/tsrun
if command -v upx >/dev/null 2>&1; then
printf 'Building tsrun-linux-upx...\n'
cp "$LINUX_OUTPUT" "$LINUX_UPX_OUTPUT"
upx --best --lzma "$LINUX_UPX_OUTPUT"
else
printf 'Skipping UPX build because upx is not available.\n'
fi
printf 'Build complete:\n'
printf ' %s\n' "$LINUX_OUTPUT"
if [[ -f "$LINUX_UPX_OUTPUT" ]]; then
printf ' %s\n' "$LINUX_UPX_OUTPUT"
fi
}
ensure_manifest
build
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
MANIFEST_PATH="$ROOT_DIR/secrets/tsrun.manifest.json"
EXAMPLE_MANIFEST_PATH="$ROOT_DIR/secrets/tsrun.manifest.json.example"
WINTUN_DLL_PATH="$ROOT_DIR/internal/runtime/full/wintun.dll"
WINTUN_ZIP_PATH="/tmp/opencode/wintun-0.14.1.zip"
WINDOWS_OUTPUT="$ROOT_DIR/tsrun.exe"
WINDOWS_UPX_OUTPUT="$ROOT_DIR/tsrun-upx.exe"
INSIDE_NIX_FLAG="${1:-}"
if [[ "$INSIDE_NIX_FLAG" != "--inside-nix" ]]; then
exec nix develop -c bash "$0" --inside-nix
fi
ensure_manifest() {
if [[ ! -f "$MANIFEST_PATH" ]]; then
cp "$EXAMPLE_MANIFEST_PATH" "$MANIFEST_PATH"
printf 'Created %s from the example manifest.\n' "$MANIFEST_PATH"
printf 'Fill in your OAuth credentials and user passwords, then rerun ./build-windows.sh\n'
exit 1
fi
if grep -q 'tskey-client-xxxxxxxx\|replace-me' "$MANIFEST_PATH"; then
printf 'Edit %s with your real OAuth credentials and user passwords, then rerun ./build-windows.sh\n' "$MANIFEST_PATH"
exit 1
fi
}
ensure_wintun() {
if [[ -f "$WINTUN_DLL_PATH" ]]; then
printf 'Using existing %s\n' "$WINTUN_DLL_PATH"
return
fi
printf 'Downloading wintun.dll...\n'
curl -L "https://www.wintun.net/builds/wintun-0.14.1.zip" -o "$WINTUN_ZIP_PATH"
unzip -p "$WINTUN_ZIP_PATH" wintun/bin/amd64/wintun.dll > "$WINTUN_DLL_PATH"
}
build() {
printf 'Generating embedded credential blob...\n'
go run ./cmd/buildblob -in "$MANIFEST_PATH" -out internal/embedded/blob_gen.go
printf 'Building tsrun.exe...\n'
env GOOS=windows GOARCH=amd64 go build -trimpath -ldflags='-s -w -buildid=' -o "$WINDOWS_OUTPUT" ./cmd/tsrun
if command -v upx >/dev/null 2>&1; then
printf 'Building tsrun-upx.exe...\n'
cp "$WINDOWS_OUTPUT" "$WINDOWS_UPX_OUTPUT"
upx --best --lzma "$WINDOWS_UPX_OUTPUT"
else
printf 'Skipping UPX build because upx is not available.\n'
fi
printf 'Build complete:\n'
printf ' %s\n' "$WINDOWS_OUTPUT"
if [[ -f "$WINDOWS_UPX_OUTPUT" ]]; then
printf ' %s\n' "$WINDOWS_UPX_OUTPUT"
fi
}
ensure_manifest
ensure_wintun
build
+47
View File
@@ -0,0 +1,47 @@
package main
import (
"encoding/base64"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"tsrun/internal/buildblob"
)
func main() {
inPath := flag.String("in", "secrets/tsrun.manifest.json", "manifest path")
outPath := flag.String("out", "internal/embedded/blob_gen.go", "generated Go output path")
flag.Parse()
blob, err := buildblob.BuildFromFile(*inPath)
if err != nil {
fail(err)
}
encoded := base64.StdEncoding.EncodeToString(blob)
content := strings.Builder{}
content.WriteString("package embedded\n\n")
content.WriteString("import \"encoding/base64\"\n\n")
content.WriteString("func init() {\n")
content.WriteString(fmt.Sprintf("\tdata, err := base64.StdEncoding.DecodeString(%q)\n", encoded))
content.WriteString("\tif err != nil {\n")
content.WriteString("\t\tpanic(err)\n")
content.WriteString("\t}\n")
content.WriteString("\tBlob = data\n")
content.WriteString("}\n")
if err := os.MkdirAll(filepath.Dir(*outPath), 0o755); err != nil {
fail(err)
}
if err := os.WriteFile(*outPath, []byte(content.String()), 0o600); err != nil {
fail(err)
}
}
func fail(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
Generated
+61
View File
@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1784497964,
"narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+28
View File
@@ -0,0 +1,28 @@
{
description = "tsrun development environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
in {
devShells.default = pkgs.mkShell {
packages = with pkgs; [
bun
go
gopls
go-tools
golangci-lint
curl
unzip
upx
nixfmt
];
};
});
}
+83
View File
@@ -0,0 +1,83 @@
module tsrun
go 1.26.4
require (
golang.org/x/crypto v0.52.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.45.0
golang.org/x/term v0.43.0
tailscale.com v1.100.0
)
require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.5 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/creachadair/msync v0.7.1 // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gaissmai/bart v0.26.1 // indirect
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-tpm v0.9.4 // indirect
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/illarion/gonotify/v3 v3.0.2 // indirect
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
github.com/mdlayher/sdnotify v1.0.0 // indirect
github.com/mdlayher/socket v0.5.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/safchain/ethtool v0.3.0 // indirect
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd // indirect
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect
)
+254
View File
@@ -0,0 +1,254 @@
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q=
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02 h1:bXAPYSbdYbS5VTy92NIUbeDI1qyggi+JYh5op9IFlcQ=
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/creachadair/mds v0.25.9 h1:080Hr8laN2h+l3NeVCGMBpXtIPnl9mz8e4HLraGPqtA=
github.com/creachadair/mds v0.25.9/go.mod h1:4hatI3hRM+qhzuAmqPRFvaBM8mONkS7nsLxkcuTYUIs=
github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7uho=
github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008=
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8=
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao=
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689 h1:0psnKZ+N2IP43/SZC8SKx6OpFJwLmQb9m9QyV9BC2f8=
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689/go.mod h1:OGmRfY/9QEK2P5zCRtmqfbCF283xPkU2dvVA4MvbvpI=
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I=
github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE=
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc=
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89/go.mod h1:wn16Km1EZOX4UEAyaZa3dBwfFGOJ7neck40NcwosJUw=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM=
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad h1:Ky26FR5yZ5IKEB0xtm5A8xSTb06ImY7kxBFrvgOmJSg=
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg=
github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 h1:Zy8IV/+FMLxy6j6p87vk/vQGKcdnbprwjTxc8UiUtsA=
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
tailscale.com v1.100.0 h1:nm/M/dEaW9RaRsGUjW2HsSDpsZ60Jwd9k4gNW9tTFiE=
tailscale.com v1.100.0/go.mod h1:DQ9YBy85DpNlSyeU2XRIWzbAu3RsGp/frv+Khg57meE=
+132
View File
@@ -0,0 +1,132 @@
package buildblob
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"os"
"golang.org/x/crypto/argon2"
)
const (
keyLen = 32
defaultTime = 3
defaultMem = 64 * 1024
defaultCPU = 4
)
type Manifest struct {
OAuthClientID string `json:"oauthClientID"`
OAuthClientSecret string `json:"oauthClientSecret"`
Tags []string `json:"tags"`
Users []ManifestUser `json:"users"`
}
type ManifestUser struct {
Username string `json:"username"`
Password string `json:"password"`
}
type Blob struct {
Version int `json:"version"`
Users []BlobUser `json:"users"`
KDF KDFParams `json:"kdf"`
}
type BlobUser struct {
Username string `json:"username"`
Salt []byte `json:"salt"`
Nonce []byte `json:"nonce"`
Ciphertext []byte `json:"ciphertext"`
}
type KDFParams struct {
Time uint32 `json:"time"`
Memory uint32 `json:"memory"`
Threads uint8 `json:"threads"`
KeyLen uint32 `json:"keyLen"`
}
type payload struct {
OAuthClientID string `json:"oauthClientID"`
OAuthClientSecret string `json:"oauthClientSecret"`
Tags []string `json:"tags"`
}
func BuildFromFile(path string) ([]byte, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var manifest Manifest
if err := json.Unmarshal(raw, &manifest); err != nil {
return nil, err
}
return Build(manifest)
}
func Build(manifest Manifest) ([]byte, error) {
if manifest.OAuthClientID == "" || manifest.OAuthClientSecret == "" {
return nil, errors.New("manifest must include oauthClientID and oauthClientSecret")
}
if len(manifest.Users) == 0 {
return nil, errors.New("manifest must include at least one user")
}
pt, err := json.Marshal(payload{
OAuthClientID: manifest.OAuthClientID,
OAuthClientSecret: manifest.OAuthClientSecret,
Tags: manifest.Tags,
})
if err != nil {
return nil, err
}
params := KDFParams{Time: defaultTime, Memory: defaultMem, Threads: defaultCPU, KeyLen: keyLen}
blob := Blob{Version: 1, KDF: params}
seen := map[string]struct{}{}
for _, user := range manifest.Users {
if user.Username == "" || user.Password == "" {
return nil, errors.New("each user must include username and password")
}
if _, ok := seen[user.Username]; ok {
return nil, fmt.Errorf("duplicate username %q", user.Username)
}
seen[user.Username] = struct{}{}
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
return nil, err
}
key := argon2.IDKey([]byte(user.Password), salt, params.Time, params.Memory, params.Threads, params.KeyLen)
block, err := aes.NewCipher(key)
zero(key)
if err != nil {
return nil, err
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, aead.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
blob.Users = append(blob.Users, BlobUser{
Username: user.Username,
Salt: salt,
Nonce: nonce,
Ciphertext: aead.Seal(nil, nonce, pt, nil),
})
}
return json.Marshal(blob)
}
func zero(buf []byte) {
for i := range buf {
buf[i] = 0
}
}
+5
View File
@@ -0,0 +1,5 @@
package embedded
// Blob is populated by cmd/buildblob. It is intentionally empty in source form
// so the tree never contains plaintext credentials.
var Blob []byte
+95
View File
@@ -0,0 +1,95 @@
package oauth
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"golang.org/x/oauth2/clientcredentials"
"tsrun/internal/vault"
)
const tokenURL = "https://api.tailscale.com/api/v2/oauth/token"
type createKeyRequest struct {
Capabilities capabilities `json:"capabilities"`
ExpirySecs int `json:"expirySeconds,omitempty"`
Description string `json:"description,omitempty"`
}
type capabilities struct {
Devices devicesCaps `json:"devices"`
}
type devicesCaps struct {
Create deviceCreate `json:"create"`
}
type deviceCreate struct {
Reusable bool `json:"reusable"`
Ephemeral bool `json:"ephemeral"`
Preauthorized bool `json:"preauthorized"`
Tags []string `json:"tags,omitempty"`
}
type createKeyResponse struct {
ID string `json:"id"`
Key string `json:"key"`
}
func CreateEphemeralAuthKey(ctx context.Context, payload vault.Payload, description string) (string, error) {
config := &clientcredentials.Config{
ClientID: payload.OAuthClientID,
ClientSecret: payload.OAuthClientSecret,
TokenURL: tokenURL,
}
httpClient := config.Client(ctx)
httpClient.Timeout = 20 * time.Second
body, err := json.Marshal(createKeyRequest{
Capabilities: capabilities{Devices: devicesCaps{Create: deviceCreate{
Reusable: false,
Ephemeral: true,
Preauthorized: true,
Tags: payload.Tags,
}}},
ExpirySecs: 3600,
Description: description,
})
if err != nil {
return "", err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.tailscale.com/api/v2/tailnet/-/keys", bytes.NewReader(body))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("tailscale key request failed: %s", resp.Status)
}
var out createKeyResponse
if err := json.Unmarshal(bodyBytes, &out); err != nil {
return "", err
}
if out.Key == "" {
return "", fmt.Errorf("tailscale key request returned empty key")
}
return out.Key, nil
}
+7
View File
@@ -0,0 +1,7 @@
//go:build !windows
package prompt
func FatalDialog(_ string) {}
func WaitForEnter(_ string) {}
+42
View File
@@ -0,0 +1,42 @@
//go:build windows
package prompt
import (
"bufio"
"fmt"
"os"
"os/signal"
"syscall"
"golang.org/x/sys/windows"
)
func FatalDialog(message string) {
text, _ := windows.UTF16PtrFromString(message)
title, _ := windows.UTF16PtrFromString("tsrun")
_, _ = windows.MessageBox(0, text, title, windows.MB_ICONERROR|windows.MB_OK)
}
func WaitForEnter(message string) {
fi, err := os.Stdin.Stat()
if err != nil {
return
}
if fi.Mode()&os.ModeCharDevice == 0 {
return
}
fmt.Println(message)
done := make(chan struct{}, 1)
go func() {
_, _ = bufio.NewReader(os.Stdin).ReadString('\n')
done <- struct{}{}
}()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(sigCh)
select {
case <-done:
case <-sigCh:
}
}
+27
View File
@@ -0,0 +1,27 @@
package prompt
import (
"bufio"
"fmt"
"os"
"strings"
"golang.org/x/term"
)
func Line(label string) (string, error) {
fmt.Print(label)
reader := bufio.NewReader(os.Stdin)
text, err := reader.ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSpace(text), nil
}
func Password(label string) ([]byte, error) {
fmt.Print(label)
value, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
return value, err
}
+46
View File
@@ -0,0 +1,46 @@
//go:build linux
package full
import (
"errors"
"log"
"os"
"os/exec"
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/tsd"
"tailscale.com/wgengine/router"
)
func Clear() error {
if os.Geteuid() != 0 {
return errors.New("tsrun clear requires root on linux")
}
logf := log.New(os.Stderr, "", log.LstdFlags).Printf
sys := tsd.NewSystem()
netMon, err := netmon.New(sys.Bus.Get(), logf)
if err != nil {
return err
}
defer netMon.Close()
dns.CleanUp(logf, netMon, sys.Bus.Get(), sys.HealthTracker.Get(), "tailscale0")
router.CleanUp(logf, netMon, "tailscale0")
_ = deleteLink("tailscale0")
return nil
}
func deleteLink(name string) error {
ip, err := exec.LookPath("ip")
if err != nil {
return nil
}
cmd := exec.Command(ip, "link", "delete", name)
if err := cmd.Run(); err != nil {
return nil
}
return nil
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !linux && !windows
package full
import "errors"
func Clear() error {
return errors.New("tsrun clear is only available on linux and windows")
}
+27
View File
@@ -0,0 +1,27 @@
//go:build windows
package full
import (
"log"
"os"
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/tsd"
"tailscale.com/wgengine/router"
)
func Clear() error {
logf := log.New(os.Stderr, "", log.LstdFlags).Printf
sys := tsd.NewSystem()
netMon, err := netmon.New(sys.Bus.Get(), logf)
if err != nil {
return err
}
defer netMon.Close()
dns.CleanUp(logf, netMon, sys.Bus.Get(), sys.HealthTracker.Get(), "Tailscale")
router.CleanUp(logf, netMon, "Tailscale")
return nil
}
+215
View File
@@ -0,0 +1,215 @@
//go:build linux
package full
import (
"context"
"errors"
"fmt"
"log"
"net/netip"
"os"
"path/filepath"
"time"
_ "tailscale.com/feature/condregister"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/store/mem"
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
"tailscale.com/net/tsdial"
"tailscale.com/net/tstun"
"tailscale.com/tsd"
"tailscale.com/types/logid"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
"tsrun/internal/oauth"
"tsrun/internal/vault"
)
func Run(ctx context.Context, payload vault.Payload) error {
if err := requireRoot(); err != nil {
return err
}
authKey, err := oauth.CreateEphemeralAuthKey(ctx, payload, "tsrun temporary access")
if err != nil {
return err
}
varRoot, err := os.MkdirTemp("", "tsrun-full-*")
if err != nil {
return err
}
defer os.RemoveAll(varRoot)
logf := log.New(os.Stderr, "", log.LstdFlags).Printf
sys := tsd.NewSystem()
netMon, err := netmon.New(sys.Bus.Get(), logf)
if err != nil {
return err
}
defer netMon.Close()
sys.Set(netMon)
dialer := &tsdial.Dialer{Logf: logf}
dialer.SetBus(sys.Bus.Get())
sys.Set(dialer)
netns.SetEnabled(true)
dev, devName, err := tstun.New(logf, "tailscale0")
if err != nil {
tstun.Diagnose(logf, "tailscale0", err)
return fmt.Errorf("creating tun device: %w", err)
}
r, err := router.New(logf, dev, sys.NetMon.Get(), sys.HealthTracker.Get(), sys.Bus.Get())
if err != nil {
return err
}
defer r.Close()
sys.Set(r)
dnsCfg, err := dns.NewOSConfigurator(logf, sys.HealthTracker.Get(), sys.Bus.Get(), sys.PolicyClientOrDefault(), sys.ControlKnobs(), devName)
if err != nil {
return err
}
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Tun: dev,
Router: r,
DNS: dnsCfg,
NetMon: netMon,
Dialer: dialer,
SetSubsystem: sys.Set,
ControlKnobs: sys.ControlKnobs(),
HealthTracker: sys.HealthTracker.Get(),
ExtraRootCAs: sys.ExtraRootCAs,
Metrics: sys.UserMetricsRegistry(),
EventBus: sys.Bus.Get(),
})
if err != nil {
return err
}
defer func() {
eng.Close()
<-eng.Done()
}()
sys.Set(eng)
store := new(mem.Store)
sys.Set(store)
if w, ok := sys.Tun.GetOK(); ok {
w.Start()
}
lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{}, sys, controlclient.LoginEphemeral|controlclient.LocalBackendStartKeyOSNeutral)
if err != nil {
return err
}
defer lb.Shutdown()
lb.SetVarRoot(filepath.Join(varRoot, "state"))
if w, ok := sys.Tun.GetOK(); ok {
ns, err := netstack.Create(logf, w, eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper())
if err != nil {
return fmt.Errorf("creating netstack: %w", err)
}
defer ns.Close()
sys.Set(ns)
if err := ns.Start(lb); err != nil {
return fmt.Errorf("starting netstack: %w", err)
}
}
prefs := ipn.NewPrefs()
prefs.WantRunning = true
prefs.RouteAll = true
prefs.Hostname = defaultHostname("tsrun")
prefs.AdvertiseTags = payload.Tags
if err := lb.Start(ipn.Options{AuthKey: authKey, UpdatePrefs: prefs}); err != nil {
return err
}
if st := lb.State(); st == ipn.NeedsLogin || st == ipn.NoState {
if err := lb.StartLoginInteractive(ctx); err != nil {
return err
}
}
startupCtx, startupCancel := context.WithTimeout(ctx, 45*time.Second)
defer startupCancel()
if err := waitRunning(startupCtx, lb); err != nil {
return err
}
printStatus(lb)
<-ctx.Done()
logoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = lb.Logout(logoutCtx, ipnauth.Self)
return ctx.Err()
}
func waitRunning(ctx context.Context, lb *ipnlocal.LocalBackend) error {
st := lb.State()
for {
switch st {
case ipn.Running:
return nil
case ipn.NeedsMachineAuth:
return errors.New("device needs manual machine approval")
case ipn.Stopped:
return errors.New("tailscale backend stopped unexpectedly")
}
if err := ctx.Err(); err != nil {
return err
}
lb.WatchNotifications(ctx, ipn.NotifyInitialState, nil, func(n *ipn.Notify) (keepGoing bool) {
if n.State != nil {
st = *n.State
}
return st == ipn.NeedsLogin || st == ipn.Starting || st == ipn.NoState
})
}
}
func printStatus(lb *ipnlocal.LocalBackend) {
nm := lb.NetMapNoPeers()
if nm == nil {
fmt.Println("connected")
return
}
var ips []netip.Addr
for _, p := range nm.GetAddresses().All() {
ips = append(ips, p.Addr())
}
if len(ips) == 0 {
fmt.Println("connected")
return
}
fmt.Printf("connected: %v\n", ips)
}
func requireRoot() error {
if os.Geteuid() != 0 {
return errors.New("full client mode requires root on linux; use sudo or run with --proxy for low-privilege mode")
}
return nil
}
func defaultHostname(prefix string) string {
host, err := os.Hostname()
if err != nil || host == "" {
return prefix
}
return fmt.Sprintf("%s-%s", prefix, host)
}
+14
View File
@@ -0,0 +1,14 @@
//go:build !windows && !linux
package full
import (
"context"
"errors"
"tsrun/internal/vault"
)
func Run(_ context.Context, _ vault.Payload) error {
return errors.New("full client mode is only available on windows and linux")
}
+235
View File
@@ -0,0 +1,235 @@
//go:build windows
package full
import (
"context"
"errors"
"fmt"
"log"
"net/netip"
"os"
"path/filepath"
"sync"
"time"
"github.com/dblohm7/wingoes/com"
"golang.org/x/sys/windows"
_ "tailscale.com/feature/condregister"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/store/mem"
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
"tailscale.com/net/tstun"
"tailscale.com/tsd"
"tailscale.com/types/logid"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
"tsrun/internal/oauth"
"tsrun/internal/vault"
)
var startCOMOnce sync.Once
func Run(ctx context.Context, payload vault.Payload) error {
startCOMOnce.Do(func() {
if err := com.StartRuntime(com.ConsoleApp); err != nil {
log.Printf("wingoes.com.StartRuntime failed: %v", err)
}
})
if err := requireElevation(); err != nil {
return err
}
if err := prepareEmbeddedWintun(); err != nil {
return err
}
authKey, err := oauth.CreateEphemeralAuthKey(ctx, payload, "tsrun temporary access")
if err != nil {
return err
}
varRoot, err := os.MkdirTemp("", "tsrun-full-*")
if err != nil {
return err
}
defer os.RemoveAll(varRoot)
logf := log.New(os.Stderr, "", log.LstdFlags).Printf
sys := tsd.NewSystem()
netMon, err := netmon.New(sys.Bus.Get(), logf)
if err != nil {
return err
}
defer netMon.Close()
sys.Set(netMon)
dialer := &tsdial.Dialer{Logf: logf}
dialer.SetBus(sys.Bus.Get())
sys.Set(dialer)
dev, devName, err := tstun.New(logf, "Tailscale")
if err != nil {
return err
}
sys.Set(dev)
r, err := router.New(logf, dev, sys.NetMon.Get(), sys.HealthTracker.Get(), sys.Bus.Get())
if err != nil {
return err
}
defer r.Close()
sys.Set(r)
dnsCfg, err := dns.NewOSConfigurator(logf, sys.HealthTracker.Get(), sys.Bus.Get(), sys.PolicyClientOrDefault(), sys.ControlKnobs(), devName)
if err != nil {
return err
}
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Tun: dev,
Router: r,
DNS: dnsCfg,
NetMon: netMon,
Dialer: dialer,
SetSubsystem: sys.Set,
ControlKnobs: sys.ControlKnobs(),
HealthTracker: sys.HealthTracker.Get(),
ExtraRootCAs: sys.ExtraRootCAs,
Metrics: sys.UserMetricsRegistry(),
EventBus: sys.Bus.Get(),
})
if err != nil {
return err
}
defer func() {
eng.Close()
<-eng.Done()
}()
sys.Set(eng)
store := new(mem.Store)
sys.Set(store)
if w, ok := sys.Tun.GetOK(); ok {
w.Start()
}
lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{}, sys, controlclient.LoginEphemeral|controlclient.LocalBackendStartKeyOSNeutral)
if err != nil {
return err
}
defer lb.Shutdown()
lb.SetVarRoot(filepath.Join(varRoot, "state"))
if w, ok := sys.Tun.GetOK(); ok {
ns, err := netstack.Create(logf, w, eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper())
if err != nil {
return fmt.Errorf("creating netstack: %w", err)
}
defer ns.Close()
sys.Set(ns)
if err := ns.Start(lb); err != nil {
return fmt.Errorf("starting netstack: %w", err)
}
}
prefs := ipn.NewPrefs()
prefs.WantRunning = true
prefs.RouteAll = true
prefs.Hostname = defaultHostname("tsrun")
prefs.AdvertiseTags = payload.Tags
if err := lb.Start(ipn.Options{AuthKey: authKey, UpdatePrefs: prefs}); err != nil {
return err
}
if st := lb.State(); st == ipn.NeedsLogin || st == ipn.NoState {
if err := lb.StartLoginInteractive(ctx); err != nil {
return err
}
}
startupCtx, startupCancel := context.WithTimeout(ctx, 45*time.Second)
defer startupCancel()
if err := waitRunning(startupCtx, lb); err != nil {
return err
}
printStatus(lb)
<-ctx.Done()
logoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = lb.Logout(logoutCtx, ipnauth.Self)
return ctx.Err()
}
func waitRunning(ctx context.Context, lb *ipnlocal.LocalBackend) error {
st := lb.State()
for {
switch st {
case ipn.Running:
return nil
case ipn.NeedsMachineAuth:
return errors.New("device needs manual machine approval")
case ipn.Stopped:
return errors.New("tailscale backend stopped unexpectedly")
}
if err := ctx.Err(); err != nil {
return err
}
lb.WatchNotifications(ctx, ipn.NotifyInitialState, nil, func(n *ipn.Notify) (keepGoing bool) {
if n.State != nil {
st = *n.State
}
return st == ipn.NeedsLogin || st == ipn.Starting || st == ipn.NoState
})
}
}
func printStatus(lb *ipnlocal.LocalBackend) {
nm := lb.NetMapNoPeers()
if nm == nil {
fmt.Println("connected")
return
}
var ips []netip.Addr
for _, p := range nm.GetAddresses().All() {
ips = append(ips, p.Addr())
}
if len(ips) == 0 {
fmt.Println("connected")
return
}
fmt.Printf("connected: %v\n", ips)
}
func requireElevation() error {
adminSID, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
if err != nil {
return err
}
token := windows.Token(0)
isMember, err := token.IsMember(adminSID)
if err != nil {
return err
}
if !isMember {
return errors.New("full client mode requires an elevated administrator shell; use --proxy for low-privilege mode")
}
return nil
}
func defaultHostname(prefix string) string {
host, err := os.Hostname()
if err != nil || host == "" {
return prefix
}
return fmt.Sprintf("%s-%s", prefix, host)
}
@@ -0,0 +1,5 @@
//go:build !windows
package full
func prepareEmbeddedWintun() error { return nil }
@@ -0,0 +1,62 @@
//go:build windows
package full
import (
"crypto/sha256"
_ "embed"
"errors"
"fmt"
"os"
"path/filepath"
"golang.org/x/sys/windows"
)
//go:embed wintun.dll
var embeddedWintun []byte
func prepareEmbeddedWintun() error {
if len(embeddedWintun) == 0 {
return fmt.Errorf("embedded wintun.dll missing; place wintun.dll at internal/runtime/full/wintun.dll before building")
}
exePath, err := os.Executable()
if err != nil {
return err
}
dir := filepath.Dir(exePath)
path := filepath.Join(dir, "wintun.dll")
if err := ensureMatchingDLL(path); err != nil {
return err
}
if _, err := windows.LoadDLL(path); err != nil {
return fmt.Errorf("loading embedded wintun.dll: %w", err)
}
return nil
}
func ensureMatchingDLL(path string) error {
wantHash := sha256.Sum256(embeddedWintun)
if existing, err := os.ReadFile(path); err == nil {
haveHash := sha256.Sum256(existing)
if haveHash == wantHash {
return nil
}
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
tempPath := path + ".tmp"
if err := os.WriteFile(tempPath, embeddedWintun, 0o600); err != nil {
return err
}
_ = os.Remove(path)
if err := os.Rename(tempPath, path); err != nil {
_ = os.Remove(tempPath)
return err
}
return nil
}
+183
View File
@@ -0,0 +1,183 @@
package proxy
import (
"context"
crand "crypto/rand"
"encoding/hex"
"fmt"
"io"
"net"
"net/http"
"strings"
"tailscale.com/net/proxymux"
"tailscale.com/net/socks5"
"tailscale.com/tsnet"
)
type listenerBundle struct {
addr string
proxyCred string
closeFn func() error
}
func startProxyListener(ctx context.Context, srv *tsnet.Server) (*listenerBundle, error) {
cred, err := randomHex(16)
if err != nil {
return nil, err
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}
socksLn, httpLn := proxymux.SplitSOCKSAndHTTP(ln)
socksSrv := &socks5.Server{
Dialer: srv.Dial,
Username: "tsnet",
Password: cred,
}
httpSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
handleConnect(w, r, srv)
return
}
handleForward(w, r, srv)
}),
}
go func() {
<-ctx.Done()
_ = httpSrv.Close()
_ = socksLn.Close()
_ = httpLn.Close()
_ = ln.Close()
}()
go func() {
_ = socksSrv.Serve(socksLn)
}()
go func() {
_ = httpSrv.Serve(httpLn)
}()
return &listenerBundle{
addr: ln.Addr().String(),
proxyCred: cred,
closeFn: func() error {
var firstErr error
for _, fn := range []func() error{
httpSrv.Close,
socksLn.Close,
httpLn.Close,
ln.Close,
} {
if err := fn(); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
},
}, nil
}
func handleConnect(w http.ResponseWriter, r *http.Request, srv *tsnet.Server) {
dst := canonicalAddr(r.Host)
upstream, err := srv.Dial(r.Context(), "tcp", dst)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
hj, ok := w.(http.Hijacker)
if !ok {
upstream.Close()
http.Error(w, "hijacking not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hj.Hijack()
if err != nil {
upstream.Close()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, _ = clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
go proxyCopy(upstream, clientConn)
go proxyCopy(clientConn, upstream)
}
func handleForward(w http.ResponseWriter, r *http.Request, srv *tsnet.Server) {
if r.URL == nil || r.URL.Scheme == "" || r.URL.Host == "" {
http.Error(w, "configure this as an HTTP proxy; do not browse directly to the proxy address", http.StatusBadRequest)
return
}
req, err := http.NewRequestWithContext(r.Context(), r.Method, r.URL.String(), r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
req.Header = r.Header.Clone()
req.RequestURI = ""
transport := &http.Transport{
Proxy: nil,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return srv.Dial(ctx, network, addr)
},
}
defer transport.CloseIdleConnections()
resp, err := transport.RoundTrip(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
copyHeader(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
_, _ = io.Copy(w, resp.Body)
}
func canonicalAddr(host string) string {
if _, _, err := net.SplitHostPort(host); err == nil {
return host
}
if strings.Contains(host, ":") {
return net.JoinHostPort(host, "443")
}
return net.JoinHostPort(host, "80")
}
func copyHeader(dst, src http.Header) {
for k, vals := range src {
for _, v := range vals {
dst.Add(k, v)
}
}
}
func proxyCopy(dst io.WriteCloser, src io.ReadCloser) {
defer dst.Close()
defer src.Close()
_, _ = io.Copy(dst, src)
}
func directRequestHint() string {
return "Use the single proxy address below as either an HTTP proxy or a SOCKS5 proxy. Do not browse directly to the proxy address itself."
}
func randomHex(size int) (string, error) {
buf := make([]byte, size)
if _, err := crand.Read(buf); err != nil {
return "", err
}
return hex.EncodeToString(buf), nil
}
func formatProxyURL(addr string) string {
return fmt.Sprintf("http://%s", addr)
}
+62
View File
@@ -0,0 +1,62 @@
package proxy
import (
"context"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
_ "tailscale.com/feature/condregister/oauthkey"
"tailscale.com/tsnet"
"tsrun/internal/vault"
)
func Run(ctx context.Context, payload vault.Payload) error {
baseDir, err := os.MkdirTemp("", "tsrun-proxy-*")
if err != nil {
return err
}
defer os.RemoveAll(baseDir)
hostname := defaultHostname("tsrun-proxy")
srv := &tsnet.Server{
Hostname: hostname,
Dir: filepath.Join(baseDir, "state"),
Ephemeral: true,
ClientSecret: payload.OAuthClientSecret,
AdvertiseTags: payload.Tags,
}
defer srv.Close()
if _, err := srv.Up(ctx); err != nil {
return err
}
proxyListener, err := startProxyListener(ctx, srv)
if err != nil {
return err
}
defer proxyListener.closeFn()
fmt.Printf("proxy mode active\n")
fmt.Printf("%s\n", directRequestHint())
fmt.Printf("Proxy: %s\n", formatProxyURL(proxyListener.addr))
fmt.Printf("Username: tsnet\n")
fmt.Printf("Password: %s\n", proxyListener.proxyCred)
waitCtx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()
<-waitCtx.Done()
return waitCtx.Err()
}
func defaultHostname(prefix string) string {
host, err := os.Hostname()
if err != nil || host == "" {
return prefix
}
return fmt.Sprintf("%s-%s", prefix, host)
}
+66
View File
@@ -0,0 +1,66 @@
package vault
import (
"crypto/aes"
"crypto/cipher"
"crypto/subtle"
"encoding/json"
"errors"
"golang.org/x/crypto/argon2"
"tsrun/internal/buildblob"
)
var ErrAuthFailed = errors.New("authentication failed")
type Payload struct {
OAuthClientID string `json:"oauthClientID"`
OAuthClientSecret string `json:"oauthClientSecret"`
Tags []string `json:"tags"`
}
func Unlock(blob []byte, username string, password []byte) (Payload, error) {
if len(blob) == 0 {
return Payload{}, errors.New("no embedded credential blob; run cmd/buildblob first")
}
var parsed buildblob.Blob
if err := json.Unmarshal(blob, &parsed); err != nil {
return Payload{}, err
}
for _, user := range parsed.Users {
if subtle.ConstantTimeCompare([]byte(user.Username), []byte(username)) != 1 {
continue
}
key := argon2.IDKey(password, user.Salt, parsed.KDF.Time, parsed.KDF.Memory, parsed.KDF.Threads, parsed.KDF.KeyLen)
defer Zero(key)
block, err := aes.NewCipher(key)
if err != nil {
return Payload{}, err
}
aead, err := cipher.NewGCM(block)
if err != nil {
return Payload{}, err
}
pt, err := aead.Open(nil, user.Nonce, user.Ciphertext, nil)
if err != nil {
return Payload{}, ErrAuthFailed
}
defer Zero(pt)
var payload Payload
if err := json.Unmarshal(pt, &payload); err != nil {
return Payload{}, err
}
if payload.OAuthClientID == "" || payload.OAuthClientSecret == "" {
return Payload{}, ErrAuthFailed
}
return payload, nil
}
return Payload{}, ErrAuthFailed
}
func Zero(buf []byte) {
for i := range buf {
buf[i] = 0
}
}
+41
View File
@@ -0,0 +1,41 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: tsrun
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: tsrun
template:
metadata:
labels:
app: tsrun
spec:
containers:
- name: tsrun
image: git.astrxl.dev/astral/tsrun:latest
env:
- name: PORT
value: "3000"
- name: BASE_URL
value: "https://tsr.dgnr.us"
ports:
- containerPort: 3000
name: http
livenessProbe:
httpGet:
path: /health
port: http
readinessProbe:
httpGet:
path: /health
port: http
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 256Mi
+18
View File
@@ -0,0 +1,18 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tsrun
namespace: default
spec:
ingressClassName: traefik
rules:
- host: tsr.dgnr.us
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: tsrun
port:
number: 80
+7
View File
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: default
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: tsrun
namespace: default
spec:
selector:
app: tsrun
ports:
- port: 80
targetPort: http
name: http
+13
View File
@@ -0,0 +1,13 @@
{
"oauthClientID": "tskey-client-xxxxxxxx",
"oauthClientSecret": "tskey-client-secret-xxxxxxxx",
"tags": [
"tag:user"
],
"users": [
{
"username": "alice",
"password": "replace-me"
}
]
}
+188
View File
@@ -0,0 +1,188 @@
const baseURL = Bun.env.BASE_URL ?? "https://tsr.dgnr.us";
const port = Number(Bun.env.PORT ?? "3000");
const routes = {
linux: "/linux",
linuxRaw: "/linux-raw",
windows: "/windows",
windowsRaw: "/windows-raw",
shell: "/l",
powershell: "/w",
};
const files = {
"/linux": "./dist/tsrun-linux-upx",
"/linux-raw": "./dist/tsrun-linux",
"/windows": "./dist/tsrun-upx.exe",
"/windows-raw": "./dist/tsrun.exe",
};
const usageText = [
"tsrun bootstrap service",
"",
`Linux: curl -fsSL ${baseURL} | bash`,
`Windows: irm ${baseURL} | iex`,
"",
"Raw binary endpoints:",
` ${baseURL}${routes.linux}`,
` ${baseURL}${routes.windows}`,
` ${baseURL}${routes.linuxRaw}`,
` ${baseURL}${routes.windowsRaw}`,
].join("\n");
function shellBootstrap(url: URL) {
const binURL = new URL(routes.linux, url).toString();
return `#!/usr/bin/env bash
set -euo pipefail
TSRUN_URL=${shellQuote(binURL)}
TSRUN_ARGS=("$@")
need_root() {
[[ "$(id -u)" -eq 0 ]]
}
run_as_root() {
if need_root; then
exec "$1" "\${TSRUN_ARGS[@]}"
fi
if ! command -v sudo >/dev/null 2>&1; then
printf 'sudo is required for full tsrun mode. Rerun with sudo or use --proxy.\n' >&2
exit 1
fi
exec sudo -- "$1" "\${TSRUN_ARGS[@]}"
}
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
outfile="$tmpdir/tsrun"
printf 'downloading tsrun...\n' >&2
curl -fsSL "$TSRUN_URL" -o "$outfile"
chmod +x "$outfile"
printf 'starting tsrun...\n' >&2
run_as_root "$outfile"
`;
}
function powershellBootstrap(url: URL) {
const binURL = new URL(routes.windows, url).toString();
return `$ErrorActionPreference = 'Stop'
function Test-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Start-Tsrun {
param(
[string[]] $TsrunArgs = @()
)
$url = ${psQuote(binURL)}
$tempDir = Join-Path $env:TEMP 'tsrun-bootstrap'
$exePath = Join-Path $tempDir 'tsrun.exe'
New-Item -ItemType Directory -Force -Path $tempDir | Out-Null
Write-Host 'downloading tsrun...'
Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $exePath
if (-not (Test-Administrator)) {
Write-Host 'elevating privileges...'
$argList = @(
'-NoProfile',
'-ExecutionPolicy', 'Bypass',
'-Command',
"& { & '${exePath}' $($TsrunArgs -join ' ') }"
)
Start-Process -FilePath 'powershell' -Verb RunAs -ArgumentList $argList | Out-Null
return
}
Write-Host 'starting tsrun...'
& $exePath @TsrunArgs
}
Start-Tsrun
`;
}
function shellQuote(value: string) {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
function psQuote(value: string) {
return `'${value.replaceAll("'", "''")}'`;
}
function detectBootstrapKind(req: Request, url: URL): "shell" | "powershell" | "text" {
const forced = url.searchParams.get("os");
if (forced === "linux") return "shell";
if (forced === "windows") return "powershell";
const ua = (req.headers.get("user-agent") ?? "").toLowerCase();
if (ua.includes("powershell") || ua.includes("windowspowershell")) return "powershell";
if (ua.includes("curl") || ua.includes("wget") || ua.includes("busybox")) return "shell";
return "text";
}
async function binaryResponse(path: string) {
const file = Bun.file(path);
if (!(await file.exists())) {
return new Response(`missing artifact: ${path}\n`, { status: 500 });
}
const name = path.split("/").at(-1) ?? "artifact";
return new Response(file, {
headers: {
"content-type": "application/octet-stream",
"content-disposition": `attachment; filename="${name}"`,
"cache-control": "public, max-age=300",
},
});
}
function textResponse(body: string, contentType = "text/plain; charset=utf-8") {
return new Response(body, {
headers: {
"content-type": contentType,
"cache-control": "no-store",
},
});
}
const server = Bun.serve({
port,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/health") {
return new Response("ok\n");
}
if (url.pathname in files) {
return await binaryResponse(files[url.pathname as keyof typeof files]);
}
if (url.pathname === routes.shell) {
return textResponse(shellBootstrap(url));
}
if (url.pathname === routes.powershell) {
return textResponse(powershellBootstrap(url));
}
if (url.pathname === "/") {
const kind = detectBootstrapKind(req, url);
if (kind === "shell") return textResponse(shellBootstrap(url));
if (kind === "powershell") return textResponse(powershellBootstrap(url));
return textResponse(usageText);
}
return new Response("not found\n", { status: 404 });
},
});
console.log(`tsrun bootstrap server listening on :${server.port}`);