mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-26 11:35:35 +00:00
1db46919ab
Due to a bug in Go (golang/go#51778), cmd/go doesn't warn about your Go version being older than the go.mod's declared Go version in that case that package loading fails before the build starts, such as when you use packages that are only in the current version of Go, like our use of net/netip. This change works around that Go bug by adding build tags and a pre-Go1.18-only file that will cause Go 1.17 and earlier to fail like: $ ~/sdk/go1.17/bin/go install ./cmd/tailscaled # tailscale.com/cmd/tailscaled ./required_version.go:11:2: undefined: you_need_Go_1_18_to_compile_Tailscale note: module requires Go 1.18 Change-Id: I39f5820de646703e19dde448dd86a7022252f75c Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
382 lines
9.9 KiB
Go
382 lines
9.9 KiB
Go
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
//go:build go1.18
|
|
// +build go1.18
|
|
|
|
package main // import "tailscale.com/cmd/tailscaled"
|
|
|
|
// TODO: check if administrator, like tswin does.
|
|
//
|
|
// TODO: try to load wintun.dll early at startup, before wireguard/tun
|
|
// does (which panics) and if we'd fail (e.g. due to access
|
|
// denied, even if administrator), use 'tasklist /m wintun.dll'
|
|
// to see if something else is currently using it and tell user.
|
|
//
|
|
// TODO: check if Tailscale service is already running, and fail early
|
|
// like tswin does.
|
|
//
|
|
// TODO: on failure, check if on a UNC drive and recommend copying it
|
|
// to C:\ to run it, like tswin does.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
|
|
"golang.org/x/sys/windows"
|
|
"golang.org/x/sys/windows/svc"
|
|
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
|
"inet.af/netaddr"
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/ipn/ipnserver"
|
|
"tailscale.com/ipn/store"
|
|
"tailscale.com/logpolicy"
|
|
"tailscale.com/net/dns"
|
|
"tailscale.com/net/tsdial"
|
|
"tailscale.com/net/tstun"
|
|
"tailscale.com/safesocket"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/winutil"
|
|
"tailscale.com/version"
|
|
"tailscale.com/wf"
|
|
"tailscale.com/wgengine"
|
|
"tailscale.com/wgengine/monitor"
|
|
"tailscale.com/wgengine/netstack"
|
|
"tailscale.com/wgengine/router"
|
|
)
|
|
|
|
const serviceName = "Tailscale"
|
|
|
|
func isWindowsService() bool {
|
|
v, err := svc.IsWindowsService()
|
|
if err != nil {
|
|
log.Fatalf("svc.IsWindowsService failed: %v", err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
// runWindowsService starts running Tailscale under the Windows
|
|
// Service environment.
|
|
//
|
|
// At this point we're still the parent process that
|
|
// Windows started.
|
|
func runWindowsService(pol *logpolicy.Policy) error {
|
|
return svc.Run(serviceName, &ipnService{Policy: pol})
|
|
}
|
|
|
|
type ipnService struct {
|
|
Policy *logpolicy.Policy
|
|
}
|
|
|
|
// Called by Windows to execute the windows service.
|
|
func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
|
|
changes <- svc.Status{State: svc.StartPending}
|
|
|
|
svcAccepts := svc.AcceptStop
|
|
if winutil.GetPolicyInteger("FlushDNSOnSessionUnlock", 0) != 0 {
|
|
svcAccepts |= svc.AcceptSessionChange
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
doneCh := make(chan struct{})
|
|
go func() {
|
|
defer close(doneCh)
|
|
args := []string{"/subproc", service.Policy.PublicID.String()}
|
|
// Make a logger without a date prefix, as filelogger
|
|
// and logtail both already add their own. All we really want
|
|
// from the log package is the automatic newline.
|
|
// We start with log.Default().Writer(), which is the logtail
|
|
// writer that logpolicy already installed as the global
|
|
// output.
|
|
logger := log.New(log.Default().Writer(), "", 0)
|
|
ipnserver.BabysitProc(ctx, args, logger.Printf)
|
|
}()
|
|
|
|
changes <- svc.Status{State: svc.Running, Accepts: svcAccepts}
|
|
|
|
for ctx.Err() == nil {
|
|
select {
|
|
case <-doneCh:
|
|
case cmd := <-r:
|
|
log.Printf("Got Windows Service event: %v", cmdName(cmd.Cmd))
|
|
switch cmd.Cmd {
|
|
case svc.Stop:
|
|
cancel()
|
|
case svc.Interrogate:
|
|
changes <- cmd.CurrentStatus
|
|
case svc.SessionChange:
|
|
handleSessionChange(cmd)
|
|
changes <- cmd.CurrentStatus
|
|
}
|
|
}
|
|
}
|
|
|
|
changes <- svc.Status{State: svc.StopPending}
|
|
return false, windows.NO_ERROR
|
|
}
|
|
|
|
func cmdName(c svc.Cmd) string {
|
|
switch c {
|
|
case svc.Stop:
|
|
return "Stop"
|
|
case svc.Pause:
|
|
return "Pause"
|
|
case svc.Continue:
|
|
return "Continue"
|
|
case svc.Interrogate:
|
|
return "Interrogate"
|
|
case svc.Shutdown:
|
|
return "Shutdown"
|
|
case svc.ParamChange:
|
|
return "ParamChange"
|
|
case svc.NetBindAdd:
|
|
return "NetBindAdd"
|
|
case svc.NetBindRemove:
|
|
return "NetBindRemove"
|
|
case svc.NetBindEnable:
|
|
return "NetBindEnable"
|
|
case svc.NetBindDisable:
|
|
return "NetBindDisable"
|
|
case svc.DeviceEvent:
|
|
return "DeviceEvent"
|
|
case svc.HardwareProfileChange:
|
|
return "HardwareProfileChange"
|
|
case svc.PowerEvent:
|
|
return "PowerEvent"
|
|
case svc.SessionChange:
|
|
return "SessionChange"
|
|
case svc.PreShutdown:
|
|
return "PreShutdown"
|
|
}
|
|
return fmt.Sprintf("Unknown-Service-Cmd-%d", c)
|
|
}
|
|
|
|
func beWindowsSubprocess() bool {
|
|
if beFirewallKillswitch() {
|
|
return true
|
|
}
|
|
|
|
if len(os.Args) != 3 || os.Args[1] != "/subproc" {
|
|
return false
|
|
}
|
|
logid := os.Args[2]
|
|
|
|
// Remove the date/time prefix; the logtail + file logggers add it.
|
|
log.SetFlags(0)
|
|
|
|
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
|
|
log.Printf("subproc mode: logid=%v", logid)
|
|
|
|
go func() {
|
|
b := make([]byte, 16)
|
|
for {
|
|
_, err := os.Stdin.Read(b)
|
|
if err != nil {
|
|
log.Fatalf("stdin err (parent process died): %v", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
err := startIPNServer(context.Background(), logid)
|
|
if err != nil {
|
|
log.Fatalf("ipnserver: %v", err)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func beFirewallKillswitch() bool {
|
|
if len(os.Args) != 3 || os.Args[1] != "/firewall" {
|
|
return false
|
|
}
|
|
|
|
log.SetFlags(0)
|
|
log.Printf("killswitch subprocess starting, tailscale GUID is %s", os.Args[2])
|
|
|
|
guid, err := windows.GUIDFromString(os.Args[2])
|
|
if err != nil {
|
|
log.Fatalf("invalid GUID %q: %v", os.Args[2], err)
|
|
}
|
|
|
|
luid, err := winipcfg.LUIDFromGUID(&guid)
|
|
if err != nil {
|
|
log.Fatalf("no interface with GUID %q: %v", guid, err)
|
|
}
|
|
|
|
start := time.Now()
|
|
fw, err := wf.New(uint64(luid))
|
|
if err != nil {
|
|
log.Fatalf("failed to enable firewall: %v", err)
|
|
}
|
|
log.Printf("killswitch enabled, took %s", time.Since(start))
|
|
|
|
// Note(maisem): when local lan access toggled, tailscaled needs to
|
|
// inform the firewall to let local routes through. The set of routes
|
|
// is passed in via stdin encoded in json.
|
|
dcd := json.NewDecoder(os.Stdin)
|
|
for {
|
|
var routes []netaddr.IPPrefix
|
|
if err := dcd.Decode(&routes); err != nil {
|
|
log.Fatalf("parent process died or requested exit, exiting (%v)", err)
|
|
}
|
|
if err := fw.UpdatePermittedRoutes(routes); err != nil {
|
|
log.Fatalf("failed to update routes (%v)", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func startIPNServer(ctx context.Context, logid string) error {
|
|
var logf logger.Logf = log.Printf
|
|
|
|
linkMon, err := monitor.New(logf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dialer := new(tsdial.Dialer)
|
|
|
|
getEngineRaw := func() (wgengine.Engine, error) {
|
|
dev, devName, err := tstun.New(logf, "Tailscale")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("TUN: %w", err)
|
|
}
|
|
r, err := router.New(logf, dev, nil)
|
|
if err != nil {
|
|
dev.Close()
|
|
return nil, fmt.Errorf("router: %w", err)
|
|
}
|
|
if wrapNetstack {
|
|
r = netstack.NewSubnetRouterWrapper(r)
|
|
}
|
|
d, err := dns.NewOSConfigurator(logf, devName)
|
|
if err != nil {
|
|
r.Close()
|
|
dev.Close()
|
|
return nil, fmt.Errorf("DNS: %w", err)
|
|
}
|
|
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
|
Tun: dev,
|
|
Router: r,
|
|
DNS: d,
|
|
ListenPort: 41641,
|
|
LinkMonitor: linkMon,
|
|
Dialer: dialer,
|
|
})
|
|
if err != nil {
|
|
r.Close()
|
|
dev.Close()
|
|
return nil, fmt.Errorf("engine: %w", err)
|
|
}
|
|
ns, err := newNetstack(logf, dialer, eng)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("newNetstack: %w", err)
|
|
}
|
|
ns.ProcessLocalIPs = false
|
|
ns.ProcessSubnets = wrapNetstack
|
|
if err := ns.Start(); err != nil {
|
|
return nil, fmt.Errorf("failed to start netstack: %w", err)
|
|
}
|
|
return wgengine.NewWatchdog(eng), nil
|
|
}
|
|
|
|
type engineOrError struct {
|
|
Engine wgengine.Engine
|
|
Err error
|
|
}
|
|
engErrc := make(chan engineOrError)
|
|
t0 := time.Now()
|
|
go func() {
|
|
const ms = time.Millisecond
|
|
for try := 1; ; try++ {
|
|
logf("tailscaled: getting engine... (try %v)", try)
|
|
t1 := time.Now()
|
|
eng, err := getEngineRaw()
|
|
d, dt := time.Since(t1).Round(ms), time.Since(t1).Round(ms)
|
|
if err != nil {
|
|
logf("tailscaled: engine fetch error (try %v) in %v (total %v, sysUptime %v): %v",
|
|
try, d, dt, windowsUptime().Round(time.Second), err)
|
|
} else {
|
|
if try > 1 {
|
|
logf("tailscaled: got engine on try %v in %v (total %v)", try, d, dt)
|
|
} else {
|
|
logf("tailscaled: got engine in %v", d)
|
|
}
|
|
}
|
|
timer := time.NewTimer(5 * time.Second)
|
|
engErrc <- engineOrError{eng, err}
|
|
if err == nil {
|
|
timer.Stop()
|
|
return
|
|
}
|
|
<-timer.C
|
|
}
|
|
}()
|
|
|
|
// getEngine is called by ipnserver to get the engine. It's
|
|
// not called concurrently and is not called again once it
|
|
// successfully returns an engine.
|
|
getEngine := func() (wgengine.Engine, error) {
|
|
if msg := envknob.String("TS_DEBUG_WIN_FAIL"); msg != "" {
|
|
return nil, fmt.Errorf("pretending to be a service failure: %v", msg)
|
|
}
|
|
for {
|
|
res := <-engErrc
|
|
if res.Engine != nil {
|
|
return res.Engine, nil
|
|
}
|
|
if time.Since(t0) < time.Minute || windowsUptime() < 10*time.Minute {
|
|
// Ignore errors during early boot. Windows 10 auto logs in the GUI
|
|
// way sooner than the networking stack components start up.
|
|
// So the network will fail for a bit (and require a few tries) while
|
|
// the GUI is still fine.
|
|
continue
|
|
}
|
|
// Return nicer errors to users, annotated with logids, which helps
|
|
// when they file bugs.
|
|
return nil, fmt.Errorf("%w\n\nlogid: %v", res.Err, logid)
|
|
}
|
|
}
|
|
store, err := store.New(logf, statePathOrDefault())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)
|
|
if err != nil {
|
|
return fmt.Errorf("safesocket.Listen: %v", err)
|
|
}
|
|
|
|
err = ipnserver.Run(ctx, logf, ln, store, linkMon, dialer, logid, getEngine, ipnServerOpts())
|
|
if err != nil {
|
|
logf("ipnserver.Run: %v", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func handleSessionChange(chgRequest svc.ChangeRequest) {
|
|
if chgRequest.Cmd != svc.SessionChange || chgRequest.EventType != windows.WTS_SESSION_UNLOCK {
|
|
return
|
|
}
|
|
|
|
log.Printf("Received WTS_SESSION_UNLOCK event, initiating DNS flush.")
|
|
go func() {
|
|
err := dns.Flush()
|
|
if err != nil {
|
|
log.Printf("Error flushing DNS on session unlock: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
var (
|
|
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
|
getTickCount64Proc = kernel32.NewProc("GetTickCount64")
|
|
)
|
|
|
|
func windowsUptime() time.Duration {
|
|
r, _, _ := getTickCount64Proc.Call()
|
|
return time.Duration(int64(r)) * time.Millisecond
|
|
}
|