summaryrefslogtreecommitdiffhomepage
path: root/ssh/tailssh/incubator_linux.go
blob: f9abdb27258e4cd4e418617787c29374ed4ff762 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

//go:build linux

package tailssh

import (
	"context"
	"fmt"
	"os"
	"syscall"
	"time"
	"unsafe"

	"github.com/godbus/dbus/v5"
	"tailscale.com/types/logger"
)

func init() {
	ptyName = ptyNameLinux
	maybeStartLoginSession = maybeStartLoginSessionLinux
}

func ptyNameLinux(f *os.File) (string, error) {
	var n uint32
	_, _, e := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n)))
	if e != 0 {
		return "", e
	}
	return fmt.Sprintf("pts/%d", n), nil
}

// callLogin1 invokes the provided method of the "login1" service over D-Bus.
// https://www.freedesktop.org/software/systemd/man/org.freedesktop.login1.html
func callLogin1(method string, flags dbus.Flags, args ...any) (*dbus.Call, error) {
	conn, err := dbus.SystemBus()
	if err != nil {
		// DBus probably not running.
		return nil, err
	}

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	name, objectPath := "org.freedesktop.login1", "/org/freedesktop/login1"
	obj := conn.Object(name, dbus.ObjectPath(objectPath))
	call := obj.CallWithContext(ctx, method, flags, args...)
	if call.Err != nil {
		return nil, call.Err
	}
	return call, nil
}

// createSessionArgs is a wrapper struct for the Login1.Manager.CreateSession args.
// The CreateSession API arguments and response types are defined here:
// https://www.freedesktop.org/software/systemd/man/org.freedesktop.login1.html
type createSessionArgs struct {
	uid        uint32     // User ID being logged in.
	pid        uint32     // Process ID for the session, 0 means current process.
	service    string     // Service creating the session.
	typ        string     // Type of login (oneof unspecified, tty, x11).
	class      string     // Type of session class (oneof user, greeter, lock-screen).
	desktop    string     // the desktop environment.
	seat       string     // the seat this session belongs to, empty otherwise.
	vtnr       uint32     // the virtual terminal number of the session if there is any, 0 otherwise.
	tty        string     // the kernel TTY path of the session if this is a text login, empty otherwise.
	display    string     // the X11 display name if this is a graphical login, empty otherwise.
	remote     bool       // whether the session is remote.
	remoteUser string     // the remote user if this is a remote session, empty otherwise.
	remoteHost string     // the remote host if this is a remote session, empty otherwise.
	properties []struct { // This is unused and exists just to make the marshaling work
		S string
		V dbus.Variant
	}
}

func (a createSessionArgs) args() []any {
	return []any{
		a.uid,
		a.pid,
		a.service,
		a.typ,
		a.class,
		a.desktop,
		a.seat,
		a.vtnr,
		a.tty,
		a.display,
		a.remote,
		a.remoteUser,
		a.remoteHost,
		a.properties,
	}
}

// createSessionResp is a wrapper struct for the Login1.Manager.CreateSession response.
// The CreateSession API arguments and response types are defined here:
// https://www.freedesktop.org/software/systemd/man/org.freedesktop.login1.html
type createSessionResp struct {
	sessionID   string
	objectPath  dbus.ObjectPath
	runtimePath string
	fifoFD      dbus.UnixFD
	uid         uint32
	seatID      string
	vtnr        uint32
	existing    bool // whether a new session was created.
}

// createSession creates a tty user login session for the provided uid.
func createSession(uid uint32, remoteUser, remoteHost, tty string) (createSessionResp, error) {
	a := createSessionArgs{
		uid:        uid,
		service:    "tailscaled",
		typ:        "tty",
		class:      "user",
		tty:        tty,
		remote:     true,
		remoteUser: remoteUser,
		remoteHost: remoteHost,
	}

	call, err := callLogin1("org.freedesktop.login1.Manager.CreateSession", 0, a.args()...)
	if err != nil {
		return createSessionResp{}, err
	}

	return createSessionResp{
		sessionID:   call.Body[0].(string),
		objectPath:  call.Body[1].(dbus.ObjectPath),
		runtimePath: call.Body[2].(string),
		fifoFD:      call.Body[3].(dbus.UnixFD),
		uid:         call.Body[4].(uint32),
		seatID:      call.Body[5].(string),
		vtnr:        call.Body[6].(uint32),
		existing:    call.Body[7].(bool),
	}, nil
}

// releaseSession releases the session identified by sessionID.
func releaseSession(sessionID string) error {
	// https://www.freedesktop.org/software/systemd/man/org.freedesktop.login1.html
	_, err := callLogin1("org.freedesktop.login1.Manager.ReleaseSession", dbus.FlagNoReplyExpected, sessionID)
	return err
}

// maybeStartLoginSessionLinux is the linux implementation of maybeStartLoginSession.
func maybeStartLoginSessionLinux(logf logger.Logf, ia incubatorArgs) (func() error, error) {
	if os.Geteuid() != 0 {
		return nil, nil
	}
	logf("starting session for user %d", ia.uid)
	// The only way we can actually start a new session is if we are
	// running outside one and are root, which is typically the case
	// for systemd managed tailscaled.
	resp, err := createSession(uint32(ia.uid), ia.remoteUser, ia.remoteIP, ia.ttyName)
	if err != nil {
		// TODO(maisem): figure out if we are running in a session.
		// We can look at the DBus GetSessionByPID API.
		// https://www.freedesktop.org/software/systemd/man/org.freedesktop.login1.html
		// For now best effort is fine.
		logf("ssh: failed to CreateSession for user %q (%d) %v", ia.localUser, ia.uid, err)
		return nil, nil
	}
	os.Setenv("DBUS_SESSION_BUS_ADDRESS", fmt.Sprintf("unix:path=%v/bus", resp.runtimePath))
	if !resp.existing {
		return func() error {
			return releaseSession(resp.sessionID)
		}, nil
	}
	return nil, nil
}