-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
serve.go
431 lines (390 loc) · 12.9 KB
/
serve.go
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipn
import (
"errors"
"fmt"
"net"
"net/netip"
"net/url"
"strconv"
"strings"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
)
// ServeConfigKey returns a StateKey that stores the
// JSON-encoded ServeConfig for a config profile.
func ServeConfigKey(profileID ProfileID) StateKey {
return StateKey("_serve/" + profileID)
}
// ServeConfig is the JSON type stored in the StateStore for
// StateKey "_serve/$PROFILE_ID" as returned by ServeConfigKey.
type ServeConfig struct {
// TCP are the list of TCP port numbers that tailscaled should handle for
// the Tailscale IP addresses. (not subnet routers, etc)
TCP map[uint16]*TCPPortHandler `json:",omitempty"`
// Web maps from "$SNI_NAME:$PORT" to a set of HTTP handlers
// keyed by mount point ("/", "/foo", etc)
Web map[HostPort]*WebServerConfig `json:",omitempty"`
// AllowFunnel is the set of SNI:port values for which funnel
// traffic is allowed, from trusted ingress peers.
AllowFunnel map[HostPort]bool `json:",omitempty"`
// Foreground is a map of an IPN Bus session ID to an alternate foreground
// serve config that's valid for the life of that WatchIPNBus session ID.
// This. This allows the config to specify ephemeral configs that are
// used in the CLI's foreground mode to ensure ungraceful shutdowns
// of either the client or the LocalBackend does not expose ports
// that users are not aware of.
Foreground map[string]*ServeConfig `json:",omitempty"`
// ETag is the checksum of the serve config that's populated
// by the LocalClient through the HTTP ETag header during a
// GetServeConfig request and is translated to an If-Match header
// during a SetServeConfig request.
ETag string `json:"-"`
}
// HostPort is an SNI name and port number, joined by a colon.
// There is no implicit port 443. It must contain a colon.
type HostPort string
// Port extracts just the port number from hp.
// An error is reported in the case that the hp does not
// have a valid numeric port ending.
func (hp HostPort) Port() (uint16, error) {
_, port, err := net.SplitHostPort(string(hp))
if err != nil {
return 0, err
}
port16, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return 0, err
}
return uint16(port16), nil
}
// A FunnelConn wraps a net.Conn that is coming over a
// Funnel connection. It can be used to determine further
// information about the connection, like the source address
// and the target SNI name.
type FunnelConn struct {
// Conn is the underlying connection.
net.Conn
// Target is what was presented in the "Tailscale-Ingress-Target"
// HTTP header.
Target HostPort
// Src is the source address of the connection.
// This is the address of the client that initiated the
// connection, not the address of the Tailscale Funnel
// node which is relaying the connection. That address
// can be found in Conn.RemoteAddr.
Src netip.AddrPort
}
// WebServerConfig describes a web server's configuration.
type WebServerConfig struct {
Handlers map[string]*HTTPHandler // mountPoint => handler
}
// TCPPortHandler describes what to do when handling a TCP
// connection.
type TCPPortHandler struct {
// HTTPS, if true, means that tailscaled should handle this connection as an
// HTTPS request as configured by ServeConfig.Web.
//
// It is mutually exclusive with TCPForward.
HTTPS bool `json:",omitempty"`
// HTTP, if true, means that tailscaled should handle this connection as an
// HTTP request as configured by ServeConfig.Web.
//
// It is mutually exclusive with TCPForward.
HTTP bool `json:",omitempty"`
// TCPForward is the IP:port to forward TCP connections to.
// Whether or not TLS is terminated by tailscaled depends on
// TerminateTLS.
//
// It is mutually exclusive with HTTPS.
TCPForward string `json:",omitempty"`
// TerminateTLS, if non-empty, means that tailscaled should terminate the
// TLS connections before forwarding them to TCPForward, permitting only the
// SNI name with this value. It is only used if TCPForward is non-empty.
// (the HTTPS mode uses ServeConfig.Web)
TerminateTLS string `json:",omitempty"`
}
// HTTPHandler is either a path or a proxy to serve.
type HTTPHandler struct {
// Exactly one of the following may be set.
Path string `json:",omitempty"` // absolute path to directory or file to serve
Proxy string `json:",omitempty"` // http://localhost:3000/, localhost:3030, 3030
Text string `json:",omitempty"` // plaintext to serve (primarily for testing)
// TODO(bradfitz): bool to not enumerate directories? TTL on mapping for
// temporary ones? Error codes? Redirects?
}
// WebHandlerExists reports whether if the ServeConfig Web handler exists for
// the given host:port and mount point.
func (sc *ServeConfig) WebHandlerExists(hp HostPort, mount string) bool {
h := sc.GetWebHandler(hp, mount)
return h != nil
}
// GetWebHandler returns the HTTPHandler for the given host:port and mount point.
// Returns nil if the handler does not exist.
func (sc *ServeConfig) GetWebHandler(hp HostPort, mount string) *HTTPHandler {
if sc == nil || sc.Web[hp] == nil {
return nil
}
return sc.Web[hp].Handlers[mount]
}
// GetTCPPortHandler returns the TCPPortHandler for the given port.
// If the port is not configured, nil is returned.
func (sc *ServeConfig) GetTCPPortHandler(port uint16) *TCPPortHandler {
if sc == nil {
return nil
}
return sc.TCP[port]
}
// 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 {
if sc == nil || len(sc.TCP) == 0 {
return false
}
for _, h := range sc.TCP {
if h.TCPForward != "" {
return true
}
}
return false
}
// IsTCPForwardingOnPort reports whether if ServeConfig is currently forwarding
// in TCPForward mode on the given port. This is exclusive of Web/HTTPS serving.
func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16) bool {
if sc == nil || sc.TCP[port] == nil {
return false
}
return !sc.IsServingWeb(port)
}
// IsServingWeb reports whether if ServeConfig is currently serving Web
// (HTTP/HTTPS) on the given port. This is exclusive of TCPForwarding.
func (sc *ServeConfig) IsServingWeb(port uint16) bool {
return sc.IsServingHTTP(port) || sc.IsServingHTTPS(port)
}
// IsServingHTTPS reports whether if ServeConfig is currently serving HTTPS on
// the given port. This is exclusive of HTTP and TCPForwarding.
func (sc *ServeConfig) IsServingHTTPS(port uint16) bool {
if sc == nil || sc.TCP[port] == nil {
return false
}
return sc.TCP[port].HTTPS
}
// IsServingHTTP reports whether if ServeConfig is currently serving HTTP on the
// given port. This is exclusive of HTTPS and TCPForwarding.
func (sc *ServeConfig) IsServingHTTP(port uint16) bool {
if sc == nil || sc.TCP[port] == nil {
return false
}
return sc.TCP[port].HTTP
}
// IsFunnelOn reports whether if ServeConfig is currently allowing funnel
// traffic for any host:port.
//
// View version of ServeConfig.IsFunnelOn.
func (v ServeConfigView) IsFunnelOn() bool { return v.ж.IsFunnelOn() }
// IsFunnelOn reports whether if ServeConfig is currently allowing funnel
// traffic for any host:port.
func (sc *ServeConfig) IsFunnelOn() bool {
if sc == nil {
return false
}
for _, b := range sc.AllowFunnel {
if b {
return true
}
}
return false
}
// CheckFunnelAccess checks whether Funnel access is allowed for the given node
// and port.
// It checks:
// 1. HTTPS is enabled on the Tailnet
// 2. the node has the "funnel" nodeAttr
// 3. the port is allowed for Funnel
//
// The node arg should be the ipnstate.Status.Self node.
func CheckFunnelAccess(port uint16, node *ipnstate.PeerStatus) error {
if !node.HasCap(tailcfg.CapabilityHTTPS) {
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.")
}
if !node.HasCap(tailcfg.NodeAttrFunnel) {
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/s/no-funnel.")
}
return CheckFunnelPort(port, node)
}
// CheckFunnelPort checks whether the given port is allowed for Funnel.
// It uses the tailcfg.CapabilityFunnelPorts nodeAttr to determine the allowed
// ports.
func CheckFunnelPort(wantedPort uint16, node *ipnstate.PeerStatus) error {
deny := func(allowedPorts string) error {
if allowedPorts == "" {
return fmt.Errorf("port %d is not allowed for funnel", wantedPort)
}
return fmt.Errorf("port %d is not allowed for funnel; allowed ports are: %v", wantedPort, allowedPorts)
}
var portsStr string
parseAttr := func(attr string) (string, error) {
u, err := url.Parse(attr)
if err != nil {
return "", deny("")
}
portsStr := u.Query().Get("ports")
if portsStr == "" {
return "", deny("")
}
u.RawQuery = ""
if u.String() != string(tailcfg.CapabilityFunnelPorts) {
return "", deny("")
}
return portsStr, nil
}
for attr := range node.CapMap {
attr := string(attr)
if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
continue
}
var err error
portsStr, err = parseAttr(attr)
if err != nil {
return err
}
break
}
if portsStr == "" {
for _, attr := range node.Capabilities {
attr := string(attr)
if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
continue
}
var err error
portsStr, err = parseAttr(attr)
if err != nil {
return err
}
break
}
}
if portsStr == "" {
return deny("")
}
wantedPortString := strconv.Itoa(int(wantedPort))
for _, ps := range strings.Split(portsStr, ",") {
if ps == "" {
continue
}
first, last, ok := strings.Cut(ps, "-")
if !ok {
if first == wantedPortString {
return nil
}
continue
}
fp, err := strconv.ParseUint(first, 10, 16)
if err != nil {
continue
}
lp, err := strconv.ParseUint(last, 10, 16)
if err != nil {
continue
}
pr := tailcfg.PortRange{First: uint16(fp), Last: uint16(lp)}
if pr.Contains(wantedPort) {
return nil
}
}
return deny(portsStr)
}
// RangeOverTCPs ranges over both background and foreground TCPs.
// If the returned bool from the given f is false, then this function stops
// iterating immediately and does not check other foreground configs.
func (v ServeConfigView) RangeOverTCPs(f func(port uint16, _ TCPPortHandlerView) bool) {
parentCont := true
v.TCP().Range(func(k uint16, v TCPPortHandlerView) (cont bool) {
parentCont = f(k, v)
return parentCont
})
v.Foreground().Range(func(k string, v ServeConfigView) (cont bool) {
if !parentCont {
return false
}
v.TCP().Range(func(k uint16, v TCPPortHandlerView) (cont bool) {
parentCont = f(k, v)
return parentCont
})
return parentCont
})
}
// RangeOverWebs ranges over both background and foreground Webs.
// If the returned bool from the given f is false, then this function stops
// iterating immediately and does not check other foreground configs.
func (v ServeConfigView) RangeOverWebs(f func(_ HostPort, conf WebServerConfigView) bool) {
parentCont := true
v.Web().Range(func(k HostPort, v WebServerConfigView) (cont bool) {
parentCont = f(k, v)
return parentCont
})
v.Foreground().Range(func(k string, v ServeConfigView) (cont bool) {
if !parentCont {
return false
}
v.Web().Range(func(k HostPort, v WebServerConfigView) (cont bool) {
parentCont = f(k, v)
return parentCont
})
return parentCont
})
}
// FindTCP returns the first TCP that matches with the given port. It
// prefers a foreground match first followed by a background search if none
// existed.
func (v ServeConfigView) FindTCP(port uint16) (res TCPPortHandlerView, ok bool) {
v.Foreground().Range(func(_ string, v ServeConfigView) (cont bool) {
res, ok = v.TCP().GetOk(port)
return !ok
})
if ok {
return res, ok
}
return v.TCP().GetOk(port)
}
// FindWeb returns the first Web that matches with the given HostPort. It
// prefers a foreground match first followed by a background search if none
// existed.
func (v ServeConfigView) FindWeb(hp HostPort) (res WebServerConfigView, ok bool) {
v.Foreground().Range(func(_ string, v ServeConfigView) (cont bool) {
res, ok = v.Web().GetOk(hp)
return !ok
})
if ok {
return res, ok
}
return v.Web().GetOk(hp)
}
// HasAllowFunnel returns whether this config has at least one AllowFunnel
// set in the background or foreground configs.
func (v ServeConfigView) HasAllowFunnel() bool {
return v.AllowFunnel().Len() > 0 || func() bool {
var exists bool
v.Foreground().Range(func(k string, v ServeConfigView) (cont bool) {
exists = v.AllowFunnel().Len() > 0
return !exists
})
return exists
}()
}
// FindFunnel reports whether target exists in in either the background AllowFunnel
// or any of the foreground configs.
func (v ServeConfigView) HasFunnelForTarget(target HostPort) bool {
if v.AllowFunnel().Get(target) {
return true
}
var exists bool
v.Foreground().Range(func(_ string, v ServeConfigView) (cont bool) {
if exists = v.AllowFunnel().Get(target); exists {
return false
}
return true
})
return exists
}