This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user