Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31e1690f38 | ||
|
|
62d580f0e8 | ||
|
|
387a98fe28 | ||
|
|
f66dc8dc0a | ||
|
|
f9fafe269a | ||
|
|
087260734b | ||
|
|
561e7b61c3 | ||
|
|
9e71851a36 | ||
|
|
4f62a2ed99 | ||
|
|
f737496d7c | ||
|
|
9107b5eadf | ||
|
|
e94d345e26 | ||
|
|
7c7f60be22 | ||
|
|
baa1fd976e | ||
|
|
42abf13843 | ||
|
|
b4be4f089f | ||
|
|
95671b71a6 | ||
|
|
ef596aed9b | ||
|
|
237b4b5a2a | ||
|
|
131518eed1 | ||
|
|
1873bc471b | ||
|
|
19e5f242e0 | ||
|
|
8326fdd60f | ||
|
|
143bda87a3 | ||
|
|
5f3cdaf283 | ||
|
|
741d7bcefe | ||
|
|
a7e4cebb90 | ||
|
|
d79e0fde9c | ||
|
|
e0a4a02b35 | ||
|
|
21b6d373b0 | ||
|
|
32194cdc70 | ||
|
|
f5a7551382 | ||
|
|
d3bc575f35 | ||
|
|
6f69fe8ad7 | ||
|
|
269a498c1e | ||
|
|
b2ae8fdf80 | ||
|
|
514539b611 | ||
|
|
593c086866 | ||
|
|
7df6f8736a | ||
|
|
35d7b3aa27 | ||
|
|
c53ee37912 | ||
|
|
f232d4554a | ||
|
|
62d08d26b6 |
28
.github/workflows/checklocks.yml
vendored
Normal file
28
.github/workflows/checklocks.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: checklocks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/*.go'
|
||||
- '.github/workflows/checklocks.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
checklocks:
|
||||
runs-on: [ ubuntu-latest ]
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build checklocks
|
||||
run: ./tool/go build -o /tmp/checklocks gvisor.dev/gvisor/tools/checklocks/cmd/checklocks
|
||||
|
||||
- name: Run checklocks vet
|
||||
# TODO: remove || true once we have applied checklocks annotations everywhere.
|
||||
run: ./tool/go vet -vettool=/tmp/checklocks ./... || true
|
||||
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@@ -22,8 +22,7 @@ on:
|
||||
- "main"
|
||||
- "release-branch/*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
# all PRs on all branches
|
||||
merge_group:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.51.0
|
||||
1.52.0
|
||||
|
||||
@@ -96,13 +96,13 @@ type browserSession struct {
|
||||
// the user has authenticated the session) and the session is not
|
||||
// expired.
|
||||
// 2023-10-05: Sessions expire by default 30 days after creation.
|
||||
func (s *browserSession) isAuthorized() bool {
|
||||
func (s *browserSession) isAuthorized(now time.Time) bool {
|
||||
switch {
|
||||
case s == nil:
|
||||
return false
|
||||
case !s.Authenticated:
|
||||
return false // awaiting auth
|
||||
case s.isExpired():
|
||||
case s.isExpired(now):
|
||||
return false // expired
|
||||
}
|
||||
return true
|
||||
@@ -110,8 +110,8 @@ func (s *browserSession) isAuthorized() bool {
|
||||
|
||||
// isExpired reports true if s is expired.
|
||||
// 2023-10-05: Sessions expire by default 30 days after creation.
|
||||
func (s *browserSession) isExpired() bool {
|
||||
return !s.Created.IsZero() && time.Now().After(s.expires()) // TODO: use Server.timeNow field
|
||||
func (s *browserSession) isExpired(now time.Time) bool {
|
||||
return !s.Created.IsZero() && now.After(s.expires())
|
||||
}
|
||||
|
||||
// expires reports when the given session expires.
|
||||
@@ -146,6 +146,7 @@ func NewServer(opts ServerOpts) (s *Server, cleanup func()) {
|
||||
s = &Server{
|
||||
devMode: opts.DevMode,
|
||||
lc: opts.LocalClient,
|
||||
cgiMode: opts.CGIMode,
|
||||
pathPrefix: opts.PathPrefix,
|
||||
timeNow: opts.TimeNow,
|
||||
}
|
||||
@@ -241,7 +242,7 @@ func (s *Server) authorizeRequest(w http.ResponseWriter, r *http.Request) (ok bo
|
||||
// should try and use the above call instead of running another
|
||||
// localapi request.
|
||||
session, _, err := s.getTailscaleBrowserSession(r)
|
||||
if err != nil || !session.isAuthorized() {
|
||||
if err != nil || !session.isAuthorized(s.timeNow()) {
|
||||
http.Error(w, "no valid session", http.StatusUnauthorized)
|
||||
return false
|
||||
}
|
||||
@@ -288,10 +289,11 @@ func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var (
|
||||
errNoSession = errors.New("no-browser-session")
|
||||
errNotUsingTailscale = errors.New("not-using-tailscale")
|
||||
errTaggedSource = errors.New("tagged-source")
|
||||
errNotOwner = errors.New("not-owner")
|
||||
errNoSession = errors.New("no-browser-session")
|
||||
errNotUsingTailscale = errors.New("not-using-tailscale")
|
||||
errTaggedRemoteSource = errors.New("tagged-remote-source")
|
||||
errTaggedLocalSource = errors.New("tagged-local-source")
|
||||
errNotOwner = errors.New("not-owner")
|
||||
)
|
||||
|
||||
// getTailscaleBrowserSession retrieves the browser session associated with
|
||||
@@ -303,8 +305,13 @@ var (
|
||||
//
|
||||
// - (errNoSession) The request does not have a session.
|
||||
//
|
||||
// - (errTaggedSource) The source is a tagged node. Users must use their
|
||||
// own user-owned devices to manage other nodes' web clients.
|
||||
// - (errTaggedRemoteSource) The source is remote (another node) and tagged.
|
||||
// Users must use their own user-owned devices to manage other nodes'
|
||||
// web clients.
|
||||
//
|
||||
// - (errTaggedLocalSource) The source is local (the same node) and tagged.
|
||||
// Tagged nodes can only be remotely managed, allowing ACLs to dictate
|
||||
// access to web clients.
|
||||
//
|
||||
// - (errNotOwner) The source is not the owner of this client (if the
|
||||
// client is user-owned). Only the owner is allowed to manage the
|
||||
@@ -317,26 +324,25 @@ var (
|
||||
// The WhoIsResponse is always populated, with a non-nil Node and UserProfile,
|
||||
// unless getTailscaleBrowserSession reports errNotUsingTailscale.
|
||||
func (s *Server) getTailscaleBrowserSession(r *http.Request) (*browserSession, *apitype.WhoIsResponse, error) {
|
||||
whoIs, err := s.lc.WhoIs(r.Context(), r.RemoteAddr)
|
||||
whoIs, whoIsErr := s.lc.WhoIs(r.Context(), r.RemoteAddr)
|
||||
status, statusErr := s.lc.StatusWithoutPeers(r.Context())
|
||||
switch {
|
||||
case err != nil:
|
||||
case whoIsErr != nil:
|
||||
return nil, nil, errNotUsingTailscale
|
||||
case statusErr != nil:
|
||||
return nil, whoIs, statusErr
|
||||
case status.Self == nil:
|
||||
return nil, whoIs, errors.New("missing self node in tailscale status")
|
||||
case whoIs.Node.IsTagged() && whoIs.Node.StableID == status.Self.ID:
|
||||
return nil, whoIs, errTaggedLocalSource
|
||||
case whoIs.Node.IsTagged():
|
||||
return nil, whoIs, errTaggedSource
|
||||
return nil, whoIs, errTaggedRemoteSource
|
||||
case !status.Self.IsTagged() && status.Self.UserID != whoIs.UserProfile.ID:
|
||||
return nil, whoIs, errNotOwner
|
||||
}
|
||||
srcNode := whoIs.Node.ID
|
||||
srcUser := whoIs.UserProfile.ID
|
||||
|
||||
status, err := s.lc.StatusWithoutPeers(r.Context())
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, whoIs, err
|
||||
case status.Self == nil:
|
||||
return nil, whoIs, errors.New("missing self node in tailscale status")
|
||||
case !status.Self.IsTagged() && status.Self.UserID != srcUser:
|
||||
return nil, whoIs, errNotOwner
|
||||
}
|
||||
|
||||
cookie, err := r.Cookie(sessionCookieName)
|
||||
if errors.Is(err, http.ErrNoCookie) {
|
||||
return nil, whoIs, errNoSession
|
||||
@@ -353,7 +359,7 @@ func (s *Server) getTailscaleBrowserSession(r *http.Request) (*browserSession, *
|
||||
// Maybe the source browser's machine was logged out and then back in as a different node.
|
||||
// Return errNoSession because there is no session for this user.
|
||||
return nil, whoIs, errNoSession
|
||||
} else if session.isExpired() {
|
||||
} else if session.isExpired(s.timeNow()) {
|
||||
// Session expired, remove from session map and return errNoSession.
|
||||
s.browserSessions.Delete(session.ID)
|
||||
return nil, whoIs, errNoSession
|
||||
@@ -408,7 +414,7 @@ func (s *Server) serveTailscaleAuth(w http.ResponseWriter, r *http.Request) {
|
||||
Expires: session.expires(),
|
||||
})
|
||||
resp = authResponse{OK: false, AuthURL: d.URL}
|
||||
case !session.isAuthorized():
|
||||
case !session.isAuthorized(s.timeNow()):
|
||||
if r.URL.Query().Get("wait") == "true" {
|
||||
// Client requested we block until user completes auth.
|
||||
d, err := s.getOrAwaitAuth(r.Context(), session.AuthID, whois.Node.ID)
|
||||
@@ -425,7 +431,7 @@ func (s *Server) serveTailscaleAuth(w http.ResponseWriter, r *http.Request) {
|
||||
s.browserSessions.Store(session.ID, session)
|
||||
}
|
||||
}
|
||||
if session.isAuthorized() {
|
||||
if session.isAuthorized(s.timeNow()) {
|
||||
resp = authResponse{OK: true}
|
||||
} else {
|
||||
resp = authResponse{OK: false, AuthURL: session.AuthURL}
|
||||
|
||||
@@ -151,15 +151,15 @@ func TestGetTailscaleBrowserSession(t *testing.T) {
|
||||
tags := views.SliceOf([]string{"tag:server"})
|
||||
tailnetNodes := map[string]*apitype.WhoIsResponse{
|
||||
userANodeIP: {
|
||||
Node: &tailcfg.Node{ID: 1},
|
||||
Node: &tailcfg.Node{ID: 1, StableID: "1"},
|
||||
UserProfile: userA,
|
||||
},
|
||||
userBNodeIP: {
|
||||
Node: &tailcfg.Node{ID: 2},
|
||||
Node: &tailcfg.Node{ID: 2, StableID: "2"},
|
||||
UserProfile: userB,
|
||||
},
|
||||
taggedNodeIP: {
|
||||
Node: &tailcfg.Node{ID: 3, Tags: tags.AsSlice()},
|
||||
Node: &tailcfg.Node{ID: 3, StableID: "3", Tags: tags.AsSlice()},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -169,7 +169,10 @@ func TestGetTailscaleBrowserSession(t *testing.T) {
|
||||
defer localapi.Close()
|
||||
go localapi.Serve(lal)
|
||||
|
||||
s := &Server{lc: &tailscale.LocalClient{Dial: lal.Dial}}
|
||||
s := &Server{
|
||||
timeNow: time.Now,
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
}
|
||||
|
||||
// Add some browser sessions to cache state.
|
||||
userASession := &browserSession{
|
||||
@@ -237,11 +240,26 @@ func TestGetTailscaleBrowserSession(t *testing.T) {
|
||||
wantError: errNotOwner,
|
||||
},
|
||||
{
|
||||
name: "tagged-source",
|
||||
name: "tagged-remote-source",
|
||||
selfNode: &ipnstate.PeerStatus{ID: "self", UserID: userA.ID},
|
||||
remoteAddr: taggedNodeIP,
|
||||
wantSession: nil,
|
||||
wantError: errTaggedSource,
|
||||
wantError: errTaggedRemoteSource,
|
||||
},
|
||||
{
|
||||
name: "tagged-local-source",
|
||||
selfNode: &ipnstate.PeerStatus{ID: "3"},
|
||||
remoteAddr: taggedNodeIP, // same node as selfNode
|
||||
wantSession: nil,
|
||||
wantError: errTaggedLocalSource,
|
||||
},
|
||||
{
|
||||
name: "not-tagged-local-source",
|
||||
selfNode: &ipnstate.PeerStatus{ID: "1", UserID: userA.ID},
|
||||
remoteAddr: userANodeIP, // same node as selfNode
|
||||
cookie: userASession.ID,
|
||||
wantSession: userASession,
|
||||
wantError: nil, // should not error
|
||||
},
|
||||
{
|
||||
name: "has-session",
|
||||
@@ -291,7 +309,7 @@ func TestGetTailscaleBrowserSession(t *testing.T) {
|
||||
if diff := cmp.Diff(session, tt.wantSession); diff != "" {
|
||||
t.Errorf("wrong session; (-got+want):%v", diff)
|
||||
}
|
||||
if gotIsAuthorized := session.isAuthorized(); gotIsAuthorized != tt.wantIsAuthorized {
|
||||
if gotIsAuthorized := session.isAuthorized(s.timeNow()); gotIsAuthorized != tt.wantIsAuthorized {
|
||||
t.Errorf("wrong isAuthorized; want=%v, got=%v", tt.wantIsAuthorized, gotIsAuthorized)
|
||||
}
|
||||
})
|
||||
@@ -321,6 +339,7 @@ func TestAuthorizeRequest(t *testing.T) {
|
||||
s := &Server{
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
tsDebugMode: "full",
|
||||
timeNow: time.Now,
|
||||
}
|
||||
validCookie := "ts-cookie"
|
||||
s.browserSessions.Store(validCookie, &browserSession{
|
||||
|
||||
@@ -86,6 +86,10 @@ type Arguments struct {
|
||||
// PkgsAddr is the address of the pkgs server to fetch updates from.
|
||||
// Defaults to "https://pkgs.tailscale.com".
|
||||
PkgsAddr string
|
||||
// ForAutoUpdate should be true when Updater is created in auto-update
|
||||
// context. When true, NewUpdater returns an error if it cannot be used for
|
||||
// auto-updates (even if Updater.Update field is non-nil).
|
||||
ForAutoUpdate bool
|
||||
}
|
||||
|
||||
func (args Arguments) validate() error {
|
||||
@@ -116,10 +120,14 @@ func NewUpdater(args Arguments) (*Updater, error) {
|
||||
if up.Stderr == nil {
|
||||
up.Stderr = os.Stderr
|
||||
}
|
||||
up.Update = up.getUpdateFunction()
|
||||
var canAutoUpdate bool
|
||||
up.Update, canAutoUpdate = up.getUpdateFunction()
|
||||
if up.Update == nil {
|
||||
return nil, errors.ErrUnsupported
|
||||
}
|
||||
if args.ForAutoUpdate && !canAutoUpdate {
|
||||
return nil, errors.ErrUnsupported
|
||||
}
|
||||
switch up.Version {
|
||||
case StableTrack, UnstableTrack:
|
||||
up.track = up.Version
|
||||
@@ -144,52 +152,70 @@ func NewUpdater(args Arguments) (*Updater, error) {
|
||||
|
||||
type updateFunction func() error
|
||||
|
||||
func (up *Updater) getUpdateFunction() updateFunction {
|
||||
func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return up.updateWindows
|
||||
return up.updateWindows, true
|
||||
case "linux":
|
||||
switch distro.Get() {
|
||||
case distro.Synology:
|
||||
return up.updateSynology
|
||||
// Synology updates use our own pkgs.tailscale.com instead of the
|
||||
// Synology Package Center. We should eventually get to a regular
|
||||
// release cadence with Synology Package Center and use their
|
||||
// auto-update mechanism.
|
||||
return up.updateSynology, false
|
||||
case distro.Debian: // includes Ubuntu
|
||||
return up.updateDebLike
|
||||
return up.updateDebLike, true
|
||||
case distro.Arch:
|
||||
return up.updateArchLike
|
||||
if up.archPackageInstalled() {
|
||||
// Arch update func just prints a message about how to update,
|
||||
// it doesn't support auto-updates.
|
||||
return up.updateArchLike, false
|
||||
}
|
||||
return up.updateLinuxBinary, true
|
||||
case distro.Alpine:
|
||||
return up.updateAlpineLike
|
||||
return up.updateAlpineLike, true
|
||||
}
|
||||
switch {
|
||||
case haveExecutable("pacman"):
|
||||
return up.updateArchLike
|
||||
if up.archPackageInstalled() {
|
||||
// Arch update func just prints a message about how to update,
|
||||
// it doesn't support auto-updates.
|
||||
return up.updateArchLike, false
|
||||
}
|
||||
return up.updateLinuxBinary, true
|
||||
case haveExecutable("apt-get"): // TODO(awly): add support for "apt"
|
||||
// The distro.Debian switch case above should catch most apt-based
|
||||
// systems, but add this fallback just in case.
|
||||
return up.updateDebLike
|
||||
return up.updateDebLike, true
|
||||
case haveExecutable("dnf"):
|
||||
return up.updateFedoraLike("dnf")
|
||||
return up.updateFedoraLike("dnf"), true
|
||||
case haveExecutable("yum"):
|
||||
return up.updateFedoraLike("yum")
|
||||
return up.updateFedoraLike("yum"), true
|
||||
case haveExecutable("apk"):
|
||||
return up.updateAlpineLike
|
||||
return up.updateAlpineLike, true
|
||||
}
|
||||
// If nothing matched, fall back to tarball updates.
|
||||
if up.Update == nil {
|
||||
return up.updateLinuxBinary
|
||||
return up.updateLinuxBinary, true
|
||||
}
|
||||
case "darwin":
|
||||
switch {
|
||||
case version.IsMacAppStore():
|
||||
return up.updateMacAppStore
|
||||
// App store update func just opens the store page, it doesn't
|
||||
// support auto-updates.
|
||||
return up.updateMacAppStore, false
|
||||
case version.IsMacSysExt():
|
||||
return up.updateMacSys
|
||||
// Macsys update func kicks off Sparkle. Auto-updates are done by
|
||||
// Sparkle.
|
||||
return up.updateMacSys, false
|
||||
default:
|
||||
return nil
|
||||
return nil, false
|
||||
}
|
||||
case "freebsd":
|
||||
return up.updateFreeBSD
|
||||
return up.updateFreeBSD, true
|
||||
}
|
||||
return nil
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Update runs a single update attempt using the platform-specific mechanism.
|
||||
@@ -246,12 +272,12 @@ func (up *Updater) updateSynology() error {
|
||||
return fmt.Errorf("cannot find Synology package for os=%s arch=%s, please report a bug with your device model", osName, arch)
|
||||
}
|
||||
|
||||
if !up.confirm(latest.SPKsVersion) {
|
||||
return nil
|
||||
}
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !up.confirm(latest.SPKsVersion) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Download the SPK into a temporary directory.
|
||||
spkDir, err := os.MkdirTemp("", "tailscale-update")
|
||||
@@ -454,12 +480,12 @@ func updateDebianAptSourcesListBytes(was []byte, dstTrack string) (newContent []
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (up *Updater) archPackageInstalled() bool {
|
||||
err := exec.Command("pacman", "--query", "tailscale").Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (up *Updater) updateArchLike() error {
|
||||
if err := exec.Command("pacman", "--query", "tailscale").Run(); err != nil && isExitError(err) {
|
||||
// Tailscale was not installed via pacman, update via tarball download
|
||||
// instead.
|
||||
return up.updateLinuxBinary()
|
||||
}
|
||||
// Arch maintainer asked us not to implement "tailscale update" or
|
||||
// auto-updates on Arch-based distros:
|
||||
// https://github.com/tailscale/tailscale/issues/6995#issuecomment-1687080106
|
||||
@@ -690,12 +716,12 @@ func (up *Updater) updateWindows() error {
|
||||
arch = "x86"
|
||||
}
|
||||
|
||||
if !up.confirm(ver) {
|
||||
return nil
|
||||
}
|
||||
if !winutil.IsCurrentProcessElevated() {
|
||||
return errors.New("must be run as Administrator")
|
||||
}
|
||||
if !up.confirm(ver) {
|
||||
return nil
|
||||
}
|
||||
|
||||
tsDir := filepath.Join(os.Getenv("ProgramData"), "Tailscale")
|
||||
msiDir := filepath.Join(tsDir, "MSICache")
|
||||
@@ -888,13 +914,13 @@ func (up *Updater) updateLinuxBinary() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !up.confirm(ver) {
|
||||
return nil
|
||||
}
|
||||
// Root is needed to overwrite binaries and restart systemd unit.
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !up.confirm(ver) {
|
||||
return nil
|
||||
}
|
||||
|
||||
dlPath, err := up.downloadLinuxTarball(ver)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,11 +2,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
|
||||
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
|
||||
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
|
||||
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
@@ -43,6 +38,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus
|
||||
LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs
|
||||
LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs
|
||||
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio
|
||||
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
|
||||
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
|
||||
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
@@ -111,7 +111,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/wsconn from tailscale.com/cmd/derper+
|
||||
tailscale.com/paths from tailscale.com/client/tailscale
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale
|
||||
tailscale.com/syncs from tailscale.com/cmd/derper+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
@@ -88,10 +87,6 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
|
||||
port64, err := strconv.ParseUint(args[0], 10, 16)
|
||||
if err != nil {
|
||||
@@ -103,11 +98,15 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
|
||||
// Don't block from turning off existing Funnel if
|
||||
// network configuration/capabilities have changed.
|
||||
// Only block from starting new Funnels.
|
||||
if err := e.verifyFunnelEnabled(ctx, st, port); err != nil {
|
||||
if err := e.verifyFunnelEnabled(ctx, port); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
hp := ipn.HostPort(dnsName + ":" + strconv.Itoa(int(port)))
|
||||
if on == sc.AllowFunnel[hp] {
|
||||
@@ -141,13 +140,7 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
|
||||
// If an error is reported, the CLI should stop execution and return the error.
|
||||
//
|
||||
// verifyFunnelEnabled may refresh the local state and modify the st input.
|
||||
func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, st *ipnstate.Status, port uint16) error {
|
||||
hasFunnelAttrs := func(selfNode *ipnstate.PeerStatus) bool {
|
||||
return selfNode.HasCap(tailcfg.CapabilityHTTPS) && selfNode.HasCap(tailcfg.NodeAttrFunnel)
|
||||
}
|
||||
if hasFunnelAttrs(st.Self) {
|
||||
return nil // already enabled
|
||||
}
|
||||
func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, port uint16) error {
|
||||
enableErr := e.enableFeatureInteractive(ctx, "funnel", tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel)
|
||||
st, statusErr := e.getLocalClientStatusWithoutPeers(ctx) // get updated status; interactive flow may block
|
||||
switch {
|
||||
|
||||
@@ -159,10 +159,10 @@ type serveEnv struct {
|
||||
// v2 specific flags
|
||||
bg bool // background mode
|
||||
setPath string // serve path
|
||||
https string // HTTP port
|
||||
http string // HTTP port
|
||||
tcp string // TCP port
|
||||
tlsTerminatedTCP string // a TLS terminated TCP port
|
||||
https uint // HTTP port
|
||||
http uint // HTTP port
|
||||
tcp uint // TCP port
|
||||
tlsTerminatedTCP uint // a TLS terminated TCP port
|
||||
subcmd serveMode // subcommand
|
||||
yes bool // update without prompt
|
||||
|
||||
@@ -171,6 +171,7 @@ type serveEnv struct {
|
||||
// optional stuff for tests:
|
||||
testFlagOut io.Writer
|
||||
testStdout io.Writer
|
||||
testStderr io.Writer
|
||||
}
|
||||
|
||||
// getSelfDNSName returns the DNS name of the current node.
|
||||
@@ -681,13 +682,6 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *serveEnv) stdout() io.Writer {
|
||||
if e.testStdout != nil {
|
||||
return e.testStdout
|
||||
}
|
||||
return os.Stdout
|
||||
}
|
||||
|
||||
func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.Status) error {
|
||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
for p, h := range sc.TCP {
|
||||
@@ -824,6 +818,24 @@ func parseServePort(s string) (uint16, error) {
|
||||
// 2023-08-09: The only valid feature values are "serve" and "funnel".
|
||||
// This can be moved to some CLI lib when expanded past serve/funnel.
|
||||
func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, caps ...tailcfg.NodeCapability) (err error) {
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
if st.Self == nil {
|
||||
return errors.New("no self node")
|
||||
}
|
||||
hasCaps := func() bool {
|
||||
for _, c := range caps {
|
||||
if !st.Self.HasCap(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
if hasCaps() {
|
||||
return nil // already enabled
|
||||
}
|
||||
info, err := e.lc.QueryFeature(ctx, feature)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -786,7 +786,7 @@ func TestVerifyFunnelEnabled(t *testing.T) {
|
||||
{
|
||||
name: "fallback-flow-enabled",
|
||||
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")},
|
||||
caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel},
|
||||
caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel, "https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090"},
|
||||
wantErr: "", // no error, success
|
||||
},
|
||||
{
|
||||
@@ -811,10 +811,6 @@ func TestVerifyFunnelEnabled(t *testing.T) {
|
||||
defer func() { fakeStatus.Self.Capabilities = oldCaps }() // reset after test
|
||||
fakeStatus.Self.Capabilities = tt.caps
|
||||
}
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
r := recover()
|
||||
@@ -826,7 +822,7 @@ func TestVerifyFunnelEnabled(t *testing.T) {
|
||||
t.Errorf("wrong panic; got=%s, want=%s", gotPanic, tt.wantPanic)
|
||||
}
|
||||
}()
|
||||
gotErr := e.verifyFunnelEnabled(ctx, st, 443)
|
||||
gotErr := e.verifyFunnelEnabled(ctx, 443)
|
||||
var got string
|
||||
if gotErr != nil {
|
||||
got = gotErr.Error()
|
||||
|
||||
@@ -5,11 +5,13 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -45,16 +47,13 @@ a partial URL (e.g., localhost:3000), or a full URL including a path (e.g., http
|
||||
|
||||
EXAMPLES
|
||||
- Expose an HTTP server running at 127.0.0.1:3000 in the foreground:
|
||||
$ tailscale %s 3000
|
||||
$ tailscale %[1]s 3000
|
||||
|
||||
- Expose an HTTP server running at 127.0.0.1:3000 in the background:
|
||||
$ tailscale %s --bg 3000
|
||||
|
||||
- Expose an HTTPS server with a valid certificate at https://localhost:8443
|
||||
$ tailscale %s https://localhost:8443
|
||||
$ tailscale %[1]s --bg 3000
|
||||
|
||||
- Expose an HTTPS server with invalid or self-signed certificates at https://localhost:8443
|
||||
$ tailscale %s https+insecure://localhost:8443
|
||||
$ tailscale %[1]s https+insecure://localhost:8443
|
||||
|
||||
For more examples and use cases visit our docs site https://tailscale.com/kb/1247/funnel-serve-use-cases
|
||||
`)
|
||||
@@ -102,6 +101,12 @@ func buildShortUsage(subcmd string) string {
|
||||
}, "\n ")
|
||||
}
|
||||
|
||||
// errHelpFunc is standard error text that prompts users to
|
||||
// run `$subcmd --help` for information on how to use serve.
|
||||
var errHelpFunc = func(m serveMode) error {
|
||||
return fmt.Errorf("try `tailscale %s --help` for usage info", infoMap[m].Name)
|
||||
}
|
||||
|
||||
// newServeV2Command returns a new "serve" subcommand using e as its environment.
|
||||
func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
if subcmd != serve && subcmd != funnel {
|
||||
@@ -118,19 +123,21 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
fmt.Sprintf("%s status [--json]", info.Name),
|
||||
fmt.Sprintf("%s reset", info.Name),
|
||||
}, "\n "),
|
||||
LongHelp: info.LongHelp + fmt.Sprintf(strings.TrimSpace(serveHelpCommon), info.Name, info.Name),
|
||||
LongHelp: info.LongHelp + fmt.Sprintf(strings.TrimSpace(serveHelpCommon), info.Name),
|
||||
Exec: e.runServeCombined(subcmd),
|
||||
|
||||
FlagSet: e.newFlags("serve-set", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.bg, "bg", false, "Run the command as a background process")
|
||||
fs.BoolVar(&e.bg, "bg", false, "Run the command as a background process (default false)")
|
||||
fs.StringVar(&e.setPath, "set-path", "", "Appends the specified path to the base URL for accessing the underlying service")
|
||||
fs.StringVar(&e.https, "https", "", "Expose an HTTPS server at the specified port (default")
|
||||
fs.StringVar(&e.http, "http", "", "Expose an HTTP server at the specified port")
|
||||
fs.StringVar(&e.tcp, "tcp", "", "Expose a TCP forwarder to forward raw TCP packets at the specified port")
|
||||
fs.StringVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", "", "Expose a TCP forwarder to forward TLS-terminated TCP packets at the specified port")
|
||||
fs.BoolVar(&e.yes, "yes", false, "Update without interactive prompts")
|
||||
fs.UintVar(&e.https, "https", 0, "Expose an HTTPS server at the specified port (default mode)")
|
||||
if subcmd == serve {
|
||||
fs.UintVar(&e.http, "http", 0, "Expose an HTTP server at the specified port")
|
||||
}
|
||||
fs.UintVar(&e.tcp, "tcp", 0, "Expose a TCP forwarder to forward raw TCP packets at the specified port")
|
||||
fs.UintVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", 0, "Expose a TCP forwarder to forward TLS-terminated TCP packets at the specified port")
|
||||
fs.BoolVar(&e.yes, "yes", false, "Update without interactive prompts (default false)")
|
||||
}),
|
||||
UsageFunc: usageFunc,
|
||||
UsageFunc: usageFuncNoDefaultValues,
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "status",
|
||||
@@ -152,20 +159,31 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func validateArgs(subcmd serveMode, args []string) error {
|
||||
switch len(args) {
|
||||
case 0:
|
||||
return flag.ErrHelp
|
||||
case 1, 2:
|
||||
if isLegacyInvocation(subcmd, args) {
|
||||
fmt.Fprintf(os.Stderr, "error: the CLI for serve and funnel has changed.")
|
||||
fmt.Fprintf(os.Stderr, "Please see https://tailscale.com/kb/1242/tailscale-serve for more information.")
|
||||
return errHelp
|
||||
func (e *serveEnv) validateArgs(subcmd serveMode, args []string) error {
|
||||
if translation, ok := isLegacyInvocation(subcmd, args); ok {
|
||||
fmt.Fprint(e.stderr(), "Error: the CLI for serve and funnel has changed.")
|
||||
if translation != "" {
|
||||
fmt.Fprint(e.stderr(), " You can run the following command instead:\n")
|
||||
fmt.Fprintf(e.stderr(), "\t- %s\n", translation)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: invalid number of arguments (%d)", len(args))
|
||||
return errHelp
|
||||
fmt.Fprint(e.stderr(), "\nPlease see https://tailscale.com/kb/1242/tailscale-serve for more information.\n")
|
||||
return errHelpFunc(subcmd)
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
if len(args) > 2 {
|
||||
fmt.Fprintf(e.stderr(), "Error: invalid number of arguments (%d)\n", len(args))
|
||||
return errHelpFunc(subcmd)
|
||||
}
|
||||
turnOff := args[len(args)-1] == "off"
|
||||
if len(args) == 2 && !turnOff {
|
||||
fmt.Fprintln(e.stderr(), "Error: invalid argument format")
|
||||
return errHelpFunc(subcmd)
|
||||
}
|
||||
|
||||
// Given the two checks above, we can assume there
|
||||
// are only 1 or 2 arguments which is valid.
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -174,22 +192,31 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
e.subcmd = subcmd
|
||||
|
||||
return func(ctx context.Context, args []string) error {
|
||||
if err := validateArgs(subcmd, args); err != nil {
|
||||
// Undocumented debug command (not using ffcli subcommands) to set raw
|
||||
// configs from stdin for now (2022-11-13).
|
||||
if len(args) == 1 && args[0] == "set-raw" {
|
||||
valb, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc := new(ipn.ServeConfig)
|
||||
if err := json.Unmarshal(valb, sc); err != nil {
|
||||
return fmt.Errorf("invalid JSON: %w", err)
|
||||
}
|
||||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
|
||||
if err := e.validateArgs(subcmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
|
||||
funnel := subcmd == funnel
|
||||
if funnel {
|
||||
// verify node has funnel capabilities
|
||||
if err := e.verifyFunnelEnabled(ctx, st, 443); err != nil {
|
||||
if err := e.verifyFunnelEnabled(ctx, 443); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -199,18 +226,10 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
return fmt.Errorf("failed to clean the mount point: %w", err)
|
||||
}
|
||||
|
||||
if e.setPath != "" {
|
||||
// TODO(marwan-at-work): either
|
||||
// 1. Warn the user that this is a side effect.
|
||||
// 2. Force the user to pass --bg
|
||||
// 3. Allow set-path to be in the foreground.
|
||||
e.bg = true
|
||||
}
|
||||
|
||||
srvType, srvPort, err := srvTypeAndPortFromFlags(e)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n\n", err)
|
||||
return errHelp
|
||||
fmt.Fprintf(e.stderr(), "error: %v\n\n", err)
|
||||
return errHelpFunc(subcmd)
|
||||
}
|
||||
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
@@ -222,6 +241,10 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
|
||||
// set parent serve config to always be persisted
|
||||
@@ -247,7 +270,13 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
}
|
||||
|
||||
var watcher *tailscale.IPNBusWatcher
|
||||
if !e.bg && !turnOff {
|
||||
wantFg := !e.bg && !turnOff
|
||||
if wantFg {
|
||||
// validate the config before creating a WatchIPNBus session
|
||||
if err := e.validateConfig(parentSC, srvPort, srvType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if foreground mode, create a WatchIPNBus session
|
||||
// and use the nested config for all following operations
|
||||
// TODO(marwan-at-work): nested-config validations should happen here or previous to this point.
|
||||
@@ -279,19 +308,19 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
msg = e.messageForPort(sc, st, dnsName, srvType, srvPort)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n\n", err)
|
||||
return errHelp
|
||||
fmt.Fprintf(e.stderr(), "error: %v\n\n", err)
|
||||
return errHelpFunc(subcmd)
|
||||
}
|
||||
|
||||
if err := e.lc.SetServeConfig(ctx, parentSC); err != nil {
|
||||
if tailscale.IsPreconditionsFailedError(err) {
|
||||
fmt.Fprintln(os.Stderr, "Another client is changing the serve config; please try again.")
|
||||
fmt.Fprintln(e.stderr(), "Another client is changing the serve config; please try again.")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if msg != "" {
|
||||
fmt.Fprintln(os.Stderr, msg)
|
||||
fmt.Fprintln(e.stdout(), msg)
|
||||
}
|
||||
|
||||
if watcher != nil {
|
||||
@@ -310,6 +339,8 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
}
|
||||
}
|
||||
|
||||
const backgroundExistsMsg = "background configuration already exists, use `tailscale %s --%s=%d off` to remove the existing configuration"
|
||||
|
||||
func (e *serveEnv) validateConfig(sc *ipn.ServeConfig, port uint16, wantServe serveType) error {
|
||||
sc, isFg := findConfig(sc, port)
|
||||
if sc == nil {
|
||||
@@ -319,7 +350,7 @@ func (e *serveEnv) validateConfig(sc *ipn.ServeConfig, port uint16, wantServe se
|
||||
return errors.New("foreground already exists under this port")
|
||||
}
|
||||
if !e.bg {
|
||||
return errors.New("background serve already exists under this port")
|
||||
return fmt.Errorf(backgroundExistsMsg, infoMap[e.subcmd].Name, wantServe.String(), port)
|
||||
}
|
||||
existingServe := serveFromPortHandler(sc.TCP[port])
|
||||
if wantServe != existingServe {
|
||||
@@ -371,6 +402,10 @@ func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName st
|
||||
return fmt.Errorf("failed apply web serve: %w", err)
|
||||
}
|
||||
case serveTypeTCP, serveTypeTLSTerminatedTCP:
|
||||
if e.setPath != "" {
|
||||
return fmt.Errorf("cannot mount a path for TCP serve")
|
||||
}
|
||||
|
||||
err := e.applyTCPServe(sc, dnsName, srvType, srvPort, target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply TCP serve: %w", err)
|
||||
@@ -405,7 +440,7 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
} else {
|
||||
output.WriteString(msgServeAvailable)
|
||||
}
|
||||
output.WriteString("\n")
|
||||
output.WriteString("\n\n")
|
||||
|
||||
scheme := "https"
|
||||
if sc.IsServingHTTP(srvPort) {
|
||||
@@ -418,13 +453,6 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
portPart = ""
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("%s://%s%s\n\n", scheme, dnsName, portPart))
|
||||
|
||||
if !e.bg {
|
||||
output.WriteString(msgToExit)
|
||||
return output.String()
|
||||
}
|
||||
|
||||
srvTypeAndDesc := func(h *ipn.HTTPHandler) (string, string) {
|
||||
switch {
|
||||
case h.Path != "":
|
||||
@@ -446,12 +474,12 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
sort.Slice(mounts, func(i, j int) bool {
|
||||
return len(mounts[i]) < len(mounts[j])
|
||||
})
|
||||
maxLen := len(mounts[len(mounts)-1])
|
||||
|
||||
for _, m := range mounts {
|
||||
h := sc.Web[hp].Handlers[m]
|
||||
t, d := srvTypeAndDesc(h)
|
||||
output.WriteString(fmt.Sprintf("%s %s%s %-5s %s\n", "|--", m, strings.Repeat(" ", maxLen-len(m)), t, d))
|
||||
output.WriteString(fmt.Sprintf("%s://%s%s%s\n", scheme, dnsName, portPart, m))
|
||||
output.WriteString(fmt.Sprintf("%s %-5s %s\n\n", "|--", t, d))
|
||||
}
|
||||
} else if sc.TCP[srvPort] != nil {
|
||||
h := sc.TCP[srvPort]
|
||||
@@ -461,6 +489,7 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
tlsStatus = "TLS terminated"
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("%s://%s%s\n", scheme, dnsName, portPart))
|
||||
output.WriteString(fmt.Sprintf("|-- tcp://%s (%s)\n", hp, tlsStatus))
|
||||
for _, a := range st.TailscaleIPs {
|
||||
ipp := net.JoinHostPort(a.String(), strconv.Itoa(int(srvPort)))
|
||||
@@ -469,11 +498,15 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
output.WriteString(fmt.Sprintf("|--> tcp://%s\n", h.TCPForward))
|
||||
}
|
||||
|
||||
subCmd := infoMap[e.subcmd].Name
|
||||
subCmdSentance := strings.ToUpper(string(subCmd[0])) + subCmd[1:]
|
||||
if !e.bg {
|
||||
output.WriteString(msgToExit)
|
||||
return output.String()
|
||||
}
|
||||
|
||||
output.WriteString("\n")
|
||||
output.WriteString(fmt.Sprintf(msgRunningInBackground, subCmdSentance))
|
||||
subCmd := infoMap[e.subcmd].Name
|
||||
subCmdUpper := strings.ToUpper(string(subCmd[0])) + subCmd[1:]
|
||||
|
||||
output.WriteString(fmt.Sprintf(msgRunningInBackground, subCmdUpper))
|
||||
output.WriteString("\n")
|
||||
output.WriteString(fmt.Sprintf(msgDisableProxy, subCmd, srvType.String(), srvPort))
|
||||
|
||||
@@ -597,6 +630,9 @@ func (e *serveEnv) applyFunnel(sc *ipn.ServeConfig, dnsName string, srvPort uint
|
||||
// TODO: add error handling for if toggling for existing sc
|
||||
if allowFunnel {
|
||||
mak.Set(&sc.AllowFunnel, hp, true)
|
||||
} else if _, exists := sc.AllowFunnel[hp]; exists {
|
||||
fmt.Fprintf(e.stderr(), "Removing Funnel for %s\n", hp)
|
||||
delete(sc.AllowFunnel, hp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,7 +659,7 @@ func (e *serveEnv) unsetServe(sc *ipn.ServeConfig, dnsName string, srvType serve
|
||||
}
|
||||
|
||||
func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, err error) {
|
||||
sourceMap := map[serveType]string{
|
||||
sourceMap := map[serveType]uint{
|
||||
serveTypeHTTP: e.http,
|
||||
serveTypeHTTPS: e.https,
|
||||
serveTypeTCP: e.tcp,
|
||||
@@ -631,13 +667,15 @@ func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, er
|
||||
}
|
||||
|
||||
var srcTypeCount int
|
||||
var srcValue string
|
||||
|
||||
for k, v := range sourceMap {
|
||||
if v != "" {
|
||||
if v != 0 {
|
||||
if v > math.MaxUint16 {
|
||||
return 0, 0, fmt.Errorf("port number %d is too high for %s flag", v, srvType)
|
||||
}
|
||||
srcTypeCount++
|
||||
srvType = k
|
||||
srcValue = v
|
||||
srvPort = uint16(v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,29 +683,104 @@ func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, er
|
||||
return 0, 0, fmt.Errorf("cannot serve multiple types for a single mount point")
|
||||
} else if srcTypeCount == 0 {
|
||||
srvType = serveTypeHTTPS
|
||||
srcValue = "443"
|
||||
}
|
||||
|
||||
srvPort, err = parseServePort(srcValue)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid port %q: %w", srcValue, err)
|
||||
srvPort = 443
|
||||
}
|
||||
|
||||
return srvType, srvPort, nil
|
||||
}
|
||||
|
||||
func isLegacyInvocation(subcmd serveMode, args []string) bool {
|
||||
if subcmd == serve && len(args) == 2 {
|
||||
prefixes := []string{"http", "https", "tcp", "tls-terminated-tcp"}
|
||||
// isLegacyInvocation helps transition customers who have been using the beta
|
||||
// CLI to the newer API by returning a translation from the old command to the new command.
|
||||
// The second result is a boolean that only returns true if the given arguments is a valid
|
||||
// legacy invocation. If the given args are in the old format but are not valid, it will
|
||||
// return false and expects the new code path has enough validations to reject the request.
|
||||
func isLegacyInvocation(subcmd serveMode, args []string) (string, bool) {
|
||||
if subcmd == funnel {
|
||||
if len(args) != 2 {
|
||||
return "", false
|
||||
}
|
||||
_, err := strconv.ParseUint(args[0], 10, 16)
|
||||
return "", err == nil && (args[1] == "on" || args[1] == "off")
|
||||
}
|
||||
turnOff := len(args) > 1 && args[len(args)-1] == "off"
|
||||
if turnOff {
|
||||
args = args[:len(args)-1]
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(args[0], prefix) {
|
||||
return true
|
||||
}
|
||||
srcType, srcPortStr, found := strings.Cut(args[0], ":")
|
||||
if !found {
|
||||
if srcType == "https" && srcPortStr == "" {
|
||||
// Default https port to 443.
|
||||
srcPortStr = "443"
|
||||
} else if srcType == "http" && srcPortStr == "" {
|
||||
// Default http port to 80.
|
||||
srcPortStr = "80"
|
||||
} else {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
var wantLength int
|
||||
switch srcType {
|
||||
case "https", "http":
|
||||
wantLength = 3
|
||||
case "tcp", "tls-terminated-tcp":
|
||||
wantLength = 2
|
||||
default:
|
||||
// return non-legacy, and let new code handle validation.
|
||||
return "", false
|
||||
}
|
||||
// The length is either exactlly the same as in "https / <target>"
|
||||
// or target is omitted as in "https / off" where omit the off at
|
||||
// the top.
|
||||
if len(args) != wantLength && !(turnOff && len(args) == wantLength-1) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
cmd := []string{"tailscale", "serve", "--bg"}
|
||||
switch srcType {
|
||||
case "https":
|
||||
// In the new code, we default to https:443,
|
||||
// so we don't need to pass the flag explicitly.
|
||||
if srcPortStr != "443" {
|
||||
cmd = append(cmd, fmt.Sprintf("--https %s", srcPortStr))
|
||||
}
|
||||
case "http":
|
||||
cmd = append(cmd, fmt.Sprintf("--http %s", srcPortStr))
|
||||
case "tcp", "tls-terminated-tcp":
|
||||
cmd = append(cmd, fmt.Sprintf("--%s %s", srcType, srcPortStr))
|
||||
}
|
||||
|
||||
var mount string
|
||||
if srcType == "https" || srcType == "http" {
|
||||
mount = args[1]
|
||||
if _, err := cleanMountPoint(mount); err != nil {
|
||||
return "", false
|
||||
}
|
||||
if mount != "/" {
|
||||
cmd = append(cmd, "--set-path "+mount)
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no "off" there must always be a target destination.
|
||||
// If there is "off", target is optional so check if it exists
|
||||
// first before appending it.
|
||||
hasTarget := !turnOff || (turnOff && len(args) == wantLength)
|
||||
if hasTarget {
|
||||
dest := args[len(args)-1]
|
||||
if strings.Contains(dest, " ") {
|
||||
dest = strconv.Quote(dest)
|
||||
}
|
||||
cmd = append(cmd, dest)
|
||||
}
|
||||
if turnOff {
|
||||
cmd = append(cmd, "off")
|
||||
}
|
||||
|
||||
return strings.Join(cmd, " "), true
|
||||
}
|
||||
|
||||
// removeWebServe removes a web handler from the serve config
|
||||
@@ -715,6 +828,7 @@ func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort u
|
||||
}
|
||||
if len(sc.Web[hp].Handlers) == 0 {
|
||||
delete(sc.Web, hp)
|
||||
delete(sc.AllowFunnel, hp)
|
||||
delete(sc.TCP, srvPort)
|
||||
}
|
||||
|
||||
@@ -732,6 +846,10 @@ func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort u
|
||||
delete(sc.AllowFunnel, hp)
|
||||
}
|
||||
|
||||
if len(sc.AllowFunnel) == 0 {
|
||||
sc.AllowFunnel = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -768,7 +886,7 @@ func (e *serveEnv) removeTCPServe(sc *ipn.ServeConfig, src uint16) error {
|
||||
// - https-insecure://localhost:3000
|
||||
// - https-insecure://localhost:3000/foo
|
||||
func expandProxyTargetDev(target string, supportedSchemes []string, defaultScheme string) (string, error) {
|
||||
var host = "127.0.0.1"
|
||||
const host = "127.0.0.1"
|
||||
|
||||
// support target being a port number
|
||||
if port, err := strconv.ParseUint(target, 10, 16); err == nil {
|
||||
@@ -791,19 +909,20 @@ func expandProxyTargetDev(target string, supportedSchemes []string, defaultSchem
|
||||
return "", fmt.Errorf("must be a URL starting with one of the supported schemes: %v", supportedSchemes)
|
||||
}
|
||||
|
||||
// validate the host.
|
||||
switch u.Hostname() {
|
||||
case "localhost", "127.0.0.1":
|
||||
default:
|
||||
return "", errors.New("only localhost or 127.0.0.1 proxies are currently supported")
|
||||
}
|
||||
|
||||
// validate the port
|
||||
port, err := strconv.ParseUint(u.Port(), 10, 16)
|
||||
if err != nil || port == 0 {
|
||||
return "", fmt.Errorf("invalid port %q", u.Port())
|
||||
}
|
||||
|
||||
// validate the host.
|
||||
switch u.Hostname() {
|
||||
case "localhost", "127.0.0.1":
|
||||
u.Host = fmt.Sprintf("%s:%d", host, port)
|
||||
default:
|
||||
return "", errors.New("only localhost or 127.0.0.1 proxies are currently supported")
|
||||
}
|
||||
u.Host = fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
@@ -841,3 +960,17 @@ func (s serveType) String() string {
|
||||
return "unknownServeType"
|
||||
}
|
||||
}
|
||||
|
||||
func (e *serveEnv) stdout() io.Writer {
|
||||
if e.testStdout != nil {
|
||||
return e.testStdout
|
||||
}
|
||||
return os.Stdout
|
||||
}
|
||||
|
||||
func (e *serveEnv) stderr() io.Writer {
|
||||
if e.testStderr != nil {
|
||||
return e.testStderr
|
||||
}
|
||||
return os.Stderr
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/clientupdate"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var setCmd = &ffcli.Command{
|
||||
@@ -65,8 +67,8 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
setf.StringVar(&setArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
setf.StringVar(&setArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\") or empty string to not advertise routes")
|
||||
setf.BoolVar(&setArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
||||
setf.BoolVar(&setArgs.updateCheck, "update-check", true, "HIDDEN: notify about available Tailscale updates")
|
||||
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "HIDDEN: automatically update to the latest available version")
|
||||
setf.BoolVar(&setArgs.updateCheck, "update-check", true, "notify about available Tailscale updates")
|
||||
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "automatically update to the latest available version")
|
||||
setf.BoolVar(&setArgs.postureChecking, "posture-checking", false, "HIDDEN: allow management plane to gather device posture information")
|
||||
|
||||
if safesocket.GOOSUsesPeerCreds(goos) {
|
||||
@@ -157,9 +159,22 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
}
|
||||
}
|
||||
if maskedPrefs.AutoUpdateSet {
|
||||
_, err := clientupdate.NewUpdater(clientupdate.Arguments{})
|
||||
if errors.Is(err, errors.ErrUnsupported) {
|
||||
return errors.New("automatic updates are not supported on this platform")
|
||||
// On macsys, tailscaled will set the Sparkle auto-update setting. It
|
||||
// does not use clientupdate.
|
||||
if version.IsMacSysExt() {
|
||||
apply := "0"
|
||||
if maskedPrefs.AutoUpdate.Apply {
|
||||
apply = "1"
|
||||
}
|
||||
out, err := exec.Command("defaults", "write", "io.tailscale.ipn.macsys", "SUAutomaticallyUpdate", apply).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to enable automatic updates: %v, %q", err, out)
|
||||
}
|
||||
} else {
|
||||
_, err := clientupdate.NewUpdater(clientupdate.Arguments{ForAutoUpdate: true})
|
||||
if errors.Is(err, errors.ErrUnsupported) {
|
||||
return errors.New("automatic updates are not supported on this platform")
|
||||
}
|
||||
}
|
||||
}
|
||||
checkPrefs := curPrefs.Clone()
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
var updateCmd = &ffcli.Command{
|
||||
Name: "update",
|
||||
ShortUsage: "update",
|
||||
ShortHelp: "[ALPHA] Update Tailscale to the latest/different version",
|
||||
ShortHelp: "[BETA] Update Tailscale to the latest/different version",
|
||||
Exec: runUpdate,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("update")
|
||||
|
||||
@@ -2,11 +2,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
|
||||
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
|
||||
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
|
||||
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
@@ -47,6 +42,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
github.com/skip2/go-qrcode from tailscale.com/cmd/tailscale/cli
|
||||
github.com/skip2/go-qrcode/bitset from github.com/skip2/go-qrcode+
|
||||
github.com/skip2/go-qrcode/reedsolomon from github.com/skip2/go-qrcode
|
||||
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio
|
||||
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
|
||||
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
|
||||
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
|
||||
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
|
||||
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
|
||||
@@ -116,7 +116,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
|
||||
@@ -2,11 +2,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
|
||||
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
|
||||
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
|
||||
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
@@ -136,6 +131,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
LD github.com/pkg/sftp from tailscale.com/ssh/tailssh
|
||||
LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp
|
||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio
|
||||
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
|
||||
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
|
||||
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
|
||||
github.com/tailscale/golang-x-crypto/acme from tailscale.com/ipn/ipnlocal
|
||||
LD github.com/tailscale/golang-x-crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh
|
||||
LD 💣 github.com/tailscale/golang-x-crypto/internal/alias from github.com/tailscale/golang-x-crypto/chacha20
|
||||
@@ -297,7 +297,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/posture from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/proxymap from tailscale.com/tsd+
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/smallzstd from tailscale.com/control/controlclient+
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck+
|
||||
@@ -352,7 +352,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/util/osdiag from tailscale.com/cmd/tailscaled+
|
||||
W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag
|
||||
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
|
||||
W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth
|
||||
tailscale.com/util/race from tailscale.com/net/dns/resolver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/rands from tailscale.com/ipn/ipnlocal+
|
||||
|
||||
@@ -226,7 +226,7 @@ func (c *Client) useHTTPS() bool {
|
||||
// tlsServerName returns the tls.Config.ServerName value (for the TLS ClientHello).
|
||||
func (c *Client) tlsServerName(node *tailcfg.DERPNode) string {
|
||||
if c.url != nil {
|
||||
return c.url.Host
|
||||
return c.url.Hostname()
|
||||
}
|
||||
return node.HostName
|
||||
}
|
||||
|
||||
29
flake.nix
29
flake.nix
@@ -41,14 +41,6 @@
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, flake-compat }: let
|
||||
# Grab a helper func out of the Nix language libraries. Annoyingly
|
||||
# these are only accessible through legacyPackages right now,
|
||||
# which forces us to indirect through a platform-specific
|
||||
# path. The x86_64-linux in here doesn't really matter, since all
|
||||
# we're grabbing is a pure Nix string manipulation function that
|
||||
# doesn't build any software.
|
||||
fileContents = nixpkgs.legacyPackages.x86_64-linux.lib.fileContents;
|
||||
|
||||
# tailscaleRev is the git commit at which this flake was imported,
|
||||
# or the empty string when building from a local checkout of the
|
||||
# tailscale repo.
|
||||
@@ -74,17 +66,29 @@
|
||||
name = "tailscale";
|
||||
|
||||
src = ./.;
|
||||
vendorSha256 = fileContents ./go.mod.sri;
|
||||
nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.makeWrapper pkgs.git ];
|
||||
vendorSha256 = pkgs.lib.fileContents ./go.mod.sri;
|
||||
nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.makeWrapper ];
|
||||
ldflags = ["-X tailscale.com/version.GitCommit=${tailscaleRev}"];
|
||||
CGO_ENABLED = 0;
|
||||
subPackages = [ "cmd/tailscale" "cmd/tailscaled" ];
|
||||
doCheck = false;
|
||||
|
||||
# NOTE: We strip the ${PORT} and $FLAGS because they are unset in the
|
||||
# environment and cause issues (specifically the unset PORT). At some
|
||||
# point, there should be a NixOS module that allows configuration of these
|
||||
# things, but for now, we hardcode the default of port 41641 (taken from
|
||||
# ./cmd/tailscaled/tailscaled.defaults).
|
||||
postInstall = pkgs.lib.optionalString pkgs.stdenv.isLinux ''
|
||||
wrapProgram $out/bin/tailscaled --prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.iproute2 pkgs.iptables pkgs.getent pkgs.shadow ]}
|
||||
wrapProgram $out/bin/tailscale --suffix PATH : ${pkgs.lib.makeBinPath [ pkgs.procps ]}
|
||||
|
||||
sed -i -e "s#/usr/sbin#$out/bin#" -e "/^EnvironmentFile/d" ./cmd/tailscaled/tailscaled.service
|
||||
sed -i \
|
||||
-e "s#/usr/sbin#$out/bin#" \
|
||||
-e "/^EnvironmentFile/d" \
|
||||
-e 's/''${PORT}/41641/' \
|
||||
-e 's/$FLAGS//' \
|
||||
./cmd/tailscaled/tailscaled.service
|
||||
|
||||
install -D -m0444 -t $out/lib/systemd/system ./cmd/tailscaled/tailscaled.service
|
||||
'';
|
||||
};
|
||||
@@ -97,6 +101,7 @@
|
||||
ts = tailscale pkgs;
|
||||
in {
|
||||
packages = {
|
||||
default = ts;
|
||||
tailscale = ts;
|
||||
};
|
||||
devShell = pkgs.mkShell {
|
||||
@@ -115,4 +120,4 @@
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
|
||||
}
|
||||
# nix-direnv cache busting line: sha256-tzMLCNvIjG5e2aslmMt8GWgnfImd0J2a11xutOe59Ss=
|
||||
# nix-direnv cache busting line: sha256-WGZkpffwe4I8FewdBHXGaLbKQP/kHr7UF2lCXBTcNb4=
|
||||
|
||||
3
go.mod
3
go.mod
@@ -4,7 +4,6 @@ go 1.21
|
||||
|
||||
require (
|
||||
filippo.io/mkcert v1.4.4
|
||||
github.com/Microsoft/go-winio v0.6.1
|
||||
github.com/akutz/memconn v0.1.0
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa
|
||||
github.com/andybalholm/brotli v1.0.5
|
||||
@@ -104,6 +103,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
)
|
||||
@@ -321,6 +321,7 @@ require (
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55
|
||||
github.com/tdakkota/asciicheck v0.2.0 // indirect
|
||||
github.com/tetafro/godot v1.4.11 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-tzMLCNvIjG5e2aslmMt8GWgnfImd0J2a11xutOe59Ss=
|
||||
sha256-WGZkpffwe4I8FewdBHXGaLbKQP/kHr7UF2lCXBTcNb4=
|
||||
|
||||
2
go.sum
2
go.sum
@@ -868,6 +868,8 @@ github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff h1:vnxdYZUJb
|
||||
github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HPjrSuJYEkdZ+0ItmGQAQ75cRHIiftIyE=
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
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/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e h1:JyeJF/HuSwvxWtsR1c0oKX1lzaSH5Wh4aX+MgiStaGQ=
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -25,6 +27,35 @@ import (
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// ErrNotImplemented is returned by ConnIdentity.WindowsToken when it is not
|
||||
// implemented for the current GOOS.
|
||||
var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS)
|
||||
|
||||
// WindowsToken represents the current security context of a Windows user.
|
||||
type WindowsToken interface {
|
||||
io.Closer
|
||||
// EqualUIDs reports whether other refers to the same user ID as the receiver.
|
||||
EqualUIDs(other WindowsToken) bool
|
||||
// IsAdministrator reports whether the receiver is a member of the built-in
|
||||
// Administrators group, or else an error. Use IsElevated to determine whether
|
||||
// the receiver is actually utilizing administrative rights.
|
||||
IsAdministrator() (bool, error)
|
||||
// IsUID reports whether the receiver's user ID matches uid.
|
||||
IsUID(uid ipn.WindowsUserID) bool
|
||||
// UID returns the ipn.WindowsUserID associated with the receiver, or else
|
||||
// an error.
|
||||
UID() (ipn.WindowsUserID, error)
|
||||
// IsElevated reports whether the receiver is currently executing as an
|
||||
// elevated administrative user.
|
||||
IsElevated() bool
|
||||
// UserDir returns the special directory identified by folderID as associated
|
||||
// with the receiver. folderID must be one of the KNOWNFOLDERID values from
|
||||
// the x/sys/windows package, serialized as a stringified GUID.
|
||||
UserDir(folderID string) (string, error)
|
||||
// Username returns the user name associated with the receiver.
|
||||
Username() (string, error)
|
||||
}
|
||||
|
||||
// ConnIdentity represents the owner of a localhost TCP or unix socket connection
|
||||
// connecting to the LocalAPI.
|
||||
type ConnIdentity struct {
|
||||
@@ -38,9 +69,7 @@ type ConnIdentity struct {
|
||||
// Used on Windows:
|
||||
// TODO(bradfitz): merge these into the peercreds package and
|
||||
// use that for all.
|
||||
pid int
|
||||
userID ipn.WindowsUserID
|
||||
user *user.User
|
||||
pid int
|
||||
}
|
||||
|
||||
// WindowsUserID returns the local machine's userid of the connection
|
||||
@@ -52,8 +81,11 @@ func (ci *ConnIdentity) WindowsUserID() ipn.WindowsUserID {
|
||||
if envknob.GOOS() != "windows" {
|
||||
return ""
|
||||
}
|
||||
if ci.userID != "" {
|
||||
return ci.userID
|
||||
if tok, err := ci.WindowsToken(); err == nil {
|
||||
defer tok.Close()
|
||||
if uid, err := tok.UID(); err == nil {
|
||||
return uid
|
||||
}
|
||||
}
|
||||
// For Linux tests running as Windows:
|
||||
const isBroken = true // TODO(bradfitz,maisem): fix tests; this doesn't work yet
|
||||
@@ -65,7 +97,6 @@ func (ci *ConnIdentity) WindowsUserID() ipn.WindowsUserID {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ci *ConnIdentity) User() *user.User { return ci.user }
|
||||
func (ci *ConnIdentity) Pid() int { return ci.pid }
|
||||
func (ci *ConnIdentity) IsUnixSock() bool { return ci.isUnixSock }
|
||||
func (ci *ConnIdentity) Creds() *peercred.Creds { return ci.creds }
|
||||
|
||||
@@ -21,3 +21,9 @@ func GetConnIdentity(_ logger.Logf, c net.Conn) (ci *ConnIdentity, err error) {
|
||||
ci.creds, _ = peercred.Get(c)
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
// WindowsToken is unsupported when GOOS != windows and always returns
|
||||
// ErrNotImplemented.
|
||||
func (ci *ConnIdentity) WindowsToken() (WindowsToken, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
@@ -6,53 +6,157 @@ package ipnauth
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/pidowner"
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procGetNamedPipeClientProcessId = kernel32.NewProc("GetNamedPipeClientProcessId")
|
||||
)
|
||||
|
||||
func getNamedPipeClientProcessId(h windows.Handle) (pid uint32, err error) {
|
||||
r1, _, err := procGetNamedPipeClientProcessId.Call(uintptr(h), uintptr(unsafe.Pointer(&pid)))
|
||||
if r1 > 0 {
|
||||
return pid, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// GetConnIdentity extracts the identity information from the connection
|
||||
// based on the user who owns the other end of the connection.
|
||||
// If c is not backed by a named pipe, an error is returned.
|
||||
func GetConnIdentity(logf logger.Logf, c net.Conn) (ci *ConnIdentity, err error) {
|
||||
ci = &ConnIdentity{conn: c}
|
||||
h, ok := c.(interface {
|
||||
Fd() uintptr
|
||||
})
|
||||
wcc, ok := c.(*safesocket.WindowsClientConn)
|
||||
if !ok {
|
||||
return ci, fmt.Errorf("not a windows handle: %T", c)
|
||||
return nil, fmt.Errorf("not a WindowsClientConn: %T", c)
|
||||
}
|
||||
pid, err := getNamedPipeClientProcessId(windows.Handle(h.Fd()))
|
||||
ci.pid, err = wcc.ClientPID()
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("getNamedPipeClientProcessId: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
ci.pid = int(pid)
|
||||
uid, err := pidowner.OwnerOfPID(ci.pid)
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("failed to map connection's pid to a user (WSL?): %w", err)
|
||||
}
|
||||
ci.userID = ipn.WindowsUserID(uid)
|
||||
u, err := LookupUserFromID(logf, uid)
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("failed to look up user from userid: %w", err)
|
||||
}
|
||||
ci.user = u
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
type token struct {
|
||||
t windows.Token
|
||||
}
|
||||
|
||||
func (t *token) UID() (ipn.WindowsUserID, error) {
|
||||
sid, err := t.uid()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to look up user from token: %w", err)
|
||||
}
|
||||
|
||||
return ipn.WindowsUserID(sid.String()), nil
|
||||
}
|
||||
|
||||
func (t *token) Username() (string, error) {
|
||||
sid, err := t.uid()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to look up user from token: %w", err)
|
||||
}
|
||||
|
||||
username, domain, _, err := sid.LookupAccount("")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to look up username from SID: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`%s\%s`, domain, username), nil
|
||||
}
|
||||
|
||||
func (t *token) IsAdministrator() (bool, error) {
|
||||
baSID, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return t.t.IsMember(baSID)
|
||||
}
|
||||
|
||||
func (t *token) IsElevated() bool {
|
||||
return t.t.IsElevated()
|
||||
}
|
||||
|
||||
func (t *token) UserDir(folderID string) (string, error) {
|
||||
guid, err := windows.GUIDFromString(folderID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return t.t.KnownFolderPath((*windows.KNOWNFOLDERID)(unsafe.Pointer(&guid)), 0)
|
||||
}
|
||||
|
||||
func (t *token) Close() error {
|
||||
if t.t == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := t.t.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
t.t = 0
|
||||
runtime.SetFinalizer(t, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *token) EqualUIDs(other WindowsToken) bool {
|
||||
if t != nil && other == nil || t == nil && other != nil {
|
||||
return false
|
||||
}
|
||||
ot, ok := other.(*token)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if t == ot {
|
||||
return true
|
||||
}
|
||||
uid, err := t.uid()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
oUID, err := ot.uid()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return uid.Equals(oUID)
|
||||
}
|
||||
|
||||
func (t *token) uid() (*windows.SID, error) {
|
||||
tu, err := t.t.GetTokenUser()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tu.User.Sid, nil
|
||||
}
|
||||
|
||||
func (t *token) IsUID(uid ipn.WindowsUserID) bool {
|
||||
tUID, err := t.UID()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return tUID == uid
|
||||
}
|
||||
|
||||
// WindowsToken returns the WindowsToken representing the security context
|
||||
// of the connection's client.
|
||||
func (ci *ConnIdentity) WindowsToken() (WindowsToken, error) {
|
||||
var wcc *safesocket.WindowsClientConn
|
||||
var ok bool
|
||||
if wcc, ok = ci.conn.(*safesocket.WindowsClientConn); !ok {
|
||||
return nil, fmt.Errorf("not a WindowsClientConn: %T", ci.conn)
|
||||
}
|
||||
|
||||
// We duplicate the token's handle so that the WindowsToken we return may have
|
||||
// a lifetime independent from the original connection.
|
||||
var h windows.Handle
|
||||
if err := windows.DuplicateHandle(
|
||||
windows.CurrentProcess(),
|
||||
windows.Handle(wcc.Token()),
|
||||
windows.CurrentProcess(),
|
||||
&h,
|
||||
0,
|
||||
false,
|
||||
windows.DUPLICATE_SAME_ACCESS,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &token{t: windows.Token(h)}
|
||||
runtime.SetFinalizer(result, func(t *token) { t.Close() })
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
|
||||
// Note that we create the Updater solely to check for errors; we do not
|
||||
// invoke it here. For this purpose, it is ok to pass it a zero Arguments.
|
||||
prefs := b.Prefs().AutoUpdate()
|
||||
_, err := clientupdate.NewUpdater(clientupdate.Arguments{})
|
||||
_, err := clientupdate.NewUpdater(clientupdate.Arguments{ForAutoUpdate: true})
|
||||
return tailcfg.C2NUpdateResponse{
|
||||
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply,
|
||||
Supported: err == nil,
|
||||
|
||||
@@ -261,6 +261,7 @@ type LocalBackend struct {
|
||||
componentLogUntil map[string]componentLogState
|
||||
// c2nUpdateStatus is the status of c2n-triggered client update.
|
||||
c2nUpdateStatus updateStatus
|
||||
currentUser ipnauth.WindowsToken
|
||||
|
||||
// ServeConfig fields. (also guarded by mu)
|
||||
lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig
|
||||
@@ -2722,7 +2723,7 @@ func (b *LocalBackend) shouldUploadServices() bool {
|
||||
return !p.ShieldsUp() && b.netMap.CollectServices
|
||||
}
|
||||
|
||||
// SetCurrentUserID is used to implement support for multi-user systems (only
|
||||
// SetCurrentUser is used to implement support for multi-user systems (only
|
||||
// Windows 2022-11-25). On such systems, the uid is used to determine which
|
||||
// user's state should be used. The current user is maintained by active
|
||||
// connections open to the backend.
|
||||
@@ -2737,18 +2738,35 @@ func (b *LocalBackend) shouldUploadServices() bool {
|
||||
// unattended mode. The user must disable unattended mode before the user can be
|
||||
// changed.
|
||||
//
|
||||
// On non-multi-user systems, the uid should be set to empty string.
|
||||
func (b *LocalBackend) SetCurrentUserID(uid ipn.WindowsUserID) {
|
||||
// On non-multi-user systems, the token should be set to nil.
|
||||
//
|
||||
// SetCurrentUser returns the ipn.WindowsUserID associated with token
|
||||
// when successful.
|
||||
func (b *LocalBackend) SetCurrentUser(token ipnauth.WindowsToken) (ipn.WindowsUserID, error) {
|
||||
var uid ipn.WindowsUserID
|
||||
if token != nil {
|
||||
var err error
|
||||
uid, err = token.UID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
if b.pm.CurrentUserID() == uid {
|
||||
b.mu.Unlock()
|
||||
return
|
||||
return uid, nil
|
||||
}
|
||||
if err := b.pm.SetCurrentUserID(uid); err != nil {
|
||||
b.mu.Unlock()
|
||||
return
|
||||
return uid, nil
|
||||
}
|
||||
if b.currentUser != nil {
|
||||
b.currentUser.Close()
|
||||
}
|
||||
b.currentUser = token
|
||||
b.resetForProfileChangeLockedOnEntry()
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) CheckPrefs(p *ipn.Prefs) error {
|
||||
@@ -4112,6 +4130,10 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
|
||||
b.setNetMapLocked(nil)
|
||||
b.pm.Reset()
|
||||
if b.currentUser != nil {
|
||||
b.currentUser.Close()
|
||||
b.currentUser = nil
|
||||
}
|
||||
b.keyExpired = false
|
||||
b.authURL = ""
|
||||
b.authURLSticky = ""
|
||||
@@ -4583,6 +4605,9 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
|
||||
if !b.peerIsTaildropTargetLocked(p) {
|
||||
continue
|
||||
}
|
||||
if p.Hostinfo().OS() == "tvOS" {
|
||||
continue
|
||||
}
|
||||
peerAPI := peerAPIBase(b.netMap, p)
|
||||
if peerAPI == "" {
|
||||
continue
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
@@ -468,6 +469,24 @@ func TestFileTargets(t *testing.T) {
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("unexpected %d peers", len(got))
|
||||
}
|
||||
|
||||
var peerMap map[tailcfg.NodeID]tailcfg.NodeView
|
||||
mak.NonNil(&peerMap)
|
||||
var nodeID tailcfg.NodeID
|
||||
nodeID = 1234
|
||||
peer := &tailcfg.Node{
|
||||
ID: 1234,
|
||||
Hostinfo: (&tailcfg.Hostinfo{OS: "tvOS"}).View(),
|
||||
}
|
||||
peerMap[nodeID] = peer.View()
|
||||
b.peers = peerMap
|
||||
got, err = b.FileTargets()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("unexpected %d peers", len(got))
|
||||
}
|
||||
// (other cases handled by TestPeerAPIBase above)
|
||||
}
|
||||
|
||||
|
||||
@@ -202,6 +202,7 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
lah := localapi.NewHandler(lb, s.logf, s.netMon, s.backendLogID)
|
||||
lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci)
|
||||
lah.PermitCert = s.connCanFetchCerts(ci)
|
||||
lah.CallerIsLocalAdmin = s.connIsLocalAdmin(ci)
|
||||
lah.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@@ -242,8 +243,30 @@ func (s *Server) checkConnIdentityLocked(ci *ipnauth.ConnIdentity) error {
|
||||
for _, active = range s.activeReqs {
|
||||
break
|
||||
}
|
||||
if active != nil && ci.WindowsUserID() != active.WindowsUserID() {
|
||||
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s, pid %d", active.User().Username, active.Pid())}
|
||||
if active != nil {
|
||||
chkTok, err := ci.WindowsToken()
|
||||
if err == nil {
|
||||
defer chkTok.Close()
|
||||
} else if !errors.Is(err, ipnauth.ErrNotImplemented) {
|
||||
return err
|
||||
}
|
||||
|
||||
activeTok, err := active.WindowsToken()
|
||||
if err == nil {
|
||||
defer activeTok.Close()
|
||||
} else if !errors.Is(err, ipnauth.ErrNotImplemented) {
|
||||
return err
|
||||
}
|
||||
|
||||
if chkTok != nil && !chkTok.EqualUIDs(activeTok) {
|
||||
var b strings.Builder
|
||||
b.WriteString("Tailscale already in use")
|
||||
if username, err := activeTok.Username(); err == nil {
|
||||
fmt.Fprintf(&b, " by %s", username)
|
||||
}
|
||||
fmt.Fprintf(&b, ", pid %d", active.Pid())
|
||||
return inUseOtherUserError{errors.New(b.String())}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := s.mustBackend().CheckIPNConnectionAllowed(ci); err != nil {
|
||||
@@ -341,6 +364,31 @@ func (s *Server) connCanFetchCerts(ci *ipnauth.ConnIdentity) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// connIsLocalAdmin reports whether ci has administrative access to the local
|
||||
// machine, for whatever that means with respect to the current OS.
|
||||
//
|
||||
// This returns true only on Windows machines when the client user is a
|
||||
// member of the built-in Administrators group (but not necessarily elevated).
|
||||
// This is useful because, on Windows, tailscaled itself always runs with
|
||||
// elevated rights: we want to avoid privilege escalation for certain mutative operations.
|
||||
func (s *Server) connIsLocalAdmin(ci *ipnauth.ConnIdentity) bool {
|
||||
tok, err := ci.WindowsToken()
|
||||
if err != nil {
|
||||
if !errors.Is(err, ipnauth.ErrNotImplemented) {
|
||||
s.logf("ipnauth.ConnIdentity.WindowsToken() error: %v", err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
defer tok.Close()
|
||||
|
||||
isAdmin, err := tok.IsAdministrator()
|
||||
if err != nil {
|
||||
s.logf("ipnauth.WindowsToken.IsAdministrator() error: %v", err)
|
||||
return false
|
||||
}
|
||||
return isAdmin
|
||||
}
|
||||
|
||||
// addActiveHTTPRequest adds c to the server's list of active HTTP requests.
|
||||
//
|
||||
// If the returned error may be of type inUseOtherUserError.
|
||||
@@ -372,14 +420,25 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, ci *ipnauth.ConnIdentit
|
||||
|
||||
mak.Set(&s.activeReqs, req, ci)
|
||||
|
||||
if uid := ci.WindowsUserID(); uid != "" && len(s.activeReqs) == 1 {
|
||||
// Tell the LocalBackend about the identity we're now running as.
|
||||
lb.SetCurrentUserID(uid)
|
||||
if s.lastUserID != uid {
|
||||
if s.lastUserID != "" {
|
||||
doReset = true
|
||||
if len(s.activeReqs) == 1 {
|
||||
token, err := ci.WindowsToken()
|
||||
if err != nil {
|
||||
if !errors.Is(err, ipnauth.ErrNotImplemented) {
|
||||
s.logf("error obtaining access token: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Tell the LocalBackend about the identity we're now running as.
|
||||
uid, err := lb.SetCurrentUser(token)
|
||||
if err != nil {
|
||||
token.Close()
|
||||
return nil, err
|
||||
}
|
||||
if s.lastUserID != uid {
|
||||
if s.lastUserID != "" {
|
||||
doReset = true
|
||||
}
|
||||
s.lastUserID = uid
|
||||
}
|
||||
s.lastUserID = uid
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -157,6 +157,17 @@ type Handler struct {
|
||||
// cert fetching access.
|
||||
PermitCert bool
|
||||
|
||||
// CallerIsLocalAdmin is whether the this handler is being invoked as a
|
||||
// result of a LocalAPI call from a user who is a local admin of the current
|
||||
// machine.
|
||||
//
|
||||
// As of 2023-10-26 it is only populated on Windows.
|
||||
//
|
||||
// It can be used to to restrict some LocalAPI operations which should only
|
||||
// be run by an admin and not unprivileged users in a computing environment
|
||||
// managed by IT admins.
|
||||
CallerIsLocalAdmin bool
|
||||
|
||||
b *ipnlocal.LocalBackend
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand
|
||||
@@ -904,6 +915,15 @@ func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
|
||||
writeErrorJSON(w, fmt.Errorf("decoding config: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// require a local admin when setting a path handler
|
||||
// TODO: roll-up this Windows-specific check into either PermitWrite
|
||||
// or a global admin escalation check.
|
||||
if shouldDenyServeConfigForGOOSAndUserContext(runtime.GOOS, configIn, h) {
|
||||
http.Error(w, "must be a Windows local admin to serve a path", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
etag := r.Header.Get("If-Match")
|
||||
if err := h.b.SetServeConfig(configIn, etag); err != nil {
|
||||
if errors.Is(err, ipnlocal.ErrETagMismatch) {
|
||||
@@ -919,6 +939,16 @@ func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func shouldDenyServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeConfig, h *Handler) bool {
|
||||
if goos != "windows" {
|
||||
return false
|
||||
}
|
||||
if !configIn.HasPathHandler() {
|
||||
return false
|
||||
}
|
||||
return !h.CallerIsLocalAdmin
|
||||
}
|
||||
|
||||
func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "IP forwarding check access denied", http.StatusForbidden)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
@@ -145,3 +146,69 @@ func TestWhoIsJustIP(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldDenyServeConfigForGOOSAndUserContext(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
goos string
|
||||
configIn *ipn.ServeConfig
|
||||
h *Handler
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "linux",
|
||||
goos: "linux",
|
||||
configIn: &ipn.ServeConfig{},
|
||||
h: &Handler{CallerIsLocalAdmin: false},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "windows-not-path-handler",
|
||||
goos: "windows",
|
||||
configIn: &ipn.ServeConfig{
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
h: &Handler{CallerIsLocalAdmin: false},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "windows-path-handler-admin",
|
||||
goos: "windows",
|
||||
configIn: &ipn.ServeConfig{
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Path: "/tmp"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
h: &Handler{CallerIsLocalAdmin: true},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "windows-path-handler-not-admin",
|
||||
goos: "windows",
|
||||
configIn: &ipn.ServeConfig{
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Path: "/tmp"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
h: &Handler{CallerIsLocalAdmin: false},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := shouldDenyServeConfigForGOOSAndUserContext(tt.goos, tt.configIn, tt.h)
|
||||
if got != tt.want {
|
||||
t.Errorf("shouldDenyServeConfigForGOOSAndUserContext() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
24
ipn/serve.go
24
ipn/serve.go
@@ -163,6 +163,30 @@ func (sc *ServeConfig) GetTCPPortHandler(port uint16) *TCPPortHandler {
|
||||
return sc.TCP[port]
|
||||
}
|
||||
|
||||
// HasPathHandler reports whether if ServeConfig has at least
|
||||
// one path handler, including foreground configs.
|
||||
func (sc *ServeConfig) HasPathHandler() bool {
|
||||
if sc.Web != nil {
|
||||
for _, webServerConfig := range sc.Web {
|
||||
for _, httpHandler := range webServerConfig.Handlers {
|
||||
if httpHandler.Path != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sc.Foreground != nil {
|
||||
for _, fgConfig := range sc.Foreground {
|
||||
if fgConfig.HasPathHandler() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsTCPForwardingAny reports whether ServeConfig is currently forwarding in
|
||||
// TCPForward mode on any port. This is exclusive of Web/HTTPS serving.
|
||||
func (sc *ServeConfig) IsTCPForwardingAny() bool {
|
||||
|
||||
@@ -43,3 +43,86 @@ func TestCheckFunnelAccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasPathHandler(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg ServeConfig
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty-config",
|
||||
cfg: ServeConfig{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "with-bg-path-handler",
|
||||
cfg: ServeConfig{
|
||||
TCP: map[uint16]*TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[HostPort]*WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*HTTPHandler{
|
||||
"/": {Path: "/tmp"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "with-fg-path-handler",
|
||||
cfg: ServeConfig{
|
||||
TCP: map[uint16]*TCPPortHandler{
|
||||
443: {HTTPS: true},
|
||||
},
|
||||
Foreground: map[string]*ServeConfig{
|
||||
"abc123": {
|
||||
TCP: map[uint16]*TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[HostPort]*WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*HTTPHandler{
|
||||
"/": {Path: "/tmp"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "with-no-bg-path-handler",
|
||||
cfg: ServeConfig{
|
||||
TCP: map[uint16]*TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[HostPort]*WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
AllowFunnel: map[HostPort]bool{"foo.test.ts.net:443": true},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "with-no-fg-path-handler",
|
||||
cfg: ServeConfig{
|
||||
Foreground: map[string]*ServeConfig{
|
||||
"abc123": {
|
||||
TCP: map[uint16]*TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[HostPort]*WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
AllowFunnel: map[HostPort]bool{"foo.test.ts.net:443": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.cfg.HasPathHandler()
|
||||
if tt.want != got {
|
||||
t.Errorf("HasPathHandler() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,57 +13,59 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [gioui.org](https://pkg.go.dev/gioui.org) ([MIT](https://git.sr.ht/~eliasnaur/gio/tree/32c6a9b10d0b/LICENSE))
|
||||
- [gioui.org/cpu](https://pkg.go.dev/gioui.org/cpu) ([MIT](https://git.sr.ht/~eliasnaur/gio-cpu/tree/8d6a761490d2/LICENSE))
|
||||
- [gioui.org/shader](https://pkg.go.dev/gioui.org/shader) ([MIT](https://git.sr.ht/~eliasnaur/gio-shader/tree/v1.0.6/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.18.0/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.18.22/config/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.13.21/credentials/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.13.3/feature/ec2/imds/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.33/internal/configsources/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.27/internal/endpoints/v2/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.34/internal/ini/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.18.0/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.27/service/internal/presigned-url/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.36.3/service/ssm/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.12.9/service/sso/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.14.9/service/ssooidc/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.18.10/service/sts/LICENSE.txt))
|
||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.13.5/LICENSE))
|
||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.13.5/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.21.0/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.18.42/config/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.13.40/credentials/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.13.11/feature/ec2/imds/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.41/internal/configsources/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.35/internal/endpoints/v2/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.43/internal/ini/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.21.0/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.35/service/internal/presigned-url/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.38.0/service/ssm/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.14.1/service/sso/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.17.1/service/ssooidc/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.22.0/service/sts/LICENSE.txt))
|
||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.14.2/LICENSE))
|
||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.14.2/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/benoitkugler/textlayout](https://pkg.go.dev/github.com/benoitkugler/textlayout) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/LICENSE))
|
||||
- [github.com/benoitkugler/textlayout/fonts](https://pkg.go.dev/github.com/benoitkugler/textlayout/fonts) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/fonts/LICENSE))
|
||||
- [github.com/benoitkugler/textlayout/graphite](https://pkg.go.dev/github.com/benoitkugler/textlayout/graphite) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/graphite/LICENSE))
|
||||
- [github.com/benoitkugler/textlayout/harfbuzz](https://pkg.go.dev/github.com/benoitkugler/textlayout/harfbuzz) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/harfbuzz/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.6.0/LICENSE))
|
||||
- [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.4.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.4.0/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.7.0/LICENSE))
|
||||
- [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.5.0/LICENSE))
|
||||
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
|
||||
- [github.com/go-text/typesetting](https://pkg.go.dev/github.com/go-text/typesetting) ([BSD-3-Clause](https://github.com/go-text/typesetting/blob/0399769901d5/LICENSE))
|
||||
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
||||
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/9aa6fdf5a28c/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.0/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.1/LICENSE))
|
||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
|
||||
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
|
||||
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/974c6f05fe16/LICENSE))
|
||||
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/65c27093e38a/LICENSE))
|
||||
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
|
||||
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
|
||||
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.2/LICENSE.md))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.16.7/LICENSE))
|
||||
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.16.7/internal/snapref/LICENSE))
|
||||
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.16.7/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.5/LICENSE.md))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.0/LICENSE))
|
||||
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.0/internal/snapref/LICENSE))
|
||||
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.17.0/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [github.com/kortschak/wol](https://pkg.go.dev/github.com/kortschak/wol) ([BSD-3-Clause](https://github.com/kortschak/wol/blob/da482cc4850a/LICENSE))
|
||||
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.3.2/LICENSE.md))
|
||||
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
|
||||
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
|
||||
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.1/LICENSE.md))
|
||||
- [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.55/LICENSE))
|
||||
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.5.0/LICENSE.md))
|
||||
- [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.56/LICENSE))
|
||||
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
|
||||
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.17/LICENSE))
|
||||
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.18/LICENSE))
|
||||
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/f0b76a10a08e/LICENSE))
|
||||
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
|
||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/tailscale-android](https://pkg.go.dev/github.com/tailscale/tailscale-android) ([BSD-3-Clause](https://github.com/tailscale/tailscale-android/blob/HEAD/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/93bd5cbf7fd8/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/2f6748dc88e7/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/3e8cd9d6bf63/LICENSE))
|
||||
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
|
||||
@@ -71,19 +73,19 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/intern](https://pkg.go.dev/go4.org/intern) ([BSD-3-Clause](https://github.com/go4org/intern/blob/ae77deb06f29/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/ad4cb58a6516/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/6213f710f925/LICENSE))
|
||||
- [go4.org/unsafe/assume-no-moving-gc](https://pkg.go.dev/go4.org/unsafe/assume-no-moving-gc) ([BSD-3-Clause](https://github.com/go4org/unsafe-assume-no-moving-gc/blob/e7c30c78aeb2/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/515e97eb:LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.14.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
|
||||
- [golang.org/x/exp/shiny](https://pkg.go.dev/golang.org/x/exp/shiny) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/334a2380:shiny/LICENSE))
|
||||
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.14.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.11.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.11.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.12.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.17.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.3.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.13.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/4fe30062272c/LICENSE))
|
||||
- [inet.af/netaddr](https://pkg.go.dev/inet.af/netaddr) ([BSD-3-Clause](https://github.com/inetaf/netaddr/blob/097006376321/LICENSE))
|
||||
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))
|
||||
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([MIT](https://github.com/nhooyr/websocket/blob/v1.8.7/LICENSE.txt))
|
||||
|
||||
@@ -14,9 +14,8 @@ Some packages may only be included on certain architectures or operating systems
|
||||
|
||||
|
||||
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.0.0/LICENSE))
|
||||
- [github.com/Microsoft/go-winio](https://pkg.go.dev/github.com/Microsoft/go-winio) ([MIT](https://github.com/Microsoft/go-winio/blob/v0.6.1/LICENSE))
|
||||
- [github.com/akutz/memconn](https://pkg.go.dev/github.com/akutz/memconn) ([Apache-2.0](https://github.com/akutz/memconn/blob/v0.1.0/LICENSE))
|
||||
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/909beea2cc74/LICENSE))
|
||||
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/1a75b4708caa/LICENSE))
|
||||
- [github.com/anmitsu/go-shlex](https://pkg.go.dev/github.com/anmitsu/go-shlex) ([MIT](https://github.com/anmitsu/go-shlex/blob/38f4b401e2be/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.21.0/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.18.42/config/LICENSE.txt))
|
||||
@@ -72,8 +71,10 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/pkg/errors](https://pkg.go.dev/github.com/pkg/errors) ([BSD-2-Clause](https://github.com/pkg/errors/blob/v0.9.1/LICENSE))
|
||||
- [github.com/pkg/sftp](https://pkg.go.dev/github.com/pkg/sftp) ([BSD-2-Clause](https://github.com/pkg/sftp/blob/v1.13.6/LICENSE))
|
||||
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
|
||||
- [github.com/tailscale/certstore](https://pkg.go.dev/github.com/tailscale/certstore) ([MIT](https://github.com/tailscale/certstore/blob/78d6e1c49d8d/LICENSE.md))
|
||||
- [github.com/tailscale/certstore](https://pkg.go.dev/github.com/tailscale/certstore) ([MIT](https://github.com/tailscale/certstore/blob/77811a65f4ff/LICENSE.md))
|
||||
- [github.com/tailscale/go-winio](https://pkg.go.dev/github.com/tailscale/go-winio) ([MIT](https://github.com/tailscale/go-winio/blob/c4f33415bf55/LICENSE))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/f0b76a10a08e/LICENSE))
|
||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/7bcd7bca7bc5/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/2f6748dc88e7/LICENSE))
|
||||
|
||||
@@ -25,10 +25,10 @@ import (
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/nettype"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// DebugKnobs contains debug configuration that can be provided when creating a
|
||||
@@ -1028,28 +1028,21 @@ var (
|
||||
|
||||
// UPnP error metric that's keyed by code; lazily registered on first read
|
||||
var (
|
||||
metricUPnPErrorsByCodeMu sync.Mutex
|
||||
metricUPnPErrorsByCode map[int]*clientmetric.Metric
|
||||
metricUPnPErrorsByCode syncs.Map[int, *clientmetric.Metric]
|
||||
)
|
||||
|
||||
func getUPnPErrorsMetric(code int) *clientmetric.Metric {
|
||||
metricUPnPErrorsByCodeMu.Lock()
|
||||
defer metricUPnPErrorsByCodeMu.Unlock()
|
||||
mm := metricUPnPErrorsByCode[code]
|
||||
if mm != nil {
|
||||
return mm
|
||||
}
|
||||
mm, _ := metricUPnPErrorsByCode.LoadOrInit(code, func() *clientmetric.Metric {
|
||||
// Metric names cannot contain a hyphen, so we handle negative
|
||||
// numbers by prefixing the name with a "minus_".
|
||||
var codeStr string
|
||||
if code < 0 {
|
||||
codeStr = fmt.Sprintf("portmap_upnp_errors_with_code_minus_%d", -code)
|
||||
} else {
|
||||
codeStr = fmt.Sprintf("portmap_upnp_errors_with_code_%d", code)
|
||||
}
|
||||
|
||||
// Metric names cannot contain a hyphen, so we handle negative numbers
|
||||
// by prefixing the name with a "minus_".
|
||||
var codeStr string
|
||||
if code < 0 {
|
||||
codeStr = fmt.Sprintf("portmap_upnp_errors_with_code_minus_%d", -code)
|
||||
} else {
|
||||
codeStr = fmt.Sprintf("portmap_upnp_errors_with_code_%d", code)
|
||||
}
|
||||
|
||||
mm = clientmetric.NewCounter(codeStr)
|
||||
mak.Set(&metricUPnPErrorsByCode, code, mm)
|
||||
return clientmetric.NewCounter(codeStr)
|
||||
})
|
||||
return mm
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestBasics(t *testing.T) {
|
||||
t.Cleanup(downgradeSDDL())
|
||||
}
|
||||
|
||||
l, err := Listen(sock)
|
||||
ln, err := Listen(sock)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -33,12 +33,12 @@ func TestBasics(t *testing.T) {
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
s, err := l.Accept()
|
||||
s, err := ln.Accept()
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
l.Close()
|
||||
ln.Close()
|
||||
s.Write([]byte("hello"))
|
||||
|
||||
b := make([]byte, 1024)
|
||||
|
||||
@@ -3,16 +3,27 @@
|
||||
|
||||
package safesocket
|
||||
|
||||
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go pipe_windows.go
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
"github.com/tailscale/go-winio"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func connect(s *ConnectionStrategy) (net.Conn, error) {
|
||||
return winio.DialPipe(s.path, nil)
|
||||
dl := time.Now().Add(20 * time.Second)
|
||||
ctx, cancel := context.WithDeadline(context.Background(), dl)
|
||||
defer cancel()
|
||||
// We use the identification impersonation level so that tailscaled may
|
||||
// obtain information about our token for access control purposes.
|
||||
return winio.DialPipeAccessImpLevel(ctx, s.path, windows.GENERIC_READ|windows.GENERIC_WRITE, winio.PipeImpLevelIdentification)
|
||||
}
|
||||
|
||||
func setFlags(network, address string, c syscall.RawConn) error {
|
||||
@@ -39,5 +50,117 @@ func listen(path string) (net.Listener, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("namedpipe.Listen: %w", err)
|
||||
}
|
||||
return lc, nil
|
||||
return &winIOPipeListener{Listener: lc}, nil
|
||||
}
|
||||
|
||||
// WindowsClientConn is an implementation of net.Conn that permits retrieval of
|
||||
// the Windows access token associated with the connection's client. The
|
||||
// embedded net.Conn must be a go-winio PipeConn.
|
||||
type WindowsClientConn struct {
|
||||
winioPipeConn
|
||||
token windows.Token
|
||||
}
|
||||
|
||||
// winioPipeConn is a subset of the interface implemented by the go-winio's
|
||||
// unexported *win32pipe type, as returned by go-winio's ListenPipe
|
||||
// net.Listener's Accept method. This type is used in places where we really are
|
||||
// assuming that specific unexported type and its Fd method.
|
||||
type winioPipeConn interface {
|
||||
net.Conn
|
||||
// Fd returns the Windows handle associated with the connection.
|
||||
Fd() uintptr
|
||||
}
|
||||
|
||||
func resolvePipeHandle(pc winioPipeConn) windows.Handle {
|
||||
return windows.Handle(pc.Fd())
|
||||
}
|
||||
|
||||
func (conn *WindowsClientConn) handle() windows.Handle {
|
||||
return resolvePipeHandle(conn.winioPipeConn)
|
||||
}
|
||||
|
||||
// ClientPID returns the pid of conn's client, or else an error.
|
||||
func (conn *WindowsClientConn) ClientPID() (int, error) {
|
||||
var pid uint32
|
||||
if err := getNamedPipeClientProcessId(conn.handle(), &pid); err != nil {
|
||||
return -1, fmt.Errorf("GetNamedPipeClientProcessId: %w", err)
|
||||
}
|
||||
return int(pid), nil
|
||||
}
|
||||
|
||||
// Token returns the Windows access token of the client user.
|
||||
func (conn *WindowsClientConn) Token() windows.Token {
|
||||
return conn.token
|
||||
}
|
||||
|
||||
func (conn *WindowsClientConn) Close() error {
|
||||
if conn.token != 0 {
|
||||
conn.token.Close()
|
||||
conn.token = 0
|
||||
}
|
||||
return conn.winioPipeConn.Close()
|
||||
}
|
||||
|
||||
// winIOPipeListener is a net.Listener that wraps a go-winio PipeListener and
|
||||
// returns net.Conn values of type *WindowsClientConn with the associated
|
||||
// windows.Token.
|
||||
type winIOPipeListener struct {
|
||||
net.Listener // must be from winio.ListenPipe
|
||||
}
|
||||
|
||||
func (lw *winIOPipeListener) Accept() (net.Conn, error) {
|
||||
conn, err := lw.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pipeConn, ok := conn.(winioPipeConn)
|
||||
if !ok {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("unexpected type %T from winio.ListenPipe listener (itself a %T)", conn, lw.Listener)
|
||||
}
|
||||
|
||||
token, err := clientUserAccessToken(pipeConn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &WindowsClientConn{
|
||||
winioPipeConn: pipeConn,
|
||||
token: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func clientUserAccessToken(pc winioPipeConn) (windows.Token, error) {
|
||||
h := resolvePipeHandle(pc)
|
||||
if h == 0 {
|
||||
return 0, fmt.Errorf("clientUserAccessToken failed to get handle from pipeConn %T", pc)
|
||||
}
|
||||
|
||||
// Impersonation touches thread-local state, so we need to lock until the
|
||||
// client access token has been extracted.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
if err := impersonateNamedPipeClient(h); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() {
|
||||
// Revert the current thread's impersonation.
|
||||
if err := windows.RevertToSelf(); err != nil {
|
||||
panic(fmt.Errorf("could not revert impersonation: %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
// Extract the client's access token from the thread-local state.
|
||||
var token windows.Token
|
||||
if err := windows.OpenThreadToken(windows.CurrentThread(), windows.TOKEN_DUPLICATE|windows.TOKEN_QUERY, true, &token); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
//sys getNamedPipeClientProcessId(h windows.Handle, clientPid *uint32) (err error) [int32(failretval)==0] = kernel32.GetNamedPipeClientProcessId
|
||||
//sys impersonateNamedPipeClient(h windows.Handle) (err error) [int32(failretval)==0] = advapi32.ImpersonateNamedPipeClient
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
|
||||
package safesocket
|
||||
|
||||
import "tailscale.com/util/winutil"
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// downgradeSDDL is a test helper that downgrades the windowsSDDL variable if
|
||||
@@ -20,3 +25,84 @@ func init() {
|
||||
return func() {}
|
||||
}
|
||||
}
|
||||
|
||||
// TestExpectedWindowsTypes is a copy of TestBasics specialized for Windows with
|
||||
// type assertions about the types of listeners and conns we expect.
|
||||
func TestExpectedWindowsTypes(t *testing.T) {
|
||||
t.Cleanup(downgradeSDDL())
|
||||
const sock = `\\.\pipe\tailscale-test`
|
||||
ln, err := Listen(sock)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := fmt.Sprintf("%T", ln), "*safesocket.winIOPipeListener"; got != want {
|
||||
t.Errorf("got listener type %q; want %q", got, want)
|
||||
}
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
s, err := ln.Accept()
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
ln.Close()
|
||||
|
||||
wcc, ok := s.(*WindowsClientConn)
|
||||
if !ok {
|
||||
s.Close()
|
||||
errs <- fmt.Errorf("accepted type %T; want WindowsClientConn", s)
|
||||
return
|
||||
}
|
||||
if wcc.winioPipeConn.Fd() == 0 {
|
||||
t.Error("accepted conn had unexpected zero fd")
|
||||
}
|
||||
if wcc.token == 0 {
|
||||
t.Error("accepted conn had unexpected zero token")
|
||||
}
|
||||
|
||||
s.Write([]byte("hello"))
|
||||
|
||||
b := make([]byte, 1024)
|
||||
n, err := s.Read(b)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
t.Logf("server read %d bytes.", n)
|
||||
if string(b[:n]) != "world" {
|
||||
errs <- fmt.Errorf("got %#v, expected %#v\n", string(b[:n]), "world")
|
||||
return
|
||||
}
|
||||
s.Close()
|
||||
errs <- nil
|
||||
}()
|
||||
|
||||
go func() {
|
||||
s := DefaultConnectionStrategy(sock)
|
||||
c, err := Connect(s)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
c.Write([]byte("world"))
|
||||
b := make([]byte, 1024)
|
||||
n, err := c.Read(b)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
if string(b[:n]) != "hello" {
|
||||
errs <- fmt.Errorf("got %#v, expected %#v\n", string(b[:n]), "hello")
|
||||
}
|
||||
c.Close()
|
||||
errs <- nil
|
||||
}()
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := <-errs; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
62
safesocket/zsyscall_windows.go
Normal file
62
safesocket/zsyscall_windows.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Code generated by 'go generate'; DO NOT EDIT.
|
||||
|
||||
package safesocket
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
// TODO: add more here, after collecting data on the common
|
||||
// error values see on Windows. (perhaps when running
|
||||
// all.bat?)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modadvapi32 = windows.NewLazySystemDLL("advapi32.dll")
|
||||
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
|
||||
procImpersonateNamedPipeClient = modadvapi32.NewProc("ImpersonateNamedPipeClient")
|
||||
procGetNamedPipeClientProcessId = modkernel32.NewProc("GetNamedPipeClientProcessId")
|
||||
)
|
||||
|
||||
func impersonateNamedPipeClient(h windows.Handle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procImpersonateNamedPipeClient.Addr(), 1, uintptr(h), 0, 0)
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getNamedPipeClientProcessId(h windows.Handle, clientPid *uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procGetNamedPipeClientProcessId.Addr(), 2, uintptr(h), uintptr(unsafe.Pointer(clientPid)), 0)
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -16,4 +16,4 @@
|
||||
) {
|
||||
src = ./.;
|
||||
}).shellNix
|
||||
# nix-direnv cache busting line: sha256-tzMLCNvIjG5e2aslmMt8GWgnfImd0J2a11xutOe59Ss=
|
||||
# nix-direnv cache busting line: sha256-WGZkpffwe4I8FewdBHXGaLbKQP/kHr7UF2lCXBTcNb4=
|
||||
|
||||
@@ -533,7 +533,9 @@ func TestAddAndDelNetfilterChains(t *testing.T) {
|
||||
checkChains(t, conn, nftables.TableFamilyIPv6, 0)
|
||||
|
||||
runner := newFakeNftablesRunner(t, conn)
|
||||
runner.AddChains()
|
||||
if err := runner.AddChains(); err != nil {
|
||||
t.Fatalf("runner.AddChains() failed: %v", err)
|
||||
}
|
||||
|
||||
tables, err := conn.ListTables()
|
||||
if err != nil {
|
||||
@@ -664,9 +666,13 @@ func TestNFTAddAndDelNetfilterBase(t *testing.T) {
|
||||
conn := newSysConn(t)
|
||||
|
||||
runner := newFakeNftablesRunner(t, conn)
|
||||
runner.AddChains()
|
||||
if err := runner.AddChains(); err != nil {
|
||||
t.Fatalf("AddChains() failed: %v", err)
|
||||
}
|
||||
defer runner.DelChains()
|
||||
runner.AddBase("testTunn")
|
||||
if err := runner.AddBase("testTunn"); err != nil {
|
||||
t.Fatalf("AddBase() failed: %v", err)
|
||||
}
|
||||
|
||||
// check number of rules in each IPv4 TS chain
|
||||
inputV4, forwardV4, postroutingV4, err := getTsChains(conn, nftables.TableFamilyIPv4)
|
||||
@@ -754,7 +760,9 @@ func TestNFTAddAndDelLoopbackRule(t *testing.T) {
|
||||
conn := newSysConn(t)
|
||||
|
||||
runner := newFakeNftablesRunner(t, conn)
|
||||
runner.AddChains()
|
||||
if err := runner.AddChains(); err != nil {
|
||||
t.Fatalf("AddChains() failed: %v", err)
|
||||
}
|
||||
defer runner.DelChains()
|
||||
|
||||
inputV4, _, _, err := getTsChains(conn, nftables.TableFamilyIPv4)
|
||||
@@ -810,9 +818,13 @@ func TestNFTAddAndDelLoopbackRule(t *testing.T) {
|
||||
func TestNFTAddAndDelHookRule(t *testing.T) {
|
||||
conn := newSysConn(t)
|
||||
runner := newFakeNftablesRunner(t, conn)
|
||||
runner.AddChains()
|
||||
if err := runner.AddChains(); err != nil {
|
||||
t.Fatalf("AddChains() failed: %v", err)
|
||||
}
|
||||
defer runner.DelChains()
|
||||
runner.AddHooks()
|
||||
if err := runner.AddHooks(); err != nil {
|
||||
t.Fatalf("AddHooks() failed: %v", err)
|
||||
}
|
||||
|
||||
forwardChain, err := getChainFromTable(conn, runner.nft4.Filter, "FORWARD")
|
||||
if err != nil {
|
||||
|
||||
@@ -59,6 +59,9 @@ func IsMacSysExt() bool {
|
||||
return false
|
||||
}
|
||||
return isMacSysExt.Get(func() bool {
|
||||
if strings.Contains(os.Getenv("HOME"), "/Containers/io.tailscale.ipn.macsys/") {
|
||||
return true
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -76,6 +79,12 @@ func IsMacAppStore() bool {
|
||||
return false
|
||||
}
|
||||
return isMacAppStore.Get(func() bool {
|
||||
// Both macsys and app store versions can run CLI executable with
|
||||
// suffix /Contents/MacOS/Tailscale. Check $HOME to filter out running
|
||||
// as macsys.
|
||||
if strings.Contains(os.Getenv("HOME"), "/Containers/io.tailscale.ipn.macsys/") {
|
||||
return false
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return false
|
||||
|
||||
@@ -2134,6 +2134,12 @@ func (c *Conn) Close() error {
|
||||
// They will frequently have been closed already by a call to connBind.Close.
|
||||
c.pconn6.Close()
|
||||
c.pconn4.Close()
|
||||
if c.closeDisco4 != nil {
|
||||
c.closeDisco4.Close()
|
||||
}
|
||||
if c.closeDisco6 != nil {
|
||||
c.closeDisco6.Close()
|
||||
}
|
||||
|
||||
// Wait on goroutines updating right at the end, once everything is
|
||||
// already closed. We want everything else in the Conn to be
|
||||
|
||||
@@ -393,10 +393,14 @@ func (ns *Impl) UpdateNetstackIPs(nm *netmap.NetworkMap) {
|
||||
pa := tcpip.ProtocolAddress{
|
||||
AddressWithPrefix: ipp,
|
||||
}
|
||||
if ipp.Address.Unspecified() || ipp.Address.Len() == 16 {
|
||||
switch ipp.Address.Len() {
|
||||
case 16:
|
||||
pa.Protocol = ipv6.ProtocolNumber
|
||||
} else {
|
||||
case 4:
|
||||
pa.Protocol = ipv4.ProtocolNumber
|
||||
default:
|
||||
ns.logf("[unexpected] netstack: could not register IP %s without protocol: unknown IP length (%v)", ipp, ipp.Address.Len())
|
||||
continue
|
||||
}
|
||||
var err tcpip.Error
|
||||
err = ns.ipstack.AddProtocolAddress(nicID, pa, stack.AddressProperties{
|
||||
|
||||
@@ -123,8 +123,7 @@ type userspaceEngine struct {
|
||||
trimmedNodes map[key.NodePublic]bool // set of node keys of peers currently excluded from wireguard config
|
||||
sentActivityAt map[netip.Addr]*mono.Time // value is accessed atomically
|
||||
destIPActivityFuncs map[netip.Addr]func()
|
||||
statusBufioReader *bufio.Reader // reusable for UAPI
|
||||
lastStatusPollTime mono.Time // last time we polled the engine status
|
||||
lastStatusPollTime mono.Time // last time we polled the engine status
|
||||
|
||||
mu sync.Mutex // guards following; see lock order comment below
|
||||
netMap *netmap.NetworkMap // or nil
|
||||
|
||||
Reference in New Issue
Block a user