i feel like committing
Deploy tsrun / deploy (push) Successful in 6m23s

This commit is contained in:
2026-07-23 02:22:19 +00:00 Unverified
parent 6aec28dbff
commit 2cfb64b496
8 changed files with 548 additions and 10 deletions
+11
View File
@@ -15,6 +15,7 @@ import (
"tsrun/internal/prompt"
"tsrun/internal/runtime/full"
"tsrun/internal/runtime/proxy"
"tsrun/internal/ui"
"tsrun/internal/vault"
)
@@ -28,9 +29,19 @@ func main() {
}
useProxy := flag.Bool("proxy", false, "run proxy mode instead of full client mode")
debugMode := flag.Bool("debug", false, "use raw prompt and log mode instead of TUI")
userFlag := flag.String("u", "", "username or username:password")
flag.Parse()
if !*debugMode {
prefill, _, _ := strings.Cut(*userFlag, ":")
if err := ui.Run(*useProxy, prefill); err != nil {
fatal(err)
}
fmt.Println("shutting down")
return
}
username, password, err := resolveCredentials(*userFlag)
if err != nil {
fatal(err)
+23 -1
View File
@@ -3,6 +3,10 @@ module tsrun
go 1.26.4
require (
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa
golang.org/x/crypto v0.52.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.45.0
@@ -14,6 +18,7 @@ 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/atotto/clipboard v0.1.4 // 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
@@ -28,11 +33,19 @@ require (
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/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // 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
@@ -52,13 +65,21 @@ require (
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/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // 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/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/rivo/uniseg v0.4.7 // 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
@@ -70,6 +91,7 @@ require (
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
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // 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
+45
View File
@@ -12,6 +12,8 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
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/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
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=
@@ -42,8 +44,30 @@ 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/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
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/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
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=
@@ -67,6 +91,8 @@ github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yez
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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
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=
@@ -125,6 +151,14 @@ 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/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
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=
@@ -137,6 +171,12 @@ 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/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
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=
@@ -154,6 +194,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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=
@@ -194,6 +236,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
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=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
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=
@@ -217,6 +261,7 @@ 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-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
+44 -2
View File
@@ -6,6 +6,7 @@ import (
"context"
"errors"
"fmt"
"io"
"log"
"net/netip"
"os"
@@ -30,14 +31,21 @@ import (
"tailscale.com/wgengine/router"
"tsrun/internal/oauth"
base "tsrun/internal/runtime"
"tsrun/internal/vault"
)
func Run(ctx context.Context, payload vault.Payload) error {
return RunWithReporter(ctx, payload, nil)
}
func RunWithReporter(ctx context.Context, payload vault.Payload, report base.Reporter) error {
report.Send(base.Event{Kind: base.EventStage, Mode: "full", Message: "starting full mode"})
if err := requireRoot(); err != nil {
return err
}
report.Send(base.Event{Kind: base.EventStage, Mode: "full", Message: "creating ephemeral auth key"})
authKey, err := oauth.CreateEphemeralAuthKey(ctx, payload, "tsrun temporary access")
if err != nil {
return err
@@ -49,7 +57,11 @@ func Run(ctx context.Context, payload vault.Payload) error {
}
defer os.RemoveAll(varRoot)
logf := log.New(os.Stderr, "", log.LstdFlags).Printf
logWriter := io.Writer(os.Stderr)
if report != nil {
logWriter = io.Discard
}
logf := log.New(logWriter, "", log.LstdFlags).Printf
sys := tsd.NewSystem()
netMon, err := netmon.New(sys.Bus.Get(), logf)
@@ -139,6 +151,7 @@ func Run(ctx context.Context, payload vault.Payload) error {
if err := lb.Start(ipn.Options{AuthKey: authKey, UpdatePrefs: prefs}); err != nil {
return err
}
report.Send(base.Event{Kind: base.EventStage, Mode: "full", Message: "waiting for tailscale network"})
if st := lb.State(); st == ipn.NeedsLogin || st == ipn.NoState {
if err := lb.StartLoginInteractive(ctx); err != nil {
return err
@@ -150,7 +163,18 @@ func Run(ctx context.Context, payload vault.Payload) error {
return err
}
printStatus(lb)
if report == nil {
printStatus(lb)
}
ipv4, ipv6 := tailscaleIPs(lb)
report.Send(base.Event{
Kind: base.EventRunning,
Mode: "full",
Message: "connected",
Hostname: prefs.Hostname,
IPv4: ipv4,
IPv6: ipv6,
})
<-ctx.Done()
logoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -199,6 +223,24 @@ func printStatus(lb *ipnlocal.LocalBackend) {
fmt.Printf("connected: %v\n", ips)
}
func tailscaleIPs(lb *ipnlocal.LocalBackend) (string, string) {
nm := lb.NetMapNoPeers()
if nm == nil {
return "", ""
}
var ip4, ip6 string
for _, p := range nm.GetAddresses().All() {
addr := p.Addr()
if addr.Is4() && ip4 == "" {
ip4 = addr.String()
}
if addr.Is6() && ip6 == "" {
ip6 = addr.String()
}
}
return ip4, ip6
}
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")
+44 -2
View File
@@ -6,6 +6,7 @@ import (
"context"
"errors"
"fmt"
"io"
"log"
"net/netip"
"os"
@@ -32,17 +33,23 @@ import (
"tailscale.com/wgengine/router"
"tsrun/internal/oauth"
base "tsrun/internal/runtime"
"tsrun/internal/vault"
)
var startCOMOnce sync.Once
func Run(ctx context.Context, payload vault.Payload) error {
return RunWithReporter(ctx, payload, nil)
}
func RunWithReporter(ctx context.Context, payload vault.Payload, report base.Reporter) error {
startCOMOnce.Do(func() {
if err := com.StartRuntime(com.ConsoleApp); err != nil {
log.Printf("wingoes.com.StartRuntime failed: %v", err)
}
})
report.Send(base.Event{Kind: base.EventStage, Mode: "full", Message: "starting full mode"})
if err := requireElevation(); err != nil {
return err
@@ -51,6 +58,7 @@ func Run(ctx context.Context, payload vault.Payload) error {
return err
}
report.Send(base.Event{Kind: base.EventStage, Mode: "full", Message: "creating ephemeral auth key"})
authKey, err := oauth.CreateEphemeralAuthKey(ctx, payload, "tsrun temporary access")
if err != nil {
return err
@@ -62,7 +70,11 @@ func Run(ctx context.Context, payload vault.Payload) error {
}
defer os.RemoveAll(varRoot)
logf := log.New(os.Stderr, "", log.LstdFlags).Printf
logWriter := io.Writer(os.Stderr)
if report != nil {
logWriter = io.Discard
}
logf := log.New(logWriter, "", log.LstdFlags).Printf
sys := tsd.NewSystem()
netMon, err := netmon.New(sys.Bus.Get(), logf)
@@ -149,6 +161,7 @@ func Run(ctx context.Context, payload vault.Payload) error {
if err := lb.Start(ipn.Options{AuthKey: authKey, UpdatePrefs: prefs}); err != nil {
return err
}
report.Send(base.Event{Kind: base.EventStage, Mode: "full", Message: "waiting for tailscale network"})
if st := lb.State(); st == ipn.NeedsLogin || st == ipn.NoState {
if err := lb.StartLoginInteractive(ctx); err != nil {
return err
@@ -160,7 +173,18 @@ func Run(ctx context.Context, payload vault.Payload) error {
return err
}
printStatus(lb)
if report == nil {
printStatus(lb)
}
ipv4, ipv6 := tailscaleIPs(lb)
report.Send(base.Event{
Kind: base.EventRunning,
Mode: "full",
Message: "connected",
Hostname: prefs.Hostname,
IPv4: ipv4,
IPv6: ipv6,
})
<-ctx.Done()
logoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -209,6 +233,24 @@ func printStatus(lb *ipnlocal.LocalBackend) {
fmt.Printf("connected: %v\n", ips)
}
func tailscaleIPs(lb *ipnlocal.LocalBackend) (string, string) {
nm := lb.NetMapNoPeers()
if nm == nil {
return "", ""
}
var ip4, ip6 string
for _, p := range nm.GetAddresses().All() {
addr := p.Addr()
if addr.Is4() && ip4 == "" {
ip4 = addr.String()
}
if addr.Is6() && ip6 == "" {
ip6 = addr.String()
}
}
return ip4, ip6
}
func requireElevation() error {
adminSID, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
if err != nil {
+23 -5
View File
@@ -11,10 +11,16 @@ import (
_ "tailscale.com/feature/condregister/oauthkey"
"tailscale.com/tsnet"
base "tsrun/internal/runtime"
"tsrun/internal/vault"
)
func Run(ctx context.Context, payload vault.Payload) error {
return RunWithReporter(ctx, payload, nil)
}
func RunWithReporter(ctx context.Context, payload vault.Payload, report base.Reporter) error {
report.Send(base.Event{Kind: base.EventStage, Mode: "proxy", Message: "starting proxy mode"})
baseDir, err := os.MkdirTemp("", "tsrun-proxy-*")
if err != nil {
return err
@@ -34,6 +40,7 @@ func Run(ctx context.Context, payload vault.Payload) error {
if _, err := srv.Up(ctx); err != nil {
return err
}
report.Send(base.Event{Kind: base.EventStage, Mode: "proxy", Message: "tailscale connected"})
proxyListener, err := startProxyListener(ctx, srv)
if err != nil {
@@ -41,11 +48,22 @@ func Run(ctx context.Context, payload vault.Payload) error {
}
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)
if report == nil {
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)
}
report.Send(base.Event{
Kind: base.EventRunning,
Mode: "proxy",
Message: "proxy ready",
Hostname: hostname,
ProxyAddr: proxyListener.addr,
ProxyUser: "tsnet",
ProxyPass: proxyListener.proxyCred,
})
waitCtx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()
+28
View File
@@ -0,0 +1,28 @@
package runtime
type EventKind string
const (
EventStage EventKind = "stage"
EventRunning EventKind = "running"
)
type Event struct {
Kind EventKind
Message string
Mode string
Hostname string
IPv4 string
IPv6 string
ProxyAddr string
ProxyUser string
ProxyPass string
}
type Reporter func(Event)
func (r Reporter) Send(event Event) {
if r != nil {
r(event)
}
}
+330
View File
@@ -0,0 +1,330 @@
package ui
import (
"context"
"errors"
"fmt"
"net/netip"
"strings"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"tsrun/internal/embedded"
base "tsrun/internal/runtime"
"tsrun/internal/runtime/full"
"tsrun/internal/runtime/proxy"
"tsrun/internal/vault"
)
type screen int
const (
screenLogin screen = iota
screenConnecting
screenDashboard
screenError
)
type runtimeEventMsg struct{ event base.Event }
type runtimeDoneMsg struct{ err error }
type App struct {
useProxy bool
prefill string
screen screen
spinner spinner.Model
username textinput.Model
password textinput.Model
focus int
status string
errText string
event base.Event
ctx context.Context
cancel context.CancelFunc
eventCh chan base.Event
doneCh chan error
started bool
connected bool
}
func Run(useProxy bool, prefill string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sp := spinner.New()
sp.Spinner = spinner.Line
username := textinput.New()
username.Placeholder = "username"
username.Prompt = "Username: "
username.CharLimit = 64
username.SetValue(prefill)
password := textinput.New()
password.Placeholder = "password"
password.Prompt = "Password: "
password.CharLimit = 128
password.EchoMode = textinput.EchoPassword
password.EchoCharacter = '•'
if prefill == "" {
username.Focus()
} else {
password.Focus()
}
app := &App{
useProxy: useProxy,
prefill: prefill,
screen: screenLogin,
spinner: sp,
username: username,
password: password,
ctx: ctx,
cancel: cancel,
}
_, err := tea.NewProgram(app, tea.WithAltScreen()).Run()
if err != nil {
return err
}
if app.errText != "" {
return errors.New(app.errText)
}
return nil
}
func (a *App) Init() tea.Cmd { return nil }
func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch a.screen {
case screenLogin:
return a.updateLogin(msg)
case screenConnecting, screenDashboard:
if msg.String() == "q" || msg.String() == "ctrl+c" {
a.cancel()
return a, tea.Quit
}
case screenError:
if msg.String() == "enter" || msg.String() == "q" || msg.String() == "esc" || msg.String() == "ctrl+c" {
return a, tea.Quit
}
}
case spinner.TickMsg:
if a.screen == screenConnecting {
var cmd tea.Cmd
a.spinner, cmd = a.spinner.Update(msg)
return a, cmd
}
case runtimeEventMsg:
a.status = msg.event.Message
a.event = msg.event
if msg.event.Kind == base.EventRunning {
a.connected = true
a.screen = screenDashboard
}
return a, a.waitForEventCmd()
case runtimeDoneMsg:
if msg.err != nil && !errors.Is(msg.err, context.Canceled) {
a.errText = msg.err.Error()
a.screen = screenError
return a, nil
}
if a.connected {
return a, tea.Quit
}
if msg.err != nil {
a.errText = msg.err.Error()
a.screen = screenError
return a, nil
}
return a, tea.Quit
}
return a, nil
}
func (a *App) updateLogin(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "ctrl+c", "esc":
return a, tea.Quit
case "tab", "shift+tab", "up", "down":
if a.focus == 0 {
a.focus = 1
a.username.Blur()
a.password.Focus()
} else {
a.focus = 0
a.password.Blur()
a.username.Focus()
}
return a, nil
case "enter":
if a.focus == 0 {
a.focus = 1
a.username.Blur()
a.password.Focus()
return a, nil
}
return a, a.startRuntime()
}
if a.focus == 0 {
var cmd tea.Cmd
a.username, cmd = a.username.Update(msg)
return a, cmd
}
var cmd tea.Cmd
a.password, cmd = a.password.Update(msg)
return a, cmd
}
func (a *App) startRuntime() tea.Cmd {
username := strings.TrimSpace(a.username.Value())
passwordBytes := []byte(a.password.Value())
a.screen = screenConnecting
a.status = "unlocking credentials"
payload, err := vault.Unlock(embedded.Blob, username, passwordBytes)
vault.Zero(passwordBytes)
if err != nil {
if errors.Is(err, vault.ErrAuthFailed) {
a.errText = "authentication failed"
} else {
a.errText = err.Error()
}
a.screen = screenError
return nil
}
a.eventCh = make(chan base.Event, 16)
a.doneCh = make(chan error, 1)
a.started = true
go func() {
report := base.Reporter(func(event base.Event) {
a.eventCh <- event
})
var runErr error
if a.useProxy {
runErr = proxy.RunWithReporter(a.ctx, payload, report)
} else {
runErr = full.RunWithReporter(a.ctx, payload, report)
}
a.doneCh <- runErr
close(a.eventCh)
close(a.doneCh)
}()
return tea.Batch(a.spinner.Tick, a.waitForEventCmd(), a.waitForDoneCmd())
}
func (a *App) waitForEventCmd() tea.Cmd {
if !a.started || a.eventCh == nil {
return nil
}
return func() tea.Msg {
event, ok := <-a.eventCh
if !ok {
return nil
}
return runtimeEventMsg{event: event}
}
}
func (a *App) waitForDoneCmd() tea.Cmd {
if !a.started || a.doneCh == nil {
return nil
}
return func() tea.Msg {
err, ok := <-a.doneCh
if !ok {
return nil
}
return runtimeDoneMsg{err: err}
}
}
func (a *App) View() string {
switch a.screen {
case screenLogin:
return pageStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
titleStyle.Render("tsrun"),
subStyle.Render("Temporary Tailscale access"),
"",
a.username.View(),
a.password.View(),
"",
helpStyle.Render("Tab to switch, Enter to continue, Esc to quit"),
))
case screenConnecting:
return pageStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
titleStyle.Render("tsrun"),
"",
fmt.Sprintf("%s %s", a.spinner.View(), a.status),
"",
helpStyle.Render("q to cancel"),
))
case screenDashboard:
lines := []string{
fmt.Sprintf("Mode: %s", a.event.Mode),
fmt.Sprintf("Status: %s", a.event.Message),
}
if a.event.Hostname != "" {
lines = append(lines, fmt.Sprintf("Hostname: %s", a.event.Hostname))
}
if validAddr(a.event.IPv4) {
lines = append(lines, fmt.Sprintf("IPv4: %s", a.event.IPv4))
}
if validAddr(a.event.IPv6) {
lines = append(lines, fmt.Sprintf("IPv6: %s", a.event.IPv6))
}
if a.event.ProxyAddr != "" {
lines = append(lines,
fmt.Sprintf("Proxy: %s", a.event.ProxyAddr),
fmt.Sprintf("Proxy user: %s", a.event.ProxyUser),
fmt.Sprintf("Proxy password: %s", a.event.ProxyPass),
)
}
return pageStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
titleStyle.Render("tsrun"),
"",
strings.Join(lines, "\n"),
"",
helpStyle.Render("q to disconnect"),
))
case screenError:
return pageStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
titleStyle.Render("tsrun"),
"",
errorStyle.Render(a.errText),
"",
helpStyle.Render("Enter or q to exit"),
))
default:
return ""
}
}
func validAddr(v string) bool {
if v == "" || v == "<nil>" {
return false
}
_, err := netip.ParseAddr(v)
return err == nil
}
var (
pageStyle = lipgloss.NewStyle().Padding(2, 4)
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
subStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Bold(true)
)